Skip to content

Commit 79c5125

Browse files
Merge branch 'canary' into add/tracing-improvements
2 parents 0e3b271 + 24b09ad commit 79c5125

File tree

36 files changed

+64960
-63850
lines changed

36 files changed

+64960
-63850
lines changed

docs/basic-features/eslint.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Next.js provides an ESLint plugin, [`eslint-plugin-next`](https://www.npmjs.com/
9595
| ✔️ | [next/no-title-in-document-head](https://nextjs.org/docs/messages/no-title-in-document-head) | Disallow using <title> with Head from next/document |
9696
| ✔️ | [next/no-unwanted-polyfillio](https://nextjs.org/docs/messages/no-unwanted-polyfillio) | Prevent duplicate polyfills from Polyfill.io |
9797
| ✔️ | next/no-typos | Ensure no typos were made declaring [Next.js's data fetching function](https://nextjs.org/docs/basic-features/data-fetching) |
98+
| ✔️ | [next/next-script-for-ga](https://nextjs.org/docs/messages/next-script-for-ga) | Use the Script component to defer loading of the script until necessary. |
9899

99100
- ✔: Enabled in the recommended configuration
100101

docs/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Cypress is a test runner used for **End-to-End (E2E)** and **Integration Testing
1616

1717
### Quickstart
1818

19-
You can use `create-next-app` with the [with-cypress example](https://github.com/vercel/next.js/tree/canary/examples/with-jest) to quickly get started.
19+
You can use `create-next-app` with the [with-cypress example](https://github.com/vercel/next.js/tree/canary/examples/with-cypress) to quickly get started.
2020

2121
```bash
2222
npx create-next-app --example with-cypress with-cypress-app

errors/next-script-for-ga.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Next Script for Google Analytics
2+
3+
### Why This Error Occurred
4+
5+
An inline script was used for Google analytics which might impact your webpage's performance.
6+
7+
### Possible Ways to Fix It
8+
9+
#### Using gtag.js
10+
11+
If you are using the [gtag.js](https://developers.google.com/analytics/devguides/collection/gtagjs) script to add analytics, use the `next/script` component with the right loading strategy to defer loading of the script until necessary.
12+
13+
```jsx
14+
import Script from 'next/script'
15+
16+
const Home = () => {
17+
return (
18+
<div class="container">
19+
<!-- Global site tag (gtag.js) - Google Analytics -->
20+
<Script
21+
src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
22+
strategy="lazyOnload"
23+
></Script>
24+
<Script>
25+
{`
26+
window.dataLayer = window.dataLayer || [];
27+
function gtag(){window.dataLayer.push(arguments);}
28+
gtag('js', new Date());
29+
30+
gtag('config', 'GA_MEASUREMENT_ID');
31+
`}
32+
</Script>
33+
</div>
34+
)
35+
}
36+
37+
export default Home
38+
```
39+
40+
#### Using analytics.js
41+
42+
If you are using the [analytics.js](https://developers.google.com/analytics/devguides/collection/analyticsjs) script to add analytics:
43+
44+
```jsx
45+
import Script from 'next/script'
46+
47+
const Home = () => {
48+
return (
49+
<div class="container">
50+
<Script>
51+
{`
52+
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
53+
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
54+
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
55+
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
56+
57+
ga('create', 'UA-XXXXX-Y', 'auto');
58+
ga('send', 'pageview');
59+
`}
60+
</Script>
61+
</div>
62+
)
63+
}
64+
65+
export default Home
66+
```
67+
68+
If you are using the [alternative async variant](https://developers.google.com/analytics/devguides/collection/analyticsjs#alternative_async_tag):
69+
70+
```jsx
71+
import Script from 'next/script'
72+
73+
const Home = () => {
74+
return (
75+
<div class="container">
76+
<Script>
77+
{`
78+
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
79+
ga('create', 'GOOGLE_ANALYTICS_ID', 'auto');
80+
ga('send', 'pageview');
81+
`}
82+
</Script>
83+
<Script
84+
src="https://www.google-analytics.com/analytics.js"
85+
strategy="lazyOnload"
86+
></Script>
87+
</div>
88+
)
89+
}
90+
91+
export default Home
92+
```
93+
94+
### Useful Links
95+
96+
- [Add analytics.js to Your Site](https://developers.google.com/analytics/devguides/collection/analyticsjs)
97+
- [Efficiently load third-party JavaScript](https://web.dev/efficiently-load-third-party-javascript/)
98+
- [next/script Documentation](https://nextjs.org/docs/basic-features/script)

packages/eslint-plugin-next/lib/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module.exports = {
1616
'no-script-in-head': require('./rules/no-script-in-head'),
1717
'no-typos': require('./rules/no-typos'),
1818
'no-duplicate-head': require('./rules/no-duplicate-head'),
19+
'next-script-for-ga': require('./rules/next-script-for-ga'),
1920
},
2021
configs: {
2122
recommended: {
@@ -31,6 +32,7 @@ module.exports = {
3132
'@next/next/google-font-display': 1,
3233
'@next/next/google-font-preconnect': 1,
3334
'@next/next/link-passhref': 1,
35+
'@next/next/next-script-for-ga': 1,
3436
'@next/next/no-document-import-in-page': 2,
3537
'@next/next/no-head-import-in-document': 2,
3638
'@next/next/no-script-in-document': 2,
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const NodeAttributes = require('../utils/node-attributes.js')
2+
3+
const SUPPORTED_SRCS = [
4+
'www.google-analytics.com/analytics.js',
5+
'www.googletagmanager.com/gtag/js',
6+
]
7+
const SUPPORTED_HTML_CONTENT_URLS = [
8+
'www.google-analytics.com/analytics.js',
9+
'www.googletagmanager.com/gtm.js',
10+
]
11+
const ERROR_MSG =
12+
'Use the `next/script` component for loading third party scripts. See: https://nextjs.org/docs/messages/next-script-for-ga.'
13+
14+
// Check if one of the items in the list is a substring of the passed string
15+
const containsStr = (str, strList) => {
16+
return strList.some((s) => str.includes(s))
17+
}
18+
19+
module.exports = {
20+
meta: {
21+
docs: {
22+
description:
23+
'Prefer next script component when using the inline script for Google Analytics',
24+
recommended: true,
25+
},
26+
},
27+
create: function (context) {
28+
return {
29+
JSXOpeningElement(node) {
30+
if (node.name.name !== 'script') {
31+
return
32+
}
33+
if (node.attributes.length === 0) {
34+
return
35+
}
36+
const attributes = new NodeAttributes(node)
37+
38+
// Check if the Alternative async tag is being used to add GA.
39+
// https://developers.google.com/analytics/devguides/collection/analyticsjs#alternative_async_tag
40+
// https://developers.google.com/analytics/devguides/collection/gtagjs
41+
if (
42+
typeof attributes.value('src') === 'string' &&
43+
containsStr(attributes.value('src'), SUPPORTED_SRCS)
44+
) {
45+
return context.report({
46+
node,
47+
message: ERROR_MSG,
48+
})
49+
}
50+
51+
// Check if inline script is being used to add GA.
52+
// https://developers.google.com/analytics/devguides/collection/analyticsjs#the_google_analytics_tag
53+
// https://developers.google.com/tag-manager/quickstart
54+
if (
55+
attributes.has('dangerouslySetInnerHTML') &&
56+
attributes.value('dangerouslySetInnerHTML')[0]
57+
) {
58+
const htmlContent =
59+
attributes.value('dangerouslySetInnerHTML')[0].value.quasis &&
60+
attributes.value('dangerouslySetInnerHTML')[0].value.quasis[0].value
61+
.raw
62+
if (
63+
htmlContent &&
64+
containsStr(htmlContent, SUPPORTED_HTML_CONTENT_URLS)
65+
) {
66+
context.report({
67+
node,
68+
message: ERROR_MSG,
69+
})
70+
}
71+
}
72+
},
73+
}
74+
},
75+
}
76+
77+
module.exports.schema = []

packages/eslint-plugin-next/lib/rules/no-document-import-in-page.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ module.exports = {
1616
}
1717

1818
const page = context.getFilename().split('pages')[1]
19+
if (!page) {
20+
return
21+
}
1922
const { name, dir } = path.parse(page)
2023
if (
21-
!page ||
2224
name.startsWith('_document') ||
2325
(dir === '/_document' && name === 'index')
2426
) {

packages/eslint-plugin-next/lib/utils/node-attributes.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ class NodeAttributes {
2626
this.attributes[attribute.name.name].value = attribute.value.value
2727
} else if (attribute.value.expression) {
2828
this.attributes[attribute.name.name].value =
29-
attribute.value.expression.value
29+
typeof attribute.value.expression.value !== 'undefined'
30+
? attribute.value.expression.value
31+
: attribute.value.expression.properties
3032
}
3133
}
3234
})

packages/next/build/index.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,8 @@ export default async function build(
660660
const serverPropsPages = new Set<string>()
661661
const additionalSsgPaths = new Map<string, Array<string>>()
662662
const additionalSsgPathsEncoded = new Map<string, Array<string>>()
663+
const pageTraceIncludes = new Map<string, Array<string>>()
664+
const pageTraceExcludes = new Map<string, Array<string>>()
663665
const pageInfos = new Map<string, PageInfo>()
664666
const pagesManifest = JSON.parse(
665667
await promises.readFile(manifestPath, 'utf8')
@@ -841,6 +843,11 @@ export default async function build(
841843
)
842844
})
843845

846+
if (config.experimental.nftTracing) {
847+
pageTraceIncludes.set(page, workerResult.traceIncludes || [])
848+
pageTraceExcludes.set(page, workerResult.traceExcludes || [])
849+
}
850+
844851
if (
845852
workerResult.isStatic === false &&
846853
(workerResult.isHybridAmp || workerResult.isAmpOnly)
@@ -979,6 +986,63 @@ export default async function build(
979986
)
980987
}
981988

989+
if (config.experimental.nftTracing) {
990+
const globOrig = require('next/dist/compiled/glob') as typeof import('next/dist/compiled/glob')
991+
const glob = (pattern: string): Promise<string[]> => {
992+
return new Promise((resolve, reject) => {
993+
globOrig(pattern, { cwd: dir }, (err, files) => {
994+
if (err) {
995+
return reject(err)
996+
}
997+
resolve(files)
998+
})
999+
})
1000+
}
1001+
1002+
for (const page of pageKeys) {
1003+
const includeGlobs = pageTraceIncludes.get(page)
1004+
const excludeGlobs = pageTraceExcludes.get(page)
1005+
1006+
if (!includeGlobs?.length && !excludeGlobs?.length) {
1007+
continue
1008+
}
1009+
1010+
const traceFile = path.join(
1011+
distDir,
1012+
'server/pages',
1013+
`${page}.js.nft.json`
1014+
)
1015+
const traceContent = JSON.parse(
1016+
await promises.readFile(traceFile, 'utf8')
1017+
)
1018+
let includes: string[] = []
1019+
let excludes: string[] = []
1020+
1021+
if (includeGlobs?.length) {
1022+
for (const includeGlob of includeGlobs) {
1023+
includes.push(...(await glob(includeGlob)))
1024+
}
1025+
}
1026+
1027+
if (excludeGlobs?.length) {
1028+
for (const excludeGlob of excludeGlobs) {
1029+
excludes.push(...(await glob(excludeGlob)))
1030+
}
1031+
}
1032+
1033+
const combined = new Set([...traceContent.files, ...includes])
1034+
excludes.forEach((file) => combined.delete(file))
1035+
1036+
await promises.writeFile(
1037+
traceFile,
1038+
JSON.stringify({
1039+
version: traceContent.version,
1040+
files: [...combined],
1041+
})
1042+
)
1043+
}
1044+
}
1045+
9821046
if (serverPropsPages.size > 0 || ssgPages.size > 0) {
9831047
// We update the routes manifest after the build with the
9841048
// data routes since we can't determine these until after build

packages/next/build/utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { getRouteMatcher, getRouteRegex } from '../shared/lib/router/utils'
2323
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
2424
import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters'
2525
import { findPageFile } from '../server/lib/find-page-file'
26-
import { GetStaticPaths } from 'next/types'
26+
import { GetStaticPaths, PageConfig } from 'next/types'
2727
import { denormalizePagePath } from '../server/normalize-page-path'
2828
import { BuildManifest } from '../server/get-page-files'
2929
import { removePathTrailingSlash } from '../client/normalize-trailing-slash'
@@ -831,6 +831,8 @@ export async function isPageStatic(
831831
encodedPrerenderRoutes?: string[]
832832
prerenderFallback?: boolean | 'blocking'
833833
isNextImageImported?: boolean
834+
traceIncludes?: string[]
835+
traceExcludes?: string[]
834836
}> {
835837
const isPageStaticSpan = trace('is-page-static-utils', parentId)
836838
return isPageStaticSpan.traceAsyncFn(async () => {
@@ -925,7 +927,7 @@ export async function isPageStatic(
925927
}
926928

927929
const isNextImageImported = (global as any).__NEXT_IMAGE_IMPORTED
928-
const config = mod.config || {}
930+
const config: PageConfig = mod.config || {}
929931
return {
930932
isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps,
931933
isHybridAmp: config.amp === 'hybrid',
@@ -936,6 +938,8 @@ export async function isPageStatic(
936938
hasStaticProps,
937939
hasServerProps,
938940
isNextImageImported,
941+
traceIncludes: config.unstable_includeFiles || [],
942+
traceExcludes: config.unstable_excludeFiles || [],
939943
}
940944
} catch (err) {
941945
if (err.code === 'MODULE_NOT_FOUND') return {}

packages/next/build/webpack-config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import BuildStatsPlugin from './webpack/plugins/build-stats-plugin'
3838
import ChunkNamesPlugin from './webpack/plugins/chunk-names-plugin'
3939
import { JsConfigPathsPlugin } from './webpack/plugins/jsconfig-paths-plugin'
4040
import { DropClientPage } from './webpack/plugins/next-drop-client-page-plugin'
41+
import { TraceEntryPointsPlugin } from './webpack/plugins/next-trace-entrypoints-plugin'
4142
import NextJsSsrImportPlugin from './webpack/plugins/nextjs-ssr-import'
4243
import NextJsSSRModuleCachePlugin from './webpack/plugins/nextjs-ssr-module-cache'
4344
import PagesManifestPlugin from './webpack/plugins/pages-manifest-plugin'
@@ -1245,6 +1246,12 @@ export default async function getBaseWebpackConfig(
12451246
pagesDir,
12461247
}),
12471248
!isServer && new DropClientPage(),
1249+
config.experimental.nftTracing &&
1250+
!isLikeServerless &&
1251+
isServer &&
1252+
!dev &&
1253+
isWebpack5 &&
1254+
new TraceEntryPointsPlugin({ appDir: dir }),
12481255
// Moment.js is an extremely popular library that bundles large locale files
12491256
// by default due to how Webpack interprets its code. This is a practical
12501257
// solution that requires the user to opt into importing specific locales.

0 commit comments

Comments
 (0)