From ba96b9a4d3a6b70f8c5a0224f55cf778ea1ae658 Mon Sep 17 00:00:00 2001 From: Lee Robinson Date: Mon, 3 Oct 2022 22:24:35 -0500 Subject: [PATCH 001/135] Add note to incremental migration about dynamic routes + fallbacks (#41147) Addresses this discussion: https://github.com/vercel/next.js/discussions/38839 --- docs/migrating/incremental-adoption.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/migrating/incremental-adoption.md b/docs/migrating/incremental-adoption.md index dbbd2e153a96e..e7bf9ae047635 100644 --- a/docs/migrating/incremental-adoption.md +++ b/docs/migrating/incremental-adoption.md @@ -33,8 +33,6 @@ module.exports = { To learn more about `basePath`, take a look at our [documentation](/docs/api-reference/next.config.js/basepath.md). -> This feature was introduced in [Next.js 9.5](https://nextjs.org/blog/next-9-5) and up. If you’re using older versions of Next.js, please upgrade before trying it out. - ### Rewrites The second strategy is to create a new Next.js app that points to the root URL of your domain. Then, you can use [`rewrites`](/docs/api-reference/next.config.js/rewrites.md) inside `next.config.js` to have some subpaths to be proxied to your existing app. @@ -78,7 +76,7 @@ module.exports = { To learn more about rewrites, take a look at our [documentation](/docs/api-reference/next.config.js/rewrites.md). -> This feature was introduced in [Next.js 9.5](https://nextjs.org/blog/next-9-5) and up. If you’re using older versions of Next.js, please upgrade before trying it out. +> **Note:** If you are incrementally migrating to a dynamic route (e.g. `[slug].js`) and using `fallback: true` or `fallback: 'blocking'` along with a fallback `rewrite`, ensure you consider the case where pages are not found. When Next.js matches the dynamic route it stops checking any further routes. Using `notFound: true` in `getStaticProps` will return the 404 page without applying the fallback `rewrite`. If this is not desired, you can use `getServerSideProps` with `stale-while-revalidate` Cache-Control headers when returning your props. Then, you can _manually_ proxy to your existing backend using something like [http-proxy](https://github.com/vercel/next.js/discussions/38839#discussioncomment-3744442) instead of returning `notFound: true`. ### Micro-Frontends with Monorepos and Subdomains From 51552c1a029be31766e071b89bb4992d5d76627b Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 4 Oct 2022 01:59:35 -0700 Subject: [PATCH 002/135] Update minimum required Node.js version to v14 (#41150) As discussed this updates our required minimum Node.js version to `v14` as `v12` is no longer being maintained. Since our targets for babel and swc already target the actively used Node.js version no change has been made there. --- package.json | 2 +- packages/create-next-app/package.json | 2 +- packages/next/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index afd65280f288e..82ba1add548b6 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,7 @@ "@babel/traverse": "7.18.0" }, "engines": { - "node": ">=12.22.0" + "node": ">=14.0.0" }, "packageManager": "pnpm@7.3.0" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 3a0be8f67fd71..884abf945e56e 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -49,6 +49,6 @@ "validate-npm-package-name": "3.0.0" }, "engines": { - "node": ">=12.22.0" + "node": ">=14.0.0" } } diff --git a/packages/next/package.json b/packages/next/package.json index 76ddff6958130..80446a17e9dd4 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -281,6 +281,6 @@ "caniuse-lite": "1.0.30001406" }, "engines": { - "node": ">=12.22.0" + "node": ">=14.0.0" } } From 34b78dc7c54c9527f9643785ceb673ac8942260b Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 4 Oct 2022 16:03:20 +0200 Subject: [PATCH 003/135] Handle hmr for edge ssr in app dir (#41156) Include the edge server changes that starting in app dir into server components changes. Most changes are merging condition `isAppPath && this.appDir` into `isAppPath`. ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` --- packages/next/server/dev/hot-reloader.ts | 69 ++++++++++--------- test/e2e/app-dir/app-edge.test.ts | 24 ++++++- .../app-dir/app-edge/app/app-edge/layout.tsx | 2 + 3 files changed, 61 insertions(+), 34 deletions(-) diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index cdf1c7c00904a..0718a3633e19a 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -612,22 +612,21 @@ export default class HotReloader { onEdgeServer: () => { // TODO-APP: verify if child entry should support. if (!isEdgeServerCompilation || !isEntry) return - const appDirLoader = - isAppPath && this.appDir - ? getAppEntry({ - name: bundlePath, - appPaths: entryData.appPaths, - pagePath: posix.join( - APP_DIR_ALIAS, - relative( - this.appDir!, - entryData.absolutePagePath - ).replace(/\\/g, '/') - ), - appDir: this.appDir!, - pageExtensions: this.config.pageExtensions, - }).import - : undefined + const appDirLoader = isAppPath + ? getAppEntry({ + name: bundlePath, + appPaths: entryData.appPaths, + pagePath: posix.join( + APP_DIR_ALIAS, + relative( + this.appDir!, + entryData.absolutePagePath + ).replace(/\\/g, '/') + ), + appDir: this.appDir!, + pageExtensions: this.config.pageExtensions, + }).import + : undefined entries[entryKey].status = BUILDING entrypoints[bundlePath] = finalizeEntrypoint({ @@ -688,25 +687,24 @@ export default class HotReloader { } entrypoints[bundlePath] = finalizeEntrypoint({ - compilerType: 'server', + compilerType: COMPILER_NAMES.server, name: bundlePath, isServerComponent, - value: - this.appDir && bundlePath.startsWith('app/') - ? getAppEntry({ - name: bundlePath, - appPaths: entryData.appPaths, - pagePath: posix.join( - APP_DIR_ALIAS, - relative( - this.appDir!, - entryData.absolutePagePath - ).replace(/\\/g, '/') - ), - appDir: this.appDir!, - pageExtensions: this.config.pageExtensions, - }) - : relativeRequest, + value: isAppPath + ? getAppEntry({ + name: bundlePath, + appPaths: entryData.appPaths, + pagePath: posix.join( + APP_DIR_ALIAS, + relative( + this.appDir!, + entryData.absolutePagePath + ).replace(/\\/g, '/') + ), + appDir: this.appDir!, + pageExtensions: this.config.pageExtensions, + }) + : relativeRequest, appDir: this.config.experimental.appDir, }) }, @@ -895,7 +893,12 @@ export default class HotReloader { changedServerPages, changedClientPages ) + const edgeServerOnlyChanges = difference( + changedEdgeServerPages, + changedClientPages + ) const serverComponentChanges = serverOnlyChanges + .concat(edgeServerOnlyChanges) .filter((key) => key.startsWith('app/')) .concat(Array.from(changedCSSImportPages)) const pageChanges = serverOnlyChanges.filter((key) => diff --git a/test/e2e/app-dir/app-edge.test.ts b/test/e2e/app-dir/app-edge.test.ts index b0f9e3ba21c39..fc40aad53209e 100644 --- a/test/e2e/app-dir/app-edge.test.ts +++ b/test/e2e/app-dir/app-edge.test.ts @@ -1,6 +1,6 @@ import { createNext, FileRef } from 'e2e-utils' import { NextInstance } from 'test/lib/next-modes/base' -import { renderViaHTTP } from 'next-test-utils' +import { check, renderViaHTTP } from 'next-test-utils' import path from 'path' describe('app-dir edge SSR', () => { @@ -37,4 +37,26 @@ describe('app-dir edge SSR', () => { const pageHtml = await renderViaHTTP(next.url, '/pages-edge') expect(pageHtml).toContain('

pages-edge-ssr

') }) + + if ((globalThis as any).isNextDev) { + it('should handle edge rsc hmr', async () => { + const pageFile = 'app/app-edge/page.tsx' + const content = await next.readFile(pageFile) + + // Update rendered content + const updatedContent = content.replace('app-edge-ssr', 'edge-hmr') + await next.patchFile(pageFile, updatedContent) + await check(async () => { + const html = await renderViaHTTP(next.url, '/app-edge') + return html + }, /edge-hmr/) + + // Revert + await next.patchFile(pageFile, content) + await check(async () => { + const html = await renderViaHTTP(next.url, '/app-edge') + return html + }, /app-edge-ssr/) + }) + } }) diff --git a/test/e2e/app-dir/app-edge/app/app-edge/layout.tsx b/test/e2e/app-dir/app-edge/app/app-edge/layout.tsx index 3715b9398be5a..0b279c322ccbc 100644 --- a/test/e2e/app-dir/app-edge/app/app-edge/layout.tsx +++ b/test/e2e/app-dir/app-edge/app/app-edge/layout.tsx @@ -1,5 +1,7 @@ 'client' +// TODO-APP: support typing for useSelectedLayoutSegment +// @ts-ignore import { useSelectedLayoutSegment } from 'next/dist/client/components/hooks-client' export default function Layout({ children }: { children: React.ReactNode }) { From 0768f7d1d0f9dbe1e77cae52502adf6987307f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Tue, 4 Oct 2022 23:16:11 +0900 Subject: [PATCH 004/135] chore: Update swc_core to `v0.28.20` (#41153) This PR updates swc crates to https://github.com/swc-project/swc/commit/6749e6948ec555352f77abebe7f5a1fbb29fa527 --- packages/next-swc/Cargo.lock | 160 +++++++++--------- packages/next-swc/Cargo.toml | 5 - packages/next-swc/crates/core/Cargo.toml | 8 +- packages/next-swc/crates/emotion/Cargo.toml | 6 +- .../crates/modularize_imports/Cargo.toml | 6 +- packages/next-swc/crates/napi/Cargo.toml | 4 +- .../crates/styled_components/Cargo.toml | 10 +- .../next-swc/crates/styled_jsx/Cargo.toml | 10 +- packages/next-swc/crates/wasm/Cargo.toml | 4 +- 9 files changed, 104 insertions(+), 109 deletions(-) diff --git a/packages/next-swc/Cargo.lock b/packages/next-swc/Cargo.lock index d55a023f96b64..25367aa1c3bed 100644 --- a/packages/next-swc/Cargo.lock +++ b/packages/next-swc/Cargo.lock @@ -165,9 +165,9 @@ dependencies = [ [[package]] name = "binding_macros" -version = "0.18.8" +version = "0.18.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b43c622315d1fc6281b5c54ea898353fcda5b9375ca34a7bbb9f3113f9f52c" +checksum = "e790a17374fcf19be8ddab829185e5e09df76349c31be76edd6913f5ae83f108" dependencies = [ "anyhow", "console_error_panic_hook", @@ -2909,9 +2909,9 @@ dependencies = [ [[package]] name = "swc" -version = "0.230.9" +version = "0.230.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d237602ebc7c79604924d74d298442b977c7ffd377d53eca5f558714797e3dd9" +checksum = "bf28ae3552b18fec0fcfe282c1420838dd6c8fef6ae938e0938ec74c68ad8532" dependencies = [ "ahash", "anyhow", @@ -2973,9 +2973,9 @@ dependencies = [ [[package]] name = "swc_bundler" -version = "0.190.21" +version = "0.190.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b354ea086aab3697fe6ca2db1d49045f60d01ea58258b7c5e54d3604384298" +checksum = "7aff0b152769e0c80f4360de57a045c42477901de00367346a96199e3ff02833" dependencies = [ "ahash", "anyhow", @@ -3022,9 +3022,9 @@ dependencies = [ [[package]] name = "swc_common" -version = "0.29.3" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93b0deba513e2bc34c559aaad154771fdc6616a3a5eb1ef8daa4ce2c17fc723d" +checksum = "0b79bef6a4a4a7f273c51f64a61ad601c3b9b101f41f9d43a202832620c2f8cd" dependencies = [ "ahash", "anyhow", @@ -3081,9 +3081,9 @@ dependencies = [ [[package]] name = "swc_core" -version = "0.28.10" +version = "0.28.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577be3d4d7b415595b46e0d4d4871dfaf91c833b00df59c4e8d140a9d9c88c6c" +checksum = "45dcdeb9f834737660f5a0a9623dd1cda5cff95b540c375630d51ba4bdd9b7e5" dependencies = [ "binding_macros", "swc", @@ -3119,9 +3119,9 @@ dependencies = [ [[package]] name = "swc_css_ast" -version = "0.114.4" +version = "0.114.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c635529f06a9df7e20a1f0a3749c5e9e9e15d080c48e285a0c41f3bfaa808d" +checksum = "9f92e143e3a47ec207d3f095c5584848c6ec27bd673b064a3245fe4e13b9e6b5" dependencies = [ "is-macro", "serde", @@ -3132,9 +3132,9 @@ dependencies = [ [[package]] name = "swc_css_codegen" -version = "0.124.4" +version = "0.124.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbd48900f19c7f6b563a4b56521658e1d58bdf473445a6b57e416272ea92ba" +checksum = "f34cc082937bbbf142a24ff96bd8b814449fa53da2e8a0232d098800f5ff1a27" dependencies = [ "auto_impl", "bitflags", @@ -3162,9 +3162,9 @@ dependencies = [ [[package]] name = "swc_css_parser" -version = "0.123.4" +version = "0.123.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84c27dde40c8f234f5b083a2490fc556829af01b0814df5e365ea012b3bf16b" +checksum = "843d902c27b8a15e27238c534eaf1b12f89a39983de1c5bdd37e12cf141b6b01" dependencies = [ "bitflags", "lexical", @@ -3176,9 +3176,9 @@ dependencies = [ [[package]] name = "swc_css_prefixer" -version = "0.125.4" +version = "0.125.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef21aea33535bb446b1340fbd8db187d120ba6b6aaabe26ff4782c8778c20e5" +checksum = "332e8fdb025ea5525cea0d0c4af932ba76699621aa70c485726f986ef7c03e01" dependencies = [ "once_cell", "preset_env_base", @@ -3193,9 +3193,9 @@ dependencies = [ [[package]] name = "swc_css_utils" -version = "0.111.4" +version = "0.111.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff8d15dcc565f348b61f894ba26cf69950ed40de9e5b62d5422b601436dd477" +checksum = "dad6bed27e22664b32d4beea8eb9074fb5584ed2e7485ab19ad91c1e553abc34" dependencies = [ "once_cell", "serde", @@ -3208,9 +3208,9 @@ dependencies = [ [[package]] name = "swc_css_visit" -version = "0.113.4" +version = "0.113.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c351ae7e8ea3cecb5d9804e2968dfad6261823e31c709e0e92b86087268a9ea6" +checksum = "8324cbebf5dbeb41b43e45dec184e08c8345538eab4802fff6cd90ee143824c5" dependencies = [ "serde", "swc_atoms", @@ -3221,9 +3221,9 @@ dependencies = [ [[package]] name = "swc_ecma_ast" -version = "0.94.3" +version = "0.94.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4dd1143f6a7c35c970486134804b869aad76cc5730e965f56589d083d712a1" +checksum = "6f35e76f46d51a118ac4a5e08eb4df855a33f8ec764dbdffe7cbe5066e08d82a" dependencies = [ "bitflags", "is-macro", @@ -3239,9 +3239,9 @@ dependencies = [ [[package]] name = "swc_ecma_codegen" -version = "0.127.5" +version = "0.127.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ca34fdd6b459fcf9643dddbac1ab928ff28b90b1b80327b719b1cac389b6a3" +checksum = "5dc8e054b412f4def211ecfe1f365b3f77e3284706f877b6c7870f50831d0168" dependencies = [ "memchr", "num-bigint", @@ -3271,9 +3271,9 @@ dependencies = [ [[package]] name = "swc_ecma_ext_transforms" -version = "0.91.5" +version = "0.91.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a95d60587bd7d15c871f3c145bf9f67596581b280a88511e8ecf3c819afc1ba" +checksum = "c028ca81905c093608f5ad84b9e78376ad2b0c1886edff4a99915711932bf155" dependencies = [ "phf", "swc_atoms", @@ -3285,9 +3285,9 @@ dependencies = [ [[package]] name = "swc_ecma_lints" -version = "0.66.8" +version = "0.66.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe0eff2bee61cf122398a044c435299d9b869f6a966e8fadb42ec8f9a60ef84" +checksum = "9404193770f98c09d0a785117ae7aa2bd33f79ce4aa87c53625f4d4cbabdf717" dependencies = [ "ahash", "auto_impl", @@ -3306,9 +3306,9 @@ dependencies = [ [[package]] name = "swc_ecma_loader" -version = "0.41.3" +version = "0.41.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b43c7796921809e5ea3b32a8bb9af318da8ccfab8407084e820f671dc95a2816" +checksum = "cd7e4ef6069a6a03fcb31a139c429211f843225c2c4c256d1f972e86e02f53b4" dependencies = [ "ahash", "anyhow", @@ -3328,9 +3328,9 @@ dependencies = [ [[package]] name = "swc_ecma_minifier" -version = "0.157.22" +version = "0.157.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01f347e045ad41c519771b677680e181250ac72ac5acaa2686ca724881ef4a03" +checksum = "a3fd03d7d2956d7e4b0e63d51e150f5cb12863ac74fd3ceb6d2b685fc8a6910b" dependencies = [ "ahash", "arrayvec", @@ -3362,9 +3362,9 @@ dependencies = [ [[package]] name = "swc_ecma_parser" -version = "0.122.4" +version = "0.122.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ca8ce779a80d153babd4c32ac428ed426e7ad4b930fc13086bc8b0a96ecb6d" +checksum = "7d040a13b3c39514255d161345b2730c9fbf52f02ad579a0a68a05af039247b5" dependencies = [ "either", "enum_kind", @@ -3381,9 +3381,9 @@ dependencies = [ [[package]] name = "swc_ecma_preset_env" -version = "0.172.10" +version = "0.172.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faac44052f43da2ddcbc4f5422b39f0694ce28b93397b9e73f1e8d7715c0e1cd" +checksum = "9b1f2f0d614a4595acfac5bbf630f8a2a07ebb6550faa5a82d81baa19fb5935a" dependencies = [ "ahash", "anyhow", @@ -3406,9 +3406,9 @@ dependencies = [ [[package]] name = "swc_ecma_testing" -version = "0.20.4" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fe0a20c95969434d7fa1fb164278ed3018048c6ca8aa0aa20b1ce25c1d984a9" +checksum = "81a09d0c939e8e691b27f91f430067ad0acca688a3e805b459956fd338966e87" dependencies = [ "anyhow", "hex", @@ -3422,9 +3422,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms" -version = "0.196.10" +version = "0.196.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ac73b1d3a00194549cc531af702804add7c634ba078c1a3e0044f2f26742927" +checksum = "2e33e7afed4a7cfb5fe4cfcb906dd96d956a0e5628ddf5b8048467f2269ae0d2" dependencies = [ "swc_atoms", "swc_common", @@ -3442,9 +3442,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_base" -version = "0.111.8" +version = "0.111.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6125f8fdbb332630b67862ae027afc80a1269c481fc747e83906d6e3e296ded9" +checksum = "bea8e599cb306e131a0136476113d96178c121be5b716bb9aa0e319b92bca408" dependencies = [ "better_scoped_tls", "bitflags", @@ -3465,9 +3465,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_classes" -version = "0.100.8" +version = "0.100.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e59ead65f01c5034bd1a1d5180404b726e4637f4bb82beaa4c6c4e8e6e652dad" +checksum = "2a19903a8e999083e5199300ae89650e3e0ba9dce988c09db4876b14199e30ce" dependencies = [ "swc_atoms", "swc_common", @@ -3479,9 +3479,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_compat" -version = "0.134.9" +version = "0.134.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139fd76efca14cd7dfcba18ffc5474c395717fc88ddcb091dd10f23f2283cbb3" +checksum = "a6e4a18225a0cd59461326dd5064ec4efe333197bbe41c04f814f567919eb6f1" dependencies = [ "ahash", "arrayvec", @@ -3520,9 +3520,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_module" -version = "0.151.9" +version = "0.151.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f9817fec12dc9fc24593cd587ff41e14a42d100e611770509e2dffdf4adb16a" +checksum = "7b988b64702f69dc2fd28cb3031256f8c3df35664533a8ac701a90b505ef3233" dependencies = [ "Inflector", "ahash", @@ -3548,9 +3548,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_optimization" -version = "0.165.10" +version = "0.165.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cead236747cc214dd05f1d486a661746eb5cb60c180979f6e6b77bbf01c07c61" +checksum = "cd230f05e997b523634e2069521cf02b5d5fafebeabf5f44eefec655afe58fba" dependencies = [ "ahash", "dashmap", @@ -3574,9 +3574,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_proposal" -version = "0.142.10" +version = "0.142.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7542b92b2bedb76bea3d25b4ab70af362e53503189ec012b96141af297f640d5" +checksum = "105bb1345c3628ec6a702efadc8065ad5a1174b113669fe4f9024dd2d19cd3e9" dependencies = [ "either", "serde", @@ -3593,9 +3593,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_react" -version = "0.153.9" +version = "0.153.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c5c632040c307858f8795c3315d375199ec63c911dd9c92105a5541077924e6" +checksum = "7656773da248ccae8b162bb452b820e2591e211a26b80fda1d36c4d94de8f08a" dependencies = [ "ahash", "base64", @@ -3620,9 +3620,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_testing" -version = "0.113.8" +version = "0.113.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e38a099fe2cf9c858d299abc86848be94c16663a086e79ee5546992dae52befe" +checksum = "6bb46ef1f434c80d66ba971e3f6908fa625a11d0658ccff378256a2814f416e2" dependencies = [ "ansi_term", "anyhow", @@ -3644,9 +3644,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_typescript" -version = "0.157.10" +version = "0.157.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d5938500d1c94b77df0a67a362ab053514ef1d323534ce7fb0e37385760987" +checksum = "a2624e1e4164e8a89a02011c01feb62f5be350ebec1e4931f0ee9d1ba3e2e570" dependencies = [ "serde", "swc_atoms", @@ -3660,9 +3660,9 @@ dependencies = [ [[package]] name = "swc_ecma_utils" -version = "0.105.5" +version = "0.105.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "675971eee1e844f4a5166f5c6abf112b14a9ecfc096a3bac91735455ee9e883d" +checksum = "9c1064b08af03fe7ea2887cf80d4b3200544158520e324abe48d3b57eab31317" dependencies = [ "indexmap", "num_cpus", @@ -3678,9 +3678,9 @@ dependencies = [ [[package]] name = "swc_ecma_visit" -version = "0.80.3" +version = "0.80.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7ad7b28194e5d7465025d8151d95624e649b88f2c45a7a30ca57208abaf09a5" +checksum = "fa2e5e96387513533be7c2094aefd1cb7ab05b17477def007fd709ea00d730e2" dependencies = [ "num-bigint", "swc_atoms", @@ -3722,9 +3722,9 @@ dependencies = [ [[package]] name = "swc_error_reporters" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a635fe957192b9f6f5eec03837a3358d31a2c5dda427a1d4439f96d1a5f7b1" +checksum = "bb7e3246738064b3b5c151638eeb8b71701711e5aec2002fbe083793dacdf412" dependencies = [ "anyhow", "miette", @@ -3735,9 +3735,9 @@ dependencies = [ [[package]] name = "swc_fast_graph" -version = "0.17.3" +version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b5bc99a1655c49a7bc146815d928964c72e1fbfb4eee0c5cb809cd1b10fedc" +checksum = "a1c896c7b96b1af515498979546dd8a00bf892fde1f727d32ad6ae9461e38bc2" dependencies = [ "ahash", "indexmap", @@ -3747,9 +3747,9 @@ dependencies = [ [[package]] name = "swc_graph_analyzer" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e50f8ac3580c972d936c542dc9c36c3d370a9f6f3c75a6adea7dbb1c6a07ad8a" +checksum = "d7e691625cb09e967935f499a13eb1fb6db0b03c9714de1fa83d872ee81c261c" dependencies = [ "ahash", "auto_impl", @@ -3782,9 +3782,9 @@ dependencies = [ [[package]] name = "swc_node_comments" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "542ae7250fb1692270a82183563d38eb60892170dc696b4ea543d104c7f8de17" +checksum = "8d17400e0f0175824465905e006cd02d7c5df765fe8d0508e5d1b91da19e6b61" dependencies = [ "ahash", "dashmap", @@ -3794,9 +3794,9 @@ dependencies = [ [[package]] name = "swc_plugin_proxy" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b470f26b76312cc9e357b1988f781ce5d0d8fc28867cd054bd29634c571c0f9" +checksum = "b2270658050fd6b570339138103b0a22a62336d62e3af7b44dc8cd60b29f681c" dependencies = [ "better_scoped_tls", "rkyv", @@ -3808,9 +3808,9 @@ dependencies = [ [[package]] name = "swc_plugin_runner" -version = "0.77.4" +version = "0.77.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f5b00fae79df7f49d742041740b7442974d5062217a9912e55a0b31d959b0ca" +checksum = "8d5ef3d009b91af32a3d5195ddda48c32cbe7fb1169f93cba7d43cbb53a24ce0" dependencies = [ "anyhow", "once_cell", @@ -3828,9 +3828,9 @@ dependencies = [ [[package]] name = "swc_timer" -version = "0.17.3" +version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d03b7f5a57ef48eab10e6d5d4845ffbf269853d0f73ba04b606b2283061a9ce" +checksum = "f90d1d5acb5f25421d1c528c58b413553080329307b972a0fc004385ee8e8be9" dependencies = [ "tracing", ] @@ -3922,9 +3922,9 @@ dependencies = [ [[package]] name = "testing" -version = "0.31.3" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1734cf1fc4359445c47d4b07718fede5af306452cc0a8d5404e108eafdfdbbc9" +checksum = "542cacf565846829c5ab5bcf64f601a1f412b1de5fcc9d4b1f5297926c16fddf" dependencies = [ "ansi_term", "difference", diff --git a/packages/next-swc/Cargo.toml b/packages/next-swc/Cargo.toml index bb1e586534961..2b31c45c0c155 100644 --- a/packages/next-swc/Cargo.toml +++ b/packages/next-swc/Cargo.toml @@ -15,8 +15,3 @@ debug-assertions = false [profile.release] lto = true - -# Declare dependencies used across workspace packages requires single version bump. -# ref: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace -[workspace.dependencies] -swc_core = "0.28.10" \ No newline at end of file diff --git a/packages/next-swc/crates/core/Cargo.toml b/packages/next-swc/crates/core/Cargo.toml index d845689ab1181..f89b7d9916707 100644 --- a/packages/next-swc/crates/core/Cargo.toml +++ b/packages/next-swc/crates/core/Cargo.toml @@ -28,7 +28,7 @@ styled_jsx = {path="../styled_jsx"} modularize_imports = {path="../modularize_imports"} tracing = { version = "0.1.32", features = ["release_max_level_info"] } -swc_core = { workspace = true, features = [ +swc_core = { features = [ "common_concurrent", "ecma_ast", "ecma_visit", @@ -45,9 +45,9 @@ swc_core = { workspace = true, features = [ "ecma_parser_typescript", "cached", "base" -] } +], version = "0.28.20" } [dev-dependencies] -swc_core = { workspace = true, features = ["testing_transform"] } -testing = "0.31.3" +swc_core = { features = ["testing_transform"], version = "0.28.20" } +testing = "0.31.4" walkdir = "2.3.2" diff --git a/packages/next-swc/crates/emotion/Cargo.toml b/packages/next-swc/crates/emotion/Cargo.toml index e38e294046ff8..b75ab0a8d2d60 100644 --- a/packages/next-swc/crates/emotion/Cargo.toml +++ b/packages/next-swc/crates/emotion/Cargo.toml @@ -19,9 +19,9 @@ regex = "1.5" serde = "1" sourcemap = "6.0.1" tracing = { version = "0.1.32", features = ["release_max_level_info"] } -swc_core = { workspace = true, features = ["common", "ecma_ast","ecma_codegen", "ecma_utils", "ecma_visit", "trace_macro"] } +swc_core = { features = ["common", "ecma_ast","ecma_codegen", "ecma_utils", "ecma_visit", "trace_macro"], version = "0.28.20" } [dev-dependencies] -swc_core = { workspace = true, features = ["testing_transform", "ecma_transforms_react"] } -testing = "0.31.3" +swc_core = { features = ["testing_transform", "ecma_transforms_react"], version = "0.28.20" } +testing = "0.31.4" serde_json = "1" diff --git a/packages/next-swc/crates/modularize_imports/Cargo.toml b/packages/next-swc/crates/modularize_imports/Cargo.toml index 6e8ccb0eee7d4..3f560bba9ca16 100644 --- a/packages/next-swc/crates/modularize_imports/Cargo.toml +++ b/packages/next-swc/crates/modularize_imports/Cargo.toml @@ -15,8 +15,8 @@ handlebars = "4.2.1" once_cell = "1.13.0" regex = "1.5" serde = "1" -swc_core = { workspace = true, features = ["cached", "ecma_ast", "ecma_visit"] } +swc_core = { features = ["cached", "ecma_ast", "ecma_visit"], version = "0.28.20" } [dev-dependencies] -swc_core = { workspace = true, features = ["testing_transform"] } -testing = "0.31.3" +swc_core = { features = ["testing_transform"], version = "0.28.20" } +testing = "0.31.4" diff --git a/packages/next-swc/crates/napi/Cargo.toml b/packages/next-swc/crates/napi/Cargo.toml index 0b1720963d00e..fdb4d65b52a26 100644 --- a/packages/next-swc/crates/napi/Cargo.toml +++ b/packages/next-swc/crates/napi/Cargo.toml @@ -30,7 +30,7 @@ next-swc = {version = "0.0.0", path = "../core"} once_cell = "1.13.0" serde = "1" serde_json = "1" -swc_core = { workspace = true, features = [ +swc_core = { features = [ "allocator_node", "base_concurrent", # concurrent? "common_concurrent", @@ -49,7 +49,7 @@ swc_core = { workspace = true, features = [ "ecma_transforms_typescript", "ecma_utils", "ecma_visit" -] } +], version = "0.28.20" } tracing = { version = "0.1.32", features = ["release_max_level_info"] } tracing-futures = "0.2.5" tracing-subscriber = "0.3.9" diff --git a/packages/next-swc/crates/styled_components/Cargo.toml b/packages/next-swc/crates/styled_components/Cargo.toml index ecf85e5dfa11d..4bced8f5bdf60 100644 --- a/packages/next-swc/crates/styled_components/Cargo.toml +++ b/packages/next-swc/crates/styled_components/Cargo.toml @@ -16,18 +16,18 @@ once_cell = "1.13.0" regex = {version = "1.5.4", features = ["std", "perf"], default-features = false} serde = {version = "1.0.130", features = ["derive"]} tracing = "0.1.32" -swc_core = { workspace = true, features = [ +swc_core = { features = [ "common", "ecma_ast", "ecma_utils", "ecma_visit" -] } +], version = "0.28.20" } [dev-dependencies] serde_json = "1" -testing = "0.31.3" -swc_core = { workspace = true, features = [ +testing = "0.31.4" +swc_core = { features = [ "ecma_parser", "ecma_transforms", "testing_transform" -] } +], version = "0.28.20" } diff --git a/packages/next-swc/crates/styled_jsx/Cargo.toml b/packages/next-swc/crates/styled_jsx/Cargo.toml index 4f2c9fbdf334d..94a5dfa69eeb5 100644 --- a/packages/next-swc/crates/styled_jsx/Cargo.toml +++ b/packages/next-swc/crates/styled_jsx/Cargo.toml @@ -13,7 +13,7 @@ version = "0.20.0" easy-error = "1.0.0" tracing = "0.1.32" -swc_core = { workspace = true, features = [ +swc_core = { features = [ "common", "css_ast", "css_codegen", @@ -24,10 +24,10 @@ swc_core = { workspace = true, features = [ "ecma_minifier", "ecma_utils", "ecma_visit" -] } +], version = "0.28.20" } [dev-dependencies] -testing = "0.31.3" -swc_core = { workspace = true, features = [ +testing = "0.31.4" +swc_core = { features = [ "testing_transform" -] } +], version = "0.28.20" } diff --git a/packages/next-swc/crates/wasm/Cargo.toml b/packages/next-swc/crates/wasm/Cargo.toml index 3ac12afbc8711..a5c1009406a8a 100644 --- a/packages/next-swc/crates/wasm/Cargo.toml +++ b/packages/next-swc/crates/wasm/Cargo.toml @@ -32,7 +32,7 @@ getrandom = { version = "0.2.5", optional = true, default-features = false } js-sys = "0.3.59" serde-wasm-bindgen = "0.4.3" -swc_core = { workspace = true, features = [ +swc_core = { features = [ "common_concurrent", "binding_macro_wasm", "ecma_codegen", @@ -45,7 +45,7 @@ swc_core = { workspace = true, features = [ "ecma_parser_typescript", "ecma_utils", "ecma_visit" -] } +], version = "0.28.20" } # Workaround a bug From d192047a34870066273c2ca319fdc6e15f16c165 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 4 Oct 2022 19:03:42 +0200 Subject: [PATCH 005/135] Remove unnecessary `moduleId` option (#41160) `deterministic` should be already the default option here. --- packages/next/build/webpack-config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 08b1df110e143..99ca9bbb1ab88 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1285,7 +1285,6 @@ export default async function getBaseWebpackConfig( emitOnErrors: !dev, checkWasmTypes: false, nodeEnv: false, - ...(hasServerComponents ? { moduleIds: 'deterministic' } : {}), splitChunks: ((): | Required['optimization']['splitChunks'] | false => { From 8d4840b15a994265c229592a2f4039841b03d33c Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 4 Oct 2022 10:08:17 -0700 Subject: [PATCH 006/135] Apply experimental configs for middleware (#41142) This applies the experimental configs for testing and also fixes `set-cookie` headers from middleware/edge functions being merged unexpectedly. x-ref: [slack thread](https://vercel.slack.com/archives/CGU8HUTUH/p1664313529422279) Fixes: https://github.com/vercel/next.js/issues/40820 ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` --- packages/next/build/webpack-config.ts | 3 + packages/next/lib/load-custom-routes.ts | 80 ++++---- packages/next/server/base-server.ts | 27 +++ packages/next/server/config-schema.ts | 6 + packages/next/server/config-shared.ts | 2 + packages/next/server/next-server.ts | 20 +- packages/next/server/request-meta.ts | 1 + packages/next/server/web/adapter.ts | 16 +- .../app/middleware.js | 27 +++ .../app/pages/another.js | 13 ++ .../app/pages/api/test-cookie-edge.js | 12 ++ .../app/pages/api/test-cookie.js | 5 + .../app/pages/blog/[slug].js | 13 ++ .../app/pages/index.js | 17 ++ .../index.test.ts | 193 ++++++++++++++++++ 15 files changed, 386 insertions(+), 49 deletions(-) create mode 100644 test/e2e/skip-trailing-slash-redirect/app/middleware.js create mode 100644 test/e2e/skip-trailing-slash-redirect/app/pages/another.js create mode 100644 test/e2e/skip-trailing-slash-redirect/app/pages/api/test-cookie-edge.js create mode 100644 test/e2e/skip-trailing-slash-redirect/app/pages/api/test-cookie.js create mode 100644 test/e2e/skip-trailing-slash-redirect/app/pages/blog/[slug].js create mode 100644 test/e2e/skip-trailing-slash-redirect/app/pages/index.js create mode 100644 test/e2e/skip-trailing-slash-redirect/index.test.ts diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 99ca9bbb1ab88..7cff337e20cc7 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -253,6 +253,9 @@ export function getDefineEnv({ 'process.env.__NEXT_I18N_SUPPORT': JSON.stringify(!!config.i18n), 'process.env.__NEXT_I18N_DOMAINS': JSON.stringify(config.i18n?.domains), 'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(config.analyticsId), + 'process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE': JSON.stringify( + config.experimental.skipMiddlewareUrlNormalize + ), 'process.env.__NEXT_HAS_WEB_VITALS_ATTRIBUTION': JSON.stringify( config.experimental.webVitalsAttribution && config.experimental.webVitalsAttribution.length > 0 diff --git a/packages/next/lib/load-custom-routes.ts b/packages/next/lib/load-custom-routes.ts index 0658b6fd1ee11..a6316ce25c36b 100644 --- a/packages/next/lib/load-custom-routes.ts +++ b/packages/next/lib/load-custom-routes.ts @@ -624,50 +624,52 @@ export default async function loadCustomRoutes( ) } - if (config.trailingSlash) { - redirects.unshift( - { - source: '/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/', - destination: '/:file', - permanent: true, - locale: config.i18n ? false : undefined, - internal: true, - } as Redirect, - { - source: '/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)', - destination: '/:notfile/', - permanent: true, - locale: config.i18n ? false : undefined, - internal: true, - } as Redirect - ) - if (config.basePath) { - redirects.unshift({ - source: config.basePath, - destination: config.basePath + '/', - permanent: true, - basePath: false, - locale: config.i18n ? false : undefined, - internal: true, - } as Redirect) - } - } else { - redirects.unshift({ - source: '/:path+/', - destination: '/:path+', - permanent: true, - locale: config.i18n ? false : undefined, - internal: true, - } as Redirect) - if (config.basePath) { + if (!config.experimental?.skipTrailingSlashRedirect) { + if (config.trailingSlash) { + redirects.unshift( + { + source: '/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/', + destination: '/:file', + permanent: true, + locale: config.i18n ? false : undefined, + internal: true, + } as Redirect, + { + source: '/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)', + destination: '/:notfile/', + permanent: true, + locale: config.i18n ? false : undefined, + internal: true, + } as Redirect + ) + if (config.basePath) { + redirects.unshift({ + source: config.basePath, + destination: config.basePath + '/', + permanent: true, + basePath: false, + locale: config.i18n ? false : undefined, + internal: true, + } as Redirect) + } + } else { redirects.unshift({ - source: config.basePath + '/', - destination: config.basePath, + source: '/:path+/', + destination: '/:path+', permanent: true, - basePath: false, locale: config.i18n ? false : undefined, internal: true, } as Redirect) + if (config.basePath) { + redirects.unshift({ + source: config.basePath + '/', + destination: config.basePath, + permanent: true, + basePath: false, + locale: config.i18n ? false : undefined, + internal: true, + } as Redirect) + } } } diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index f5ff7587e3beb..d26be948965d1 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -440,6 +440,33 @@ export default abstract class Server { parsedUrl?: NextUrlWithParsedQuery ): Promise { try { + // ensure cookies set in middleware are merged and + // not overridden by API routes/getServerSideProps + const _res = (res as any).originalResponse || res + const origSetHeader = _res.setHeader.bind(_res) + + _res.setHeader = (name: string, val: string | string[]) => { + if (name.toLowerCase() === 'set-cookie') { + const middlewareValue = getRequestMeta(req, '_nextMiddlewareCookie') + + if ( + !middlewareValue || + !Array.isArray(val) || + !val.every((item, idx) => item === middlewareValue[idx]) + ) { + val = [ + ...(middlewareValue || []), + ...(typeof val === 'string' + ? [val] + : Array.isArray(val) + ? val + : []), + ] + } + } + return origSetHeader(name, val) + } + const urlParts = (req.url || '').split('?') const urlNoQuery = urlParts[0] diff --git a/packages/next/server/config-schema.ts b/packages/next/server/config-schema.ts index 2e1d2aff57122..b0fef4eeb818f 100644 --- a/packages/next/server/config-schema.ts +++ b/packages/next/server/config-schema.ts @@ -354,6 +354,12 @@ const configSchema = { sharedPool: { type: 'boolean', }, + skipMiddlewareUrlNormalize: { + type: 'boolean', + }, + skipTrailingSlashRedirect: { + type: 'boolean', + }, sri: { properties: { algorithm: { diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index 61de1ad0c1524..69b1dc76914cd 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -79,6 +79,8 @@ export interface NextJsWebpackConfig { } export interface ExperimentalConfig { + skipMiddlewareUrlNormalize?: boolean + skipTrailingSlashRedirect?: boolean optimisticClientCache?: boolean legacyBrowsers?: boolean browsersListForSwc?: boolean diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 06ccb3d4cd553..68aab94a4ac7b 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -84,7 +84,7 @@ import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import { loadComponents } from './load-components' import isError, { getProperError } from '../lib/is-error' import { FontManifest } from './font-utils' -import { toNodeHeaders } from './web/utils' +import { splitCookiesString, toNodeHeaders } from './web/utils' import { relativizeURL } from '../shared/lib/router/utils/relativize-url' import { prepareDestination } from '../shared/lib/router/utils/prepare-destination' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' @@ -1796,9 +1796,16 @@ export default class NextNodeServer extends BaseServer { } else { for (let [key, value] of allHeaders) { result.response.headers.set(key, value) + + if (key.toLowerCase() === 'set-cookie') { + addRequestMeta( + params.request, + '_nextMiddlewareCookie', + splitCookiesString(value) + ) + } } } - return result } @@ -2097,8 +2104,13 @@ export default class NextNodeServer extends BaseServer { params.res.statusCode = result.response.status params.res.statusMessage = result.response.statusText - result.response.headers.forEach((value, key) => { - params.res.appendHeader(key, value) + result.response.headers.forEach((value: string, key) => { + // the append handling is special cased for `set-cookie` + if (key.toLowerCase() === 'set-cookie') { + params.res.setHeader(key, value) + } else { + params.res.appendHeader(key, value) + } }) if (result.response.body) { diff --git a/packages/next/server/request-meta.ts b/packages/next/server/request-meta.ts index 39985a4918483..ac3dcad68d525 100644 --- a/packages/next/server/request-meta.ts +++ b/packages/next/server/request-meta.ts @@ -21,6 +21,7 @@ export interface RequestMeta { _nextDidRewrite?: boolean _nextHadBasePath?: boolean _nextRewroteUrl?: string + _nextMiddlewareCookie?: string[] _protocol?: string } diff --git a/packages/next/server/web/adapter.ts b/packages/next/server/web/adapter.ts index 5e6721f9d8be2..53dcce2e4715e 100644 --- a/packages/next/server/web/adapter.ts +++ b/packages/next/server/web/adapter.ts @@ -117,9 +117,11 @@ export async function adapter(params: { nextConfig: params.request.nextConfig, }) - if (rewriteUrl.host === request.nextUrl.host) { - rewriteUrl.buildId = buildId || rewriteUrl.buildId - response.headers.set('x-middleware-rewrite', String(rewriteUrl)) + if (!process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE) { + if (rewriteUrl.host === request.nextUrl.host) { + rewriteUrl.buildId = buildId || rewriteUrl.buildId + response.headers.set('x-middleware-rewrite', String(rewriteUrl)) + } } /** @@ -154,9 +156,11 @@ export async function adapter(params: { */ response = new Response(response.body, response) - if (redirectURL.host === request.nextUrl.host) { - redirectURL.buildId = buildId || redirectURL.buildId - response.headers.set('Location', String(redirectURL)) + if (!process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE) { + if (redirectURL.host === request.nextUrl.host) { + redirectURL.buildId = buildId || redirectURL.buildId + response.headers.set('Location', String(redirectURL)) + } } /** diff --git a/test/e2e/skip-trailing-slash-redirect/app/middleware.js b/test/e2e/skip-trailing-slash-redirect/app/middleware.js new file mode 100644 index 0000000000000..72690b6c4650b --- /dev/null +++ b/test/e2e/skip-trailing-slash-redirect/app/middleware.js @@ -0,0 +1,27 @@ +import { NextResponse } from 'next/server' + +export default function handler(req) { + if (req.nextUrl.pathname === '/middleware-rewrite-with-slash') { + return NextResponse.rewrite(new URL('/another/', req.nextUrl)) + } + + if (req.nextUrl.pathname === '/middleware-rewrite-without-slash') { + return NextResponse.rewrite(new URL('/another', req.nextUrl)) + } + + if (req.nextUrl.pathname === '/middleware-redirect-external-with') { + return NextResponse.redirect('https://example.vercel.sh/somewhere/', 307) + } + + if (req.nextUrl.pathname === '/middleware-redirect-external-without') { + return NextResponse.redirect('https://example.vercel.sh/somewhere', 307) + } + + if (req.nextUrl.pathname.startsWith('/api/test-cookie')) { + const res = NextResponse.next() + res.cookies.set('from-middleware', 1) + return res + } + + return NextResponse.next() +} diff --git a/test/e2e/skip-trailing-slash-redirect/app/pages/another.js b/test/e2e/skip-trailing-slash-redirect/app/pages/another.js new file mode 100644 index 0000000000000..8fa884af1e5e8 --- /dev/null +++ b/test/e2e/skip-trailing-slash-redirect/app/pages/another.js @@ -0,0 +1,13 @@ +import Link from 'next/link' + +export default function Page(props) { + return ( + <> +

another page

+ + to index + +
+ + ) +} diff --git a/test/e2e/skip-trailing-slash-redirect/app/pages/api/test-cookie-edge.js b/test/e2e/skip-trailing-slash-redirect/app/pages/api/test-cookie-edge.js new file mode 100644 index 0000000000000..6018a223708fd --- /dev/null +++ b/test/e2e/skip-trailing-slash-redirect/app/pages/api/test-cookie-edge.js @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server' + +export const config = { + runtime: 'experimental-edge', +} + +export default function handler(req) { + console.log('setting cookie in api route') + const res = NextResponse.json({ name: 'API' }) + res.cookies.set('hello', 'From API') + return res +} diff --git a/test/e2e/skip-trailing-slash-redirect/app/pages/api/test-cookie.js b/test/e2e/skip-trailing-slash-redirect/app/pages/api/test-cookie.js new file mode 100644 index 0000000000000..4aec0e3eec7b7 --- /dev/null +++ b/test/e2e/skip-trailing-slash-redirect/app/pages/api/test-cookie.js @@ -0,0 +1,5 @@ +export default function handler(req, res) { + console.log('setting cookie in api route') + res.setHeader('Set-Cookie', 'hello=From API') + res.status(200).json({ name: 'API' }) +} diff --git a/test/e2e/skip-trailing-slash-redirect/app/pages/blog/[slug].js b/test/e2e/skip-trailing-slash-redirect/app/pages/blog/[slug].js new file mode 100644 index 0000000000000..4e988b3acf0bd --- /dev/null +++ b/test/e2e/skip-trailing-slash-redirect/app/pages/blog/[slug].js @@ -0,0 +1,13 @@ +import Link from 'next/link' + +export default function Page(props) { + return ( + <> +

blog page

+ + to index + +
+ + ) +} diff --git a/test/e2e/skip-trailing-slash-redirect/app/pages/index.js b/test/e2e/skip-trailing-slash-redirect/app/pages/index.js new file mode 100644 index 0000000000000..ff064e7ab56b9 --- /dev/null +++ b/test/e2e/skip-trailing-slash-redirect/app/pages/index.js @@ -0,0 +1,17 @@ +import Link from 'next/link' + +export default function Page(props) { + return ( + <> +

index page

+ + to another + +
+ + to /blog/first + +
+ + ) +} diff --git a/test/e2e/skip-trailing-slash-redirect/index.test.ts b/test/e2e/skip-trailing-slash-redirect/index.test.ts new file mode 100644 index 0000000000000..d1c71bba4f8b1 --- /dev/null +++ b/test/e2e/skip-trailing-slash-redirect/index.test.ts @@ -0,0 +1,193 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { fetchViaHTTP } from 'next-test-utils' +import { join } from 'path' +import webdriver from 'next-webdriver' + +describe('skip-trailing-slash-redirect', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(join(__dirname, 'app')), + dependencies: {}, + nextConfig: { + experimental: { + skipTrailingSlashRedirect: true, + skipMiddlewareUrlNormalize: true, + }, + async redirects() { + return [ + { + source: '/redirect-me', + destination: '/another', + permanent: false, + }, + ] + }, + async rewrites() { + return [ + { + source: '/rewrite-me', + destination: '/another', + }, + ] + }, + }, + }) + }) + afterAll(() => next.destroy()) + + it('should merge cookies from middleware and API routes correctly', async () => { + const res = await fetchViaHTTP(next.url, '/api/test-cookie', undefined, { + redirect: 'manual', + }) + expect(res.status).toBe(200) + expect(res.headers.get('set-cookie')).toEqual( + 'from-middleware=1; Path=/, hello=From API' + ) + }) + + it('should merge cookies from middleware and edge API routes correctly', async () => { + const res = await fetchViaHTTP( + next.url, + '/api/test-cookie-edge', + undefined, + { + redirect: 'manual', + } + ) + expect(res.status).toBe(200) + expect(res.headers.get('set-cookie')).toEqual( + 'from-middleware=1; Path=/, hello=From%20API; Path=/' + ) + }) + + if ((global as any).isNextStart) { + it('should not have trailing slash redirects in manifest', async () => { + const routesManifest = JSON.parse( + await next.readFile('.next/routes-manifest.json') + ) + + expect( + routesManifest.redirects.some((redirect) => { + return ( + redirect.statusCode === 308 && + (redirect.destination === '/:path+' || + redirect.destination === '/:path+/') + ) + }) + ).toBe(false) + }) + } + + it('should correct skip URL normalizing in middleware', async () => { + let res = await fetchViaHTTP( + next.url, + '/middleware-rewrite-with-slash', + undefined, + { redirect: 'manual', headers: { 'x-nextjs-data': '1' } } + ) + expect(res.headers.get('x-nextjs-rewrite').endsWith('/another/')).toBe(true) + + res = await fetchViaHTTP( + next.url, + '/middleware-rewrite-without-slash', + undefined, + { redirect: 'manual', headers: { 'x-nextjs-data': '1' } } + ) + expect(res.headers.get('x-nextjs-rewrite').endsWith('/another')).toBe(true) + + res = await fetchViaHTTP( + next.url, + '/middleware-redirect-external-with', + undefined, + { redirect: 'manual' } + ) + expect(res.status).toBe(307) + expect(res.headers.get('Location')).toBe( + 'https://example.vercel.sh/somewhere/' + ) + + res = await fetchViaHTTP( + next.url, + '/middleware-redirect-external-without', + undefined, + { redirect: 'manual' } + ) + expect(res.status).toBe(307) + expect(res.headers.get('Location')).toBe( + 'https://example.vercel.sh/somewhere' + ) + }) + + it('should apply config redirect correctly', async () => { + const res = await fetchViaHTTP(next.url, '/redirect-me', undefined, { + redirect: 'manual', + }) + expect(res.status).toBe(307) + expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe( + '/another' + ) + }) + + it('should apply config rewrites correctly', async () => { + const res = await fetchViaHTTP(next.url, '/rewrite-me', undefined, { + redirect: 'manual', + }) + expect(res.status).toBe(200) + expect(await res.text()).toContain('another page') + }) + + it('should not apply trailing slash redirect (with slash)', async () => { + const res = await fetchViaHTTP(next.url, '/another/', undefined, { + redirect: 'manual', + }) + expect(res.status).toBe(200) + expect(await res.text()).toContain('another page') + }) + + it('should not apply trailing slash redirect (without slash)', async () => { + const res = await fetchViaHTTP(next.url, '/another', undefined, { + redirect: 'manual', + }) + expect(res.status).toBe(200) + expect(await res.text()).toContain('another page') + }) + + it('should respond to index correctly', async () => { + const res = await fetchViaHTTP(next.url, '/', undefined, { + redirect: 'manual', + }) + expect(res.status).toBe(200) + expect(await res.text()).toContain('index page') + }) + + it('should respond to dynamic route correctly', async () => { + const res = await fetchViaHTTP(next.url, '/blog/first', undefined, { + redirect: 'manual', + }) + expect(res.status).toBe(200) + expect(await res.text()).toContain('blog page') + }) + + it('should navigate client side correctly', async () => { + const browser = await webdriver(next.url, '/') + + expect(await browser.eval('location.pathname')).toBe('/') + + await browser.elementByCss('#to-another').click() + await browser.waitForElementByCss('#another') + + expect(await browser.eval('location.pathname')).toBe('/another') + await browser.back() + await browser.waitForElementByCss('#index') + + expect(await browser.eval('location.pathname')).toBe('/') + + await browser.elementByCss('#to-blog-first').click() + await browser.waitForElementByCss('#blog') + + expect(await browser.eval('location.pathname')).toBe('/blog/first') + }) +}) From 0d5886f20bffd615f28cdd8d96d20db29ff3157e Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 4 Oct 2022 10:15:01 -0700 Subject: [PATCH 007/135] v12.3.2-canary.19 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-next/package.json | 2 +- packages/font/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 17 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lerna.json b/lerna.json index 2cd186f709128..46cfa723e8c83 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "12.3.2-canary.18" + "version": "12.3.2-canary.19" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 884abf945e56e..93ef5ea6f191e 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 9dd328dde3a73..8400675704f7b 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "description": "ESLint configuration used by NextJS.", "main": "index.js", "license": "MIT", @@ -9,7 +9,7 @@ "directory": "packages/eslint-config-next" }, "dependencies": { - "@next/eslint-plugin-next": "12.3.2-canary.18", + "@next/eslint-plugin-next": "12.3.2-canary.19", "@rushstack/eslint-patch": "^1.1.3", "@typescript-eslint/parser": "^5.21.0", "eslint-import-resolver-node": "^0.3.6", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 02c006337575b..b02f8aca6c4cd 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "description": "ESLint plugin for NextJS.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index b569df7e2ed53..3d454280cd30c 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 2378ad9bc4f9e..6eab5530b3c7c 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 76fb748b72135..a15a6fc651029 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index cb28283e99fc9..f61809c387bdf 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 58a053810aad1..d35483549edea 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 5c9297cbf2634..7fc866627f30e 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 44dea1cb906a7..4d2dec3f49fb0 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 17d38aa9c32cd..134927b996871 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 4099e800d2b7c..45a865a14d780 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "private": true, "scripts": { "build-native": "napi build --platform -p next-swc-napi --cargo-name next_swc_napi native --features plugin", diff --git a/packages/next/package.json b/packages/next/package.json index 80446a17e9dd4..00df6063df68d 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -69,7 +69,7 @@ ] }, "dependencies": { - "@next/env": "12.3.2-canary.18", + "@next/env": "12.3.2-canary.19", "@swc/helpers": "0.4.11", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.14", @@ -120,11 +120,11 @@ "@hapi/accept": "5.0.2", "@napi-rs/cli": "2.7.0", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "12.3.2-canary.18", - "@next/polyfill-nomodule": "12.3.2-canary.18", - "@next/react-dev-overlay": "12.3.2-canary.18", - "@next/react-refresh-utils": "12.3.2-canary.18", - "@next/swc": "12.3.2-canary.18", + "@next/polyfill-module": "12.3.2-canary.19", + "@next/polyfill-nomodule": "12.3.2-canary.19", + "@next/react-dev-overlay": "12.3.2-canary.19", + "@next/react-refresh-utils": "12.3.2-canary.19", + "@next/swc": "12.3.2-canary.19", "@segment/ajv-human-errors": "2.1.2", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 072cb0476cf34..9884cb4de844a 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index ee1b4e983d732..be001c5fddb6c 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "12.3.2-canary.18", + "version": "12.3.2-canary.19", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a920d5362acba..0d95082e8b090 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -397,7 +397,7 @@ importers: packages/eslint-config-next: specifiers: - '@next/eslint-plugin-next': 12.3.2-canary.18 + '@next/eslint-plugin-next': 12.3.2-canary.19 '@rushstack/eslint-patch': ^1.1.3 '@typescript-eslint/parser': ^5.21.0 eslint-import-resolver-node: ^0.3.6 @@ -458,12 +458,12 @@ importers: '@hapi/accept': 5.0.2 '@napi-rs/cli': 2.7.0 '@napi-rs/triples': 1.1.0 - '@next/env': 12.3.2-canary.18 - '@next/polyfill-module': 12.3.2-canary.18 - '@next/polyfill-nomodule': 12.3.2-canary.18 - '@next/react-dev-overlay': 12.3.2-canary.18 - '@next/react-refresh-utils': 12.3.2-canary.18 - '@next/swc': 12.3.2-canary.18 + '@next/env': 12.3.2-canary.19 + '@next/polyfill-module': 12.3.2-canary.19 + '@next/polyfill-nomodule': 12.3.2-canary.19 + '@next/react-dev-overlay': 12.3.2-canary.19 + '@next/react-refresh-utils': 12.3.2-canary.19 + '@next/swc': 12.3.2-canary.19 '@segment/ajv-human-errors': 2.1.2 '@swc/helpers': 0.4.11 '@taskr/clear': 1.1.0 From 5af1a930a21f13facef381262884ae5ba3428010 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 4 Oct 2022 11:13:31 -0700 Subject: [PATCH 008/135] Add more test job timeouts (#41162) This adds some upper bound time limits on our test jobs to ensure we aren't slipping on test times or letting CI job stall and waste concurrency. --- .github/workflows/build_test_deploy.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index c509d7429ce99..c945c51077c25 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -208,6 +208,7 @@ jobs: name: Test Unit runs-on: ubuntu-latest needs: [build, build-native-test] + timeout-minutes: 5 env: NEXT_TELEMETRY_DISABLED: 1 NEXT_TEST_JOB: 1 @@ -242,6 +243,7 @@ jobs: name: Test Development runs-on: ubuntu-latest needs: [build, build-native-test] + timeout-minutes: 20 env: NEXT_TELEMETRY_DISABLED: 1 NEXT_TEST_JOB: 1 @@ -362,6 +364,7 @@ jobs: name: Test Development (E2E) runs-on: ubuntu-latest needs: [build, build-native-test] + timeout-minutes: 20 env: NEXT_TELEMETRY_DISABLED: 1 NEXT_TEST_JOB: 1 @@ -502,6 +505,7 @@ jobs: name: Test Production runs-on: ubuntu-latest needs: [build, build-native-test] + timeout-minutes: 15 env: NEXT_TELEMETRY_DISABLED: 1 NEXT_TEST_JOB: 1 @@ -602,6 +606,7 @@ jobs: name: Test Production (E2E) runs-on: ubuntu-latest needs: [build, build-native-test] + timeout-minutes: 25 env: NEXT_TELEMETRY_DISABLED: 1 NEXT_TEST_JOB: 1 @@ -704,6 +709,7 @@ jobs: name: Test Integration runs-on: ubuntu-latest needs: [build, build-native-test] + timeout-minutes: 20 env: NEXT_TELEMETRY_DISABLED: 1 NEXT_TEST_JOB: 1 @@ -785,6 +791,7 @@ jobs: name: Test Electron runs-on: ubuntu-latest needs: [build, build-native-test] + timeout-minutes: 5 env: NEXT_TELEMETRY_DISABLED: 1 NEXT_TEST_JOB: 1 @@ -840,6 +847,7 @@ jobs: name: Test Firefox (production) runs-on: ubuntu-latest needs: [build, build-native-test] + timeout-minutes: 5 env: BROWSER_NAME: 'firefox' NEXT_TELEMETRY_DISABLED: 1 @@ -874,6 +882,7 @@ jobs: name: Test Safari (production) runs-on: ubuntu-latest needs: [build, build-native-test] + timeout-minutes: 10 env: BROWSER_NAME: 'safari' NEXT_TEST_MODE: 'start' @@ -919,6 +928,7 @@ jobs: name: Test Safari 10.1 (nav) runs-on: ubuntu-latest needs: [build, build-native-test] + timeout-minutes: 5 env: BROWSERSTACK: true LEGACY_SAFARI: true @@ -965,6 +975,7 @@ jobs: name: Test Firefox Node.js 18 runs-on: ubuntu-latest needs: [build, testFirefox, build-native-test] + timeout-minutes: 5 env: BROWSER_NAME: 'firefox' NEXT_TELEMETRY_DISABLED: 1 @@ -1237,6 +1248,7 @@ jobs: test-wasm: name: Test the wasm build runs-on: ubuntu-latest + timeout-minutes: 10 needs: [build, build-native-test, build-wasm-dev] steps: From 328c3a765a2164ee2c85955e8aadde7485561c9d Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 4 Oct 2022 11:46:11 -0700 Subject: [PATCH 009/135] Fix reading edge info for app paths (#41163) This fixes the build failing due to attempting to read `edgeInfo` that wasn't present from using the wrong key to look up the manifest entry. Regression test added by enabling `experimental-edge` on a page that was failing to be looked up. Fixes: ```sh TypeError: Cannot read properties of undefined (reading 'files') at /Users/jj/dev/vercel/layouts-playground/node_modules/next/dist/build/utils.js:786:33 at Span.traceAsyncFn (/Users/jj/dev/vercel/layouts-playground/node_modules/next/dist/trace/trace.js:79:26) at Object.isPageStatic (/Users/jj/dev/vercel/layouts-playground/node_modules/next/dist/build/utils.js:771:29) ``` ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` --- packages/next/build/index.ts | 2 +- test/e2e/app-dir/app/app/(rootonly)/dashboard/hello/page.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 2b0482838af1f..8533413b197d9 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -1358,7 +1358,7 @@ export default async function build( MIDDLEWARE_MANIFEST )) const manifestKey = - pageType === 'pages' ? page : join(page, 'page') + pageType === 'pages' ? page : originalAppPath || '' edgeInfo = manifest.functions[manifestKey] } diff --git a/test/e2e/app-dir/app/app/(rootonly)/dashboard/hello/page.js b/test/e2e/app-dir/app/app/(rootonly)/dashboard/hello/page.js index f7d874bf7c554..20fef22a73245 100644 --- a/test/e2e/app-dir/app/app/(rootonly)/dashboard/hello/page.js +++ b/test/e2e/app-dir/app/app/(rootonly)/dashboard/hello/page.js @@ -5,3 +5,7 @@ export default function HelloPage(props) { ) } + +export const config = { + runtime: 'experimental-edge', +} From d2efbc88190742de2c87f5833b1b7f836fcb7e8c Mon Sep 17 00:00:00 2001 From: Adarsh Konchady Date: Tue, 4 Oct 2022 15:34:35 -0500 Subject: [PATCH 010/135] Fix warning messages for next export (#41165) ## Bug Noticed the warning messages don't have spaces. Just adding spaces for better messaging. image - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- packages/next/server/render.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index d6454a76b36b5..6d113cd644651 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -465,8 +465,8 @@ export async function renderToHTML( ) { warn( `Detected getInitialProps on page '${pathname}'` + - `while running "next export". It's recommended to use getStaticProps` + - `which has a more correct behavior for static exporting.` + + ` while running "next export". It's recommended to use getStaticProps` + + ` which has a more correct behavior for static exporting.` + `\nRead more: https://nextjs.org/docs/messages/get-initial-props-export` ) } From 5f2e44d451755259294533ddfac7291f3a5b6ef6 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 5 Oct 2022 00:16:44 +0200 Subject: [PATCH 011/135] Refactor app dir related flags (#41166) simplify the `appDir` passing down --- packages/next-plugin-storybook/preset.js | 2 +- packages/next/build/entries.ts | 8 ++--- packages/next/build/index.ts | 10 +++--- packages/next/build/jest/jest.ts | 7 +++- packages/next/build/swc/jest-transformer.js | 1 + packages/next/build/swc/options.js | 7 +++- packages/next/build/webpack-config.ts | 33 ++++++++++--------- .../build/webpack/config/blocks/css/index.ts | 12 +++---- .../config/blocks/css/loaders/client.ts | 6 ++-- .../config/blocks/css/loaders/font-loader.ts | 2 +- .../config/blocks/css/loaders/global.ts | 2 +- .../config/blocks/css/loaders/modules.ts | 2 +- packages/next/build/webpack/config/index.ts | 8 +++-- packages/next/build/webpack/config/utils.ts | 1 + .../loaders/next-edge-ssr-loader/index.ts | 2 +- .../loaders/next-edge-ssr-loader/render.ts | 3 +- .../build/webpack/loaders/next-swc-loader.js | 2 ++ .../webpack/plugins/middleware-plugin.ts | 2 +- packages/next/cli/next-lint.ts | 4 +-- packages/next/export/index.ts | 7 ++-- packages/next/lib/eslint/runLintCheck.ts | 5 ++- packages/next/lib/find-pages-dir.ts | 26 +++++++++------ packages/next/lib/verifyAndLint.ts | 16 +++++---- packages/next/server/base-server.ts | 7 ++-- packages/next/server/config-schema.ts | 3 -- packages/next/server/dev/hot-reloader.ts | 15 +++++---- packages/next/server/dev/next-dev-server.ts | 9 ++--- packages/next/server/next-server.ts | 19 +++++++---- packages/next/server/web-server.ts | 4 +++ 29 files changed, 132 insertions(+), 93 deletions(-) diff --git a/packages/next-plugin-storybook/preset.js b/packages/next-plugin-storybook/preset.js index 8a70445c8d134..0e83637c8fd4b 100644 --- a/packages/next-plugin-storybook/preset.js +++ b/packages/next-plugin-storybook/preset.js @@ -6,8 +6,8 @@ const getWebpackConfig = require('next/dist/build/webpack-config').default const CWD = process.cwd() async function webpackFinal(config) { - const pagesDir = findPagesDir(CWD) const nextConfig = await loadConfig(PHASE_PRODUCTION_BUILD, CWD) + const pagesDir = findPagesDir(CWD, !!nextConfig.experimental.appDir) const nextWebpackConfig = await getWebpackConfig(CWD, { pagesDir, entrypoints: {}, diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 6b0a6e0a39025..b01a29b94d896 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -164,7 +164,7 @@ export function getEdgeServerEntry(opts: { page: string pages: { [page: string]: string } middleware?: Partial - pagesType?: 'app' | 'pages' | 'root' + pagesType: 'app' | 'pages' | 'root' appDirLoader?: string }) { if (isMiddlewareFile(opts.page)) { @@ -526,13 +526,13 @@ export function finalizeEntrypoint({ compilerType, value, isServerComponent, - appDir, + hasAppDir, }: { compilerType?: CompilerNameValues name: string value: ObjectValue isServerComponent?: boolean - appDir?: boolean + hasAppDir?: boolean }): ObjectValue { const entry = typeof value !== 'object' || Array.isArray(value) @@ -575,7 +575,7 @@ export function finalizeEntrypoint({ name !== CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH ) { // TODO-APP: this is a temporary fix. @shuding is going to change the handling of server components - if (appDir && entry.import.includes('flight')) { + if (hasAppDir && entry.import.includes('flight')) { return { dependOn: CLIENT_STATIC_FILES_RUNTIME_MAIN_APP, ...entry, diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 8533413b197d9..c3b13bde241e7 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -301,10 +301,8 @@ export default async function build( setGlobal('telemetry', telemetry) const publicDir = path.join(dir, 'public') - const { pages: pagesDir, appDir } = findPagesDir( - dir, - config.experimental.appDir - ) + const hasAppDir = !!config.experimental.appDir + const { pagesDir, appDir } = findPagesDir(dir, hasAppDir) const hasPublicDir = await fileExists(publicDir) @@ -396,7 +394,7 @@ export default async function build( config.experimental.cpus, config.experimental.workerThreads, telemetry, - !!config.experimental.appDir + hasAppDir ) }), ]) @@ -1990,7 +1988,7 @@ export default async function build( combinedPages.length > 0 || useStatic404 || useDefaultStatic500 || - config.experimental.appDir + hasAppDir ) { const staticGenerationSpan = nextBuildSpan.traceChild('static-generation') diff --git a/packages/next/build/jest/jest.ts b/packages/next/build/jest/jest.ts index 1dbd27da10374..cb4b50887e3a7 100644 --- a/packages/next/build/jest/jest.ts +++ b/packages/next/build/jest/jest.ts @@ -64,14 +64,18 @@ export default function nextJest(options: { dir?: string } = {}) { let resolvedBaseUrl let isEsmProject = false let pagesDir: string | undefined + let hasServerComponents: boolean | undefined if (options.dir) { const resolvedDir = resolve(options.dir) - pagesDir = findPagesDir(resolvedDir).pages const packageConfig = loadClosestPackageJson(resolvedDir) isEsmProject = packageConfig.type === 'module' nextConfig = await getConfig(resolvedDir) + const hasAppDir = !!nextConfig.experimental.appDir + const findPagesDirResult = findPagesDir(resolvedDir, hasAppDir) + hasServerComponents = !!findPagesDirResult.appDir + pagesDir = findPagesDirResult.pagesDir setUpEnv(resolvedDir, nextConfig) // TODO: revisit when bug in SWC is fixed that strips `.css` const result = await loadJsConfig(resolvedDir, nextConfig) @@ -134,6 +138,7 @@ export default function nextJest(options: { dir?: string } = {}) { nextConfig, jsConfig, resolvedBaseUrl, + hasServerComponents, isEsmProject, pagesDir, }, diff --git a/packages/next/build/swc/jest-transformer.js b/packages/next/build/swc/jest-transformer.js index cc11890430ef6..ac912dfd8fc79 100644 --- a/packages/next/build/swc/jest-transformer.js +++ b/packages/next/build/swc/jest-transformer.js @@ -48,6 +48,7 @@ module.exports = { jsConfig: inputOptions.jsConfig, resolvedBaseUrl: inputOptions.resolvedBaseUrl, pagesDir: inputOptions.pagesDir, + hasServerComponents: inputOptions.hasServerComponents, esm: isSupportEsm && isEsm(Boolean(inputOptions.isEsmProject), filename, jestConfig), diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js index 71043d3bd003c..6411fb427cbab 100644 --- a/packages/next/build/swc/options.js +++ b/packages/next/build/swc/options.js @@ -34,6 +34,7 @@ function getBaseSWCOptions({ swcCacheDir, isServerLayer, relativeFilePathFromRoot, + hasServerComponents, }) { const parserConfig = getParserOptions({ filename, jsConfig }) const paths = jsConfig?.compilerOptions?.paths @@ -119,7 +120,7 @@ function getBaseSWCOptions({ modularizeImports: nextConfig?.experimental?.modularizeImports, relay: nextConfig?.compiler?.relay, emotion: getEmotionOptions(nextConfig, development), - serverComponents: nextConfig?.experimental?.appDir + serverComponents: hasServerComponents ? { isServer: !!isServerLayer, } @@ -180,6 +181,7 @@ export function getJestSWCOptions({ nextConfig, jsConfig, pagesDir, + hasServerComponents, // This is not passed yet as "paths" resolving needs a test first // resolvedBaseUrl, }) { @@ -191,6 +193,7 @@ export function getJestSWCOptions({ globalWindow: !isServer, nextConfig, jsConfig, + hasServerComponents, // resolvedBaseUrl, }) @@ -226,6 +229,7 @@ export function getLoaderSWCOptions({ supportedBrowsers, swcCacheDir, relativeFilePathFromRoot, + hasServerComponents, // This is not passed yet as "paths" resolving is handled by webpack currently. // resolvedBaseUrl, }) { @@ -240,6 +244,7 @@ export function getLoaderSWCOptions({ swcCacheDir, isServerLayer, relativeFilePathFromRoot, + hasServerComponents, }) const isNextDist = nextDistPath.test(filename) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 7cff337e20cc7..99d48bf29f3ac 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -33,9 +33,9 @@ import { execOnce } from '../shared/lib/utils' import { NextConfigComplete } from '../server/config-shared' import { finalizeEntrypoint } from './entries' import * as Log from './output/log' -import { build as buildConfiguration } from './webpack/config' +import { buildConfiguration } from './webpack/config' import MiddlewarePlugin, { - handleWebpackExtenalForEdgeRuntime, + handleWebpackExternalForEdgeRuntime, } from './webpack/plugins/middleware-plugin' import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin' import { JsConfigPathsPlugin } from './webpack/plugins/jsconfig-paths-plugin' @@ -563,6 +563,10 @@ export default async function getBaseWebpackConfig( rewrites.afterFiles.length > 0 || rewrites.fallback.length > 0 + const hasAppDir = !!config.experimental.appDir + const hasConcurrentFeatures = hasReactRoot + const hasServerComponents = hasAppDir + // Only error in first one compiler (client) once if (isClient) { if (!hasReactRoot) { @@ -571,7 +575,7 @@ export default async function getBaseWebpackConfig( '`experimental.runtime` requires React 18 to be installed.' ) } - if (config.experimental.appDir) { + if (hasAppDir) { throw new Error( '`experimental.appDir` requires React 18 to be installed.' ) @@ -579,8 +583,6 @@ export default async function getBaseWebpackConfig( } } - const hasConcurrentFeatures = hasReactRoot - const hasServerComponents = !!config.experimental.appDir const disableOptimizedLoading = hasConcurrentFeatures ? true : config.experimental.disableOptimizedLoading @@ -654,6 +656,7 @@ export default async function getBaseWebpackConfig( pagesDir, cwd: dir, development: dev, + hasServerComponents, hasReactRefresh: dev && isClient, hasJsxRuntime: true, }, @@ -682,6 +685,7 @@ export default async function getBaseWebpackConfig( isServer: isNodeServer || isEdgeServer, rootDir: dir, pagesDir, + hasServerComponents, hasReactRefresh: dev && isClient, fileReading: config.experimental.swcFileReading, nextConfig: config, @@ -745,7 +749,7 @@ export default async function getBaseWebpackConfig( ) ) .replace(/\\/g, '/'), - ...(config.experimental.appDir + ...(hasAppDir ? { [CLIENT_STATIC_FILES_RUNTIME_MAIN_APP]: dev ? [ @@ -1215,7 +1219,7 @@ export default async function getBaseWebpackConfig( '{}', 'react-dom': '{}', }, - handleWebpackExtenalForEdgeRuntime, + handleWebpackExternalForEdgeRuntime, ] : []), ] @@ -1520,7 +1524,7 @@ export default async function getBaseWebpackConfig( }, module: { rules: [ - ...(config.experimental.appDir && !isClient && !isEdgeServer + ...(hasAppDir && !isClient && !isEdgeServer ? [ { issuerLayer: WEBPACK_LAYERS.server, @@ -1827,7 +1831,7 @@ export default async function getBaseWebpackConfig( appDir: dir, esmExternals: config.experimental.esmExternals, outputFileTracingRoot: config.experimental.outputFileTracingRoot, - appDirEnabled: !!config.experimental.appDir, + appDirEnabled: hasAppDir, } ), // Moment.js is an extremely popular library that bundles large locale files @@ -1872,7 +1876,7 @@ export default async function getBaseWebpackConfig( serverless: isLikeServerless, dev, isEdgeRuntime: isEdgeServer, - appDirEnabled: !!config.experimental.appDir, + appDirEnabled: hasAppDir, }), // MiddlewarePlugin should be after DefinePlugin so NEXT_PUBLIC_* // replacement is done before its process.env.* handling @@ -1888,7 +1892,7 @@ export default async function getBaseWebpackConfig( rewrites, isDevFallback, exportRuntime: hasConcurrentFeatures, - appDirEnabled: !!config.experimental.appDir, + appDirEnabled: hasAppDir, }), new ProfilingPlugin({ runWebpackSpan }), config.optimizeFonts && @@ -1919,9 +1923,7 @@ export default async function getBaseWebpackConfig( minimized: true, }, }), - !!config.experimental.appDir && - isClient && - new AppBuildManifestPlugin({ dev }), + hasAppDir && isClient && new AppBuildManifestPlugin({ dev }), hasServerComponents && (isClient ? new FlightManifestPlugin({ @@ -2200,6 +2202,7 @@ export default async function getBaseWebpackConfig( customAppFile: pagesDir ? new RegExp(escapeStringRegexp(path.join(pagesDir, `_app`))) : undefined, + hasAppDir, isDevelopment: dev, isServer: isNodeServer || isEdgeServer, isEdgeRuntime: isEdgeServer, @@ -2588,7 +2591,7 @@ export default async function getBaseWebpackConfig( value: entry[name], compilerType, name, - appDir: config.experimental.appDir, + hasAppDir, }) } diff --git a/packages/next/build/webpack/config/blocks/css/index.ts b/packages/next/build/webpack/config/blocks/css/index.ts index 9de0f420288c9..bb8a8d8570c2e 100644 --- a/packages/next/build/webpack/config/blocks/css/index.ts +++ b/packages/next/build/webpack/config/blocks/css/index.ts @@ -257,7 +257,7 @@ export const css = curry(async function css( // CSS Modules support must be enabled on the server and client so the class // names are available for SSR or Prerendering. - if (ctx.experimental.appDir && !ctx.isProduction) { + if (ctx.hasAppDir && !ctx.isProduction) { fns.push( loader({ oneOf: [ @@ -373,7 +373,7 @@ export const css = curry(async function css( ) } - if (!ctx.experimental.appDir) { + if (!ctx.hasAppDir) { // Throw an error for CSS Modules used outside their supported scope fns.push( loader({ @@ -393,7 +393,7 @@ export const css = curry(async function css( } if (ctx.isServer) { - if (ctx.experimental.appDir && !ctx.isProduction) { + if (ctx.hasAppDir && !ctx.isProduction) { fns.push( loader({ oneOf: [ @@ -420,7 +420,7 @@ export const css = curry(async function css( ) } } else { - if (ctx.experimental.appDir) { + if (ctx.hasAppDir) { fns.push( loader({ oneOf: [ @@ -552,7 +552,7 @@ export const css = curry(async function css( oneOf: [ markRemovable({ test: [regexCssGlobal, regexSassGlobal], - issuer: ctx.experimental.appDir + issuer: ctx.hasAppDir ? { // If it's inside the app dir, but not importing from a layout file, // throw an error. @@ -597,7 +597,7 @@ export const css = curry(async function css( } // Enable full mini-css-extract-plugin hmr for prod mode pages or app dir - if (ctx.isClient && (ctx.isProduction || ctx.experimental.appDir)) { + if (ctx.isClient && (ctx.isProduction || ctx.hasAppDir)) { // Extract CSS as CSS file(s) in the client-side production bundle. const MiniCssExtractPlugin = require('../../../plugins/mini-css-extract-plugin').default diff --git a/packages/next/build/webpack/config/blocks/css/loaders/client.ts b/packages/next/build/webpack/config/blocks/css/loaders/client.ts index 78d662643b46c..be889904ebc72 100644 --- a/packages/next/build/webpack/config/blocks/css/loaders/client.ts +++ b/packages/next/build/webpack/config/blocks/css/loaders/client.ts @@ -1,16 +1,16 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack' export function getClientStyleLoader({ - isAppDir, + hasAppDir, isDevelopment, assetPrefix, }: { - isAppDir: boolean + hasAppDir: boolean isDevelopment: boolean assetPrefix: string }): webpack.RuleSetUseItem { // Keep next-style-loader for development mode in `pages/` - if (isDevelopment && !isAppDir) { + if (isDevelopment && !hasAppDir) { return { loader: 'next-style-loader', options: { diff --git a/packages/next/build/webpack/config/blocks/css/loaders/font-loader.ts b/packages/next/build/webpack/config/blocks/css/loaders/font-loader.ts index c48efd1d75916..0da13432aba48 100644 --- a/packages/next/build/webpack/config/blocks/css/loaders/font-loader.ts +++ b/packages/next/build/webpack/config/blocks/css/loaders/font-loader.ts @@ -15,7 +15,7 @@ export function getFontLoader( // loader loaders.push( getClientStyleLoader({ - isAppDir: !!ctx.experimental.appDir, + hasAppDir: ctx.hasAppDir, isDevelopment: ctx.isDevelopment, assetPrefix: ctx.assetPrefix, }) diff --git a/packages/next/build/webpack/config/blocks/css/loaders/global.ts b/packages/next/build/webpack/config/blocks/css/loaders/global.ts index 6e4a4e6bbcf5b..4f8c40205c059 100644 --- a/packages/next/build/webpack/config/blocks/css/loaders/global.ts +++ b/packages/next/build/webpack/config/blocks/css/loaders/global.ts @@ -16,7 +16,7 @@ export function getGlobalCssLoader( // loader loaders.push( getClientStyleLoader({ - isAppDir: !!ctx.experimental.appDir, + hasAppDir: ctx.hasAppDir, isDevelopment: ctx.isDevelopment, assetPrefix: ctx.assetPrefix, }) diff --git a/packages/next/build/webpack/config/blocks/css/loaders/modules.ts b/packages/next/build/webpack/config/blocks/css/loaders/modules.ts index 6f7c0937ed701..4048058dbf2d2 100644 --- a/packages/next/build/webpack/config/blocks/css/loaders/modules.ts +++ b/packages/next/build/webpack/config/blocks/css/loaders/modules.ts @@ -16,7 +16,7 @@ export function getCssModuleLoader( // loader loaders.push( getClientStyleLoader({ - isAppDir: !!ctx.experimental.appDir, + hasAppDir: ctx.hasAppDir, isDevelopment: ctx.isDevelopment, assetPrefix: ctx.assetPrefix, }) diff --git a/packages/next/build/webpack/config/index.ts b/packages/next/build/webpack/config/index.ts index 201403bae8c26..bc51ff125371a 100644 --- a/packages/next/build/webpack/config/index.ts +++ b/packages/next/build/webpack/config/index.ts @@ -1,14 +1,16 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack' import type { NextConfigComplete } from '../../../server/config-shared' +import type { ConfigurationContext } from './utils' import { base } from './blocks/base' import { css } from './blocks/css' import { images } from './blocks/images' -import { ConfigurationContext, pipe } from './utils' +import { pipe } from './utils' -export async function build( +export async function buildConfiguration( config: webpack.Configuration, { + hasAppDir, supportedBrowsers, rootDirectory, customAppFile, @@ -23,6 +25,7 @@ export async function build( experimental, disableStaticImages, }: { + hasAppDir: boolean supportedBrowsers: string[] | undefined rootDirectory: string customAppFile: RegExp | undefined @@ -39,6 +42,7 @@ export async function build( } ): Promise { const ctx: ConfigurationContext = { + hasAppDir, supportedBrowsers, rootDirectory, customAppFile, diff --git a/packages/next/build/webpack/config/utils.ts b/packages/next/build/webpack/config/utils.ts index b8355ad52ef52..3c9092bf6ca0f 100644 --- a/packages/next/build/webpack/config/utils.ts +++ b/packages/next/build/webpack/config/utils.ts @@ -2,6 +2,7 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack' import type { NextConfigComplete } from '../../../server/config-shared' export type ConfigurationContext = { + hasAppDir: boolean supportedBrowsers: string[] | undefined rootDirectory: string customAppFile: RegExp | undefined diff --git a/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts index 19d649f50878d..c9a9c12316c3f 100644 --- a/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts @@ -13,7 +13,7 @@ export type EdgeSSRLoaderQuery = { page: string stringifiedConfig: string appDirLoader?: string - pagesType?: 'app' | 'pages' | 'root' + pagesType: 'app' | 'pages' | 'root' sriEnabled: boolean hasFontLoaders: boolean } diff --git a/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts index 6210f458c7768..6eb8ffa3f09b6 100644 --- a/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts @@ -32,7 +32,7 @@ export function getRender({ buildId, fontLoaderManifest, }: { - pagesType?: 'app' | 'pages' | 'root' + pagesType: 'app' | 'pages' | 'root' dev: boolean page: string appMod: any @@ -69,6 +69,7 @@ export function getRender({ minimalMode: true, webServerConfig: { page, + pagesType, extendRenderOpts: { buildId, runtime: SERVER_RUNTIME.edge, diff --git a/packages/next/build/webpack/loaders/next-swc-loader.js b/packages/next/build/webpack/loaders/next-swc-loader.js index 4826ab9156c00..959549747bde2 100644 --- a/packages/next/build/webpack/loaders/next-swc-loader.js +++ b/packages/next/build/webpack/loaders/next-swc-loader.js @@ -46,6 +46,7 @@ async function loaderTransform(parentTrace, source, inputSourceMap) { jsConfig, supportedBrowsers, swcCacheDir, + hasServerComponents, } = loaderOptions const isPageFile = filename.startsWith(pagesDir) const relativeFilePathFromRoot = path.relative(rootDir, filename) @@ -63,6 +64,7 @@ async function loaderTransform(parentTrace, source, inputSourceMap) { supportedBrowsers, swcCacheDir, relativeFilePathFromRoot, + hasServerComponents, }) const programmaticOptions = { diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 0d2908a9e46a2..e0e206bb11541 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -868,7 +868,7 @@ export default class MiddlewarePlugin { } } -export async function handleWebpackExtenalForEdgeRuntime({ +export async function handleWebpackExternalForEdgeRuntime({ request, context, contextInfo, diff --git a/packages/next/cli/next-lint.ts b/packages/next/cli/next-lint.ts index 83f0a53bb9dec..a678fa2f9b12f 100755 --- a/packages/next/cli/next-lint.ts +++ b/packages/next/cli/next-lint.ts @@ -187,8 +187,9 @@ const nextLint: cliCommand = async (argv) => { const distDir = join(baseDir, nextConfig.distDir) const defaultCacheLocation = join(distDir, 'cache', 'eslint/') + const hasAppDir = !!nextConfig.experimental.appDir - runLintCheck(baseDir, pathsToLint, { + runLintCheck(baseDir, pathsToLint, hasAppDir, { lintDuringBuild: false, eslintOptions: eslintOptions(args, defaultCacheLocation), reportErrorsOnly: reportErrorsOnly, @@ -196,7 +197,6 @@ const nextLint: cliCommand = async (argv) => { formatter, outputFile, strict, - hasAppDir: !!nextConfig.experimental.appDir, }) .then(async (lintResults) => { const lintOutput = diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 5cb8eb05842ff..9650a54248060 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -147,6 +147,7 @@ export default async function exportApp( configuration?: NextConfigComplete ): Promise { const nextExportSpan = span.traceChild('next-export') + const hasAppDir = !!options.appPaths return nextExportSpan.traceAsyncFn(async () => { dir = resolve(dir) @@ -389,7 +390,7 @@ export default async function exportApp( nextScriptWorkers: nextConfig.experimental.nextScriptWorkers, optimizeFonts: nextConfig.optimizeFonts as FontConfig, largePageDataBytes: nextConfig.experimental.largePageDataBytes, - serverComponents: !!nextConfig.experimental.appDir, + serverComponents: hasAppDir, fontLoaderManifest: nextConfig.experimental.fontLoaders ? require(join(distDir, 'server', `${FONT_LOADER_MANIFEST}.json`)) : undefined, @@ -422,7 +423,7 @@ export default async function exportApp( return exportMap }) - if (options.buildExport && nextConfig.experimental.appDir) { + if (options.buildExport && hasAppDir) { // @ts-expect-error untyped renderOpts.serverComponentManifest = require(join( distDir, @@ -617,7 +618,7 @@ export default async function exportApp( nextConfig.experimental.disableOptimizedLoading, parentSpanId: pageExportSpan.id, httpAgentOptions: nextConfig.httpAgentOptions, - serverComponents: !!nextConfig.experimental.appDir, + serverComponents: hasAppDir, appPaths: options.appPaths || [], enableUndici: nextConfig.experimental.enableUndici, }) diff --git a/packages/next/lib/eslint/runLintCheck.ts b/packages/next/lib/eslint/runLintCheck.ts index 6e735741dcd24..96e9118e6fcb8 100644 --- a/packages/next/lib/eslint/runLintCheck.ts +++ b/packages/next/lib/eslint/runLintCheck.ts @@ -185,7 +185,7 @@ async function lint( } } - const pagesDir = findPagesDir(baseDir, hasAppDir).pages + const pagesDir = findPagesDir(baseDir, hasAppDir).pagesDir const pagesDirRules = pagesDir ? ['@next/next/no-html-link-for-pages'] : [] if (nextEslintPluginIsEnabled) { @@ -277,6 +277,7 @@ async function lint( export async function runLintCheck( baseDir: string, lintDirs: string[], + hasAppDir: boolean, opts: { lintDuringBuild?: boolean eslintOptions?: any @@ -285,7 +286,6 @@ export async function runLintCheck( formatter?: string | null outputFile?: string | null strict?: boolean - hasAppDir: boolean } ): ReturnType { const { @@ -296,7 +296,6 @@ export async function runLintCheck( formatter = null, outputFile = null, strict = false, - hasAppDir, } = opts try { // Find user's .eslintrc file diff --git a/packages/next/lib/find-pages-dir.ts b/packages/next/lib/find-pages-dir.ts index 2edec19c3f99c..0de35966f5616 100644 --- a/packages/next/lib/find-pages-dir.ts +++ b/packages/next/lib/find-pages-dir.ts @@ -23,21 +23,27 @@ function findDir(dir: string, name: 'pages' | 'app'): string | null { export function findPagesDir( dir: string, - appDirEnabled?: boolean -): { pages: string | undefined; appDir: string | undefined } { + isAppDirEnabled: boolean +): { + pagesDir: string | undefined + appDir: string | undefined +} { const pagesDir = findDir(dir, 'pages') || undefined let appDir: undefined | string - if (appDirEnabled) { + if (isAppDirEnabled) { appDir = findDir(dir, 'app') || undefined - if (appDirEnabled == null && pagesDir == null) { - throw new Error( - "> Couldn't find any `pages` or `app` directory. Please create one under the project root" - ) - } + } + const hasAppDir = + !!appDir && fs.existsSync(appDir) && fs.statSync(appDir).isDirectory() + + if (hasAppDir && appDir == null && pagesDir == null) { + throw new Error( + "> Couldn't find any `pages` or `app` directory. Please create one under the project root" + ) } - if (!appDirEnabled) { + if (!isAppDirEnabled) { if (pagesDir == null) { throw new Error( "> Couldn't find a `pages` directory. Please create one under the project root" @@ -46,7 +52,7 @@ export function findPagesDir( } return { - pages: pagesDir, + pagesDir, appDir, } } diff --git a/packages/next/lib/verifyAndLint.ts b/packages/next/lib/verifyAndLint.ts index b5e47d423f823..40da86f39ba28 100644 --- a/packages/next/lib/verifyAndLint.ts +++ b/packages/next/lib/verifyAndLint.ts @@ -39,13 +39,17 @@ export async function verifyAndLint( [] ) - const lintResults = await lintWorkers.runLintCheck(dir, lintDirs, { + const lintResults = await lintWorkers.runLintCheck( + dir, + lintDirs, hasAppDir, - lintDuringBuild: true, - eslintOptions: { - cacheLocation, - }, - }) + { + lintDuringBuild: true, + eslintOptions: { + cacheLocation, + }, + } + ) const lintOutput = typeof lintResults === 'string' ? lintResults : lintResults?.output diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index d26be948965d1..2735dd3382dac 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -185,6 +185,7 @@ export default abstract class Server { protected distDir: string protected publicDir: string protected hasStaticDir: boolean + protected hasAppDir: boolean protected pagesManifest?: PagesManifest protected appPathsManifest?: PagesManifest protected buildId: string @@ -237,6 +238,7 @@ export default abstract class Server { protected abstract getPublicDir(): string protected abstract getHasStaticDir(): boolean + protected abstract getHasAppDir(): boolean protected abstract getPagesManifest(): PagesManifest | undefined protected abstract getAppPathsManifest(): PagesManifest | undefined protected abstract getBuildId(): string @@ -353,6 +355,7 @@ export default abstract class Server { : require('path').join(this.dir, this.nextConfig.distDir) this.publicDir = this.getPublicDir() this.hasStaticDir = !minimalMode && this.getHasStaticDir() + this.hasAppDir = this.getHasAppDir() // Only serverRuntimeConfig needs the default // publicRuntimeConfig gets it's default in client/index.js @@ -366,7 +369,7 @@ export default abstract class Server { this.buildId = this.getBuildId() this.minimalMode = minimalMode || !!process.env.NEXT_PRIVATE_MINIMAL_MODE - const serverComponents = !!this.nextConfig.experimental.appDir + const serverComponents = this.hasAppDir this.serverComponentManifest = serverComponents ? this.getServerComponentManifest() : undefined @@ -1588,7 +1591,7 @@ export default abstract class Server { // map the route to the actual bundle name protected getOriginalAppPaths(route: string) { - if (this.nextConfig.experimental.appDir) { + if (this.hasAppDir) { const originalAppPath = this.appPathRoutes?.[route] if (!originalAppPath) { diff --git a/packages/next/server/config-schema.ts b/packages/next/server/config-schema.ts index b0fef4eeb818f..745148e3ebcb9 100644 --- a/packages/next/server/config-schema.ts +++ b/packages/next/server/config-schema.ts @@ -240,9 +240,6 @@ const configSchema = { }, type: 'object', }, - appDir: { - type: 'boolean', - }, browsersListForSwc: { type: 'boolean', }, diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 0718a3633e19a..84314c211b96a 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -217,7 +217,7 @@ export default class HotReloader { this.config = config this.hasReactRoot = !!process.env.__NEXT_REACT_ROOT - this.hasServerComponents = this.hasReactRoot && !!config.experimental.appDir + this.hasServerComponents = this.hasReactRoot && !!this.appDir this.previewProps = previewProps this.rewrites = rewrites this.hotReloaderSpan = trace('hot-reloader', undefined, { @@ -595,7 +595,8 @@ export default class HotReloader { } } - const isAppPath = !!this.appDir && bundlePath.startsWith('app/') + const hasAppDir = !!this.appDir + const isAppPath = hasAppDir && bundlePath.startsWith('app/') const staticInfo = isEntry ? await getPageStaticInfo({ pageFilePath: entryData.absolutePagePath, @@ -643,9 +644,9 @@ export default class HotReloader { pages: this.pagesMapping, isServerComponent, appDirLoader, - pagesType: isAppPath ? 'app' : undefined, + pagesType: isAppPath ? 'app' : 'pages', }), - appDir: this.config.experimental.appDir, + hasAppDir, }) }, onClient: () => { @@ -656,7 +657,7 @@ export default class HotReloader { name: bundlePath, compilerType: COMPILER_NAMES.client, value: entryData.request, - appDir: this.config.experimental.appDir, + hasAppDir, }) } else { entries[entryKey].status = BUILDING @@ -667,7 +668,7 @@ export default class HotReloader { absolutePagePath: entryData.absolutePagePath, page, }), - appDir: this.config.experimental.appDir, + hasAppDir, }) } }, @@ -705,7 +706,7 @@ export default class HotReloader { pageExtensions: this.config.pageExtensions, }) : relativeRequest, - appDir: this.config.experimental.appDir, + hasAppDir, }) }, }) diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index edb45aa982840..8b4da6444666d 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -190,11 +190,8 @@ export default class DevServer extends Server { } this.isCustomServer = !options.isNextDevCommand - // TODO: hot-reload root/pages dirs? - const { pages: pagesDir, appDir } = findPagesDir( - this.dir, - this.nextConfig.experimental.appDir - ) + + const { pagesDir, appDir } = findPagesDir(this.dir, this.hasAppDir) this.pagesDir = pagesDir this.appDir = appDir } @@ -1355,7 +1352,7 @@ export default class DevServer extends Server { // When the new page is compiled, we need to reload the server component // manifest. - if (this.nextConfig.experimental.appDir) { + if (!!this.appDir) { this.serverComponentManifest = super.getServerComponentManifest() this.serverCSSManifest = super.getServerCSSManifest() } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 68aab94a4ac7b..54dced8895134 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -286,7 +286,7 @@ export default class NextNodeServer extends BaseServer { fs: this.getCacheFilesystem(), dev, serverDistDir: this.serverDistDir, - appDir: this.nextConfig.experimental.appDir, + appDir: this.hasAppDir, maxMemoryCacheSize: this.nextConfig.experimental.isrMemoryCacheSize, flushToDisk: !this.minimalMode && this.nextConfig.experimental.isrFlushToDisk, @@ -323,7 +323,7 @@ export default class NextNodeServer extends BaseServer { } protected getAppPathsManifest(): PagesManifest | undefined { - if (this.nextConfig.experimental.appDir) { + if (this.hasAppDir) { const appPathsManifestPath = join(this.serverDistDir, APP_PATHS_MANIFEST) return require(appPathsManifestPath) } @@ -474,6 +474,13 @@ export default class NextNodeServer extends BaseServer { ] } + protected getHasAppDir(): boolean { + const appDirectory = join(this.dir, 'app') + return ( + fs.existsSync(appDirectory) && fs.statSync(appDirectory).isDirectory() + ) + } + protected generateStaticRoutes(): Route[] { return this.hasStaticDir ? [ @@ -823,7 +830,7 @@ export default class NextNodeServer extends BaseServer { renderOpts.serverCSSManifest = this.serverCSSManifest renderOpts.fontLoaderManifest = this.fontLoaderManifest - if (this.nextConfig.experimental.appDir && renderOpts.isAppPath) { + if (this.hasAppDir && renderOpts.isAppPath) { return appRenderToHTMLOrFlight( req.originalRequest, res.originalResponse, @@ -882,7 +889,7 @@ export default class NextNodeServer extends BaseServer { this._isLikeServerless, this.renderOpts.dev, locales, - this.nextConfig.experimental.appDir + this.hasAppDir ) } @@ -998,12 +1005,12 @@ export default class NextNodeServer extends BaseServer { } protected getServerComponentManifest() { - if (!this.nextConfig.experimental.appDir) return undefined + if (!this.hasAppDir) return undefined return require(join(this.distDir, 'server', FLIGHT_MANIFEST + '.json')) } protected getServerCSSManifest() { - if (!this.nextConfig.experimental.appDir) return undefined + if (!this.hasAppDir) return undefined return require(join( this.distDir, 'server', diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts index 2c58740b41d9d..664accbbaabb4 100644 --- a/packages/next/server/web-server.ts +++ b/packages/next/server/web-server.ts @@ -31,6 +31,7 @@ import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex' interface WebServerOptions extends Options { webServerConfig: { page: string + pagesType: 'app' | 'pages' | 'root' loadComponent: ( pathname: string ) => Promise @@ -88,6 +89,9 @@ export default class NextWebServer extends BaseServer { // The web server does not need to load the env config. This is done by the // runtime already. } + protected getHasAppDir() { + return this.serverOptions.webServerConfig.pagesType === 'app' + } protected getHasStaticDir() { return false } From 81b818515af066fc2112c2b1116a91c3646062ee Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Wed, 5 Oct 2022 15:45:46 +0200 Subject: [PATCH 012/135] Fix prefetch for new router (#41119) - Add a failing test for navigating between many levels of dynamic routes - Create router tree during prefetch action so that it can be reused across multiple urls - Ensure segmentPath is correct when rendering a subtree. Previously it would generate a segmentPath that starts at the level it renders at which causes the layout-router fetchServerResponse to inject `refetch` at the wrong level. - Fixed a case where Segment was compared using `===` which is no longer valid as dynamic parameters are expressed as arrays. Used `matchSegment` helper instead. ## Bug - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- .../client/components/app-router.client.tsx | 58 +++++++++--------- .../components/layout-router.client.tsx | 24 +------- packages/next/client/components/reducer.ts | 37 ++++++++---- packages/next/server/app-render.tsx | 59 +++++++++++++------ .../app/app/nested-navigation/CategoryNav.js | 28 +++++++++ .../app/app/nested-navigation/TabNavItem.js | 9 +++ .../[categorySlug]/SubCategoryNav.js | 31 ++++++++++ .../[categorySlug]/[subCategorySlug]/page.js | 11 ++++ .../[categorySlug]/layout.js | 14 +++++ .../nested-navigation/[categorySlug]/page.js | 9 +++ .../app/nested-navigation/getCategories.js | 50 ++++++++++++++++ .../app/app/nested-navigation/layout.js | 17 ++++++ .../app-dir/app/app/nested-navigation/page.js | 3 + test/e2e/app-dir/index.test.ts | 39 +++++++++++- 14 files changed, 307 insertions(+), 82 deletions(-) create mode 100644 test/e2e/app-dir/app/app/nested-navigation/CategoryNav.js create mode 100644 test/e2e/app-dir/app/app/nested-navigation/TabNavItem.js create mode 100644 test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/SubCategoryNav.js create mode 100644 test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/[subCategorySlug]/page.js create mode 100644 test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/layout.js create mode 100644 test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/page.js create mode 100644 test/e2e/app-dir/app/app/nested-navigation/getCategories.js create mode 100644 test/e2e/app-dir/app/app/nested-navigation/layout.js create mode 100644 test/e2e/app-dir/app/app/nested-navigation/page.js diff --git a/packages/next/client/components/app-router.client.tsx b/packages/next/client/components/app-router.client.tsx index e6011fcf77a29..fa6e25a6388f1 100644 --- a/packages/next/client/components/app-router.client.tsx +++ b/packages/next/client/components/app-router.client.tsx @@ -15,7 +15,7 @@ import type { import type { FlightRouterState, FlightData } from '../../server/app-render' import { ACTION_NAVIGATE, - // ACTION_PREFETCH, + ACTION_PREFETCH, ACTION_RELOAD, ACTION_RESTORE, ACTION_SERVER_PATCH, @@ -97,7 +97,7 @@ function ErrorOverlay({ children }: PropsWithChildren<{}>): ReactElement { let initialParallelRoutes: CacheNode['parallelRoutes'] = typeof window === 'undefined' ? null! : new Map() -// const prefetched = new Set() +const prefetched = new Set() /** * The global router that wraps the application components. @@ -208,32 +208,34 @@ export default function AppRouter({ const routerInstance: AppRouterInstance = { // TODO-APP: implement prefetching of flight - prefetch: async (_href) => { + prefetch: async (href) => { // If prefetch has already been triggered, don't trigger it again. - // if (prefetched.has(href)) { - // return - // } - // prefetched.add(href) - // const url = new URL(href, location.origin) - // try { - // // TODO-APP: handle case where history.state is not the new router history entry - // const serverResponse = await fetchServerResponse( - // url, - // // initialTree is used when history.state.tree is missing because the history state is set in `useEffect` below, it being missing means this is the hydration case. - // window.history.state?.tree || initialTree, - // true - // ) - // // @ts-ignore startTransition exists - // React.startTransition(() => { - // dispatch({ - // type: ACTION_PREFETCH, - // url, - // serverResponse, - // }) - // }) - // } catch (err) { - // console.error('PREFETCH ERROR', err) - // } + if (prefetched.has(href)) { + return + } + prefetched.add(href) + const url = new URL(href, location.origin) + try { + const routerTree = window.history.state?.tree || initialTree + // TODO-APP: handle case where history.state is not the new router history entry + const serverResponse = await fetchServerResponse( + url, + // initialTree is used when history.state.tree is missing because the history state is set in `useEffect` below, it being missing means this is the hydration case. + routerTree, + true + ) + // @ts-ignore startTransition exists + React.startTransition(() => { + dispatch({ + type: ACTION_PREFETCH, + url, + tree: routerTree, + serverResponse, + }) + }) + } catch (err) { + console.error('PREFETCH ERROR', err) + } }, replace: (href, options = {}) => { // @ts-ignore startTransition exists @@ -266,7 +268,7 @@ export default function AppRouter({ } return routerInstance - }, [dispatch /*, initialTree*/]) + }, [dispatch, initialTree]) useEffect(() => { // When mpaNavigation flag is set do a hard navigation to the new url. diff --git a/packages/next/client/components/layout-router.client.tsx b/packages/next/client/components/layout-router.client.tsx index ed96a6fb390f8..447a82f15d3dd 100644 --- a/packages/next/client/components/layout-router.client.tsx +++ b/packages/next/client/components/layout-router.client.tsx @@ -29,27 +29,7 @@ import { import { fetchServerResponse } from './app-router.client' import { createInfinitePromise } from './infinite-promise' -// import { matchSegment } from './match-segments' - -/** - * Check if every segment in array a and b matches - */ -// function equalSegmentPaths(a: Segment[], b: Segment[]) { -// // Comparing length is a fast path. -// return a.length === b.length && a.every((val, i) => matchSegment(val, b[i])) -// } - -/** - * Check if flightDataPath matches layoutSegmentPath - */ -// function segmentPathMatches( -// flightDataPath: FlightDataPath, -// layoutSegmentPath: FlightSegmentPath -// ): boolean { -// // The last three items are the current segment, tree, and subTreeData -// const pathToLayout = flightDataPath.slice(0, -3) -// return equalSegmentPaths(layoutSegmentPath, pathToLayout) -// } +import { matchSegment } from './match-segments' /** * Add refetch marker to router state at the point of the current layout segment. @@ -63,7 +43,7 @@ function walkAddRefetch( const [segment, parallelRouteKey] = segmentPathToWalk const isLast = segmentPathToWalk.length === 2 - if (treeToRecreate[0] === segment) { + if (matchSegment(treeToRecreate[0], segment)) { if (treeToRecreate[1].hasOwnProperty(parallelRouteKey)) { if (isLast) { const subTree = walkAddRefetch( diff --git a/packages/next/client/components/reducer.ts b/packages/next/client/components/reducer.ts index b1bb80536369c..ff7517af44a6d 100644 --- a/packages/next/client/components/reducer.ts +++ b/packages/next/client/components/reducer.ts @@ -590,6 +590,7 @@ interface ServerPatchAction { interface PrefetchAction { type: typeof ACTION_PREFETCH url: URL + tree: FlightRouterState serverResponse: Awaited> } @@ -627,7 +628,7 @@ type AppRouterState = { string, { flightSegmentPath: FlightSegmentPath - treePatch: FlightRouterState + tree: FlightRouterState canonicalUrlOverride: URL | undefined } > @@ -691,16 +692,11 @@ function clientReducer( const prefetchValues = state.prefetchCache.get(href) if (prefetchValues) { // The one before last item is the router state tree patch - const { flightSegmentPath, treePatch, canonicalUrlOverride } = - prefetchValues - - // Create new tree based on the flightSegmentPath and router state patch - const newTree = applyRouterStatePatchToTree( - // TODO-APP: remove '' - ['', ...flightSegmentPath], - state.tree, - treePatch - ) + const { + flightSegmentPath, + tree: newTree, + canonicalUrlOverride, + } = prefetchValues if (newTree !== null) { mutable.previousTree = state.tree @@ -1130,11 +1126,26 @@ function clientReducer( fillCacheWithPrefetchedSubTreeData(state.cache, flightDataPath) } + const flightSegmentPath = flightDataPath.slice(0, -2) + + const newTree = applyRouterStatePatchToTree( + // TODO-APP: remove '' + ['', ...flightSegmentPath], + state.tree, + treePatch + ) + + // Patch did not apply correctly + if (newTree === null) { + return state + } + // Create new tree based on the flightSegmentPath and router state patch state.prefetchCache.set(href, { // Path without the last segment, router state, and the subTreeData - flightSegmentPath: flightDataPath.slice(0, -2), - treePatch, + flightSegmentPath, + // Create new tree based on the flightSegmentPath and router state patch + tree: newTree, canonicalUrlOverride, }) diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 98421fb1fed11..5697cef3dfcda 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -1126,12 +1126,21 @@ export async function renderToHTMLOrFlight( * Use router state to decide at what common layout to render the page. * This can either be the common layout between two pages or a specific place to start rendering from using the "refetch" marker in the tree. */ - const walkTreeWithFlightRouterState = async ( - loaderTreeToFilter: LoaderTree, - parentParams: { [key: string]: string | string[] }, - flightRouterState?: FlightRouterState, + const walkTreeWithFlightRouterState = async ({ + createSegmentPath, + loaderTreeToFilter, + parentParams, + isFirst, + flightRouterState, + parentRendered, + }: { + createSegmentPath: CreateSegmentPath + loaderTreeToFilter: LoaderTree + parentParams: { [key: string]: string | string[] } + isFirst: boolean + flightRouterState?: FlightRouterState parentRendered?: boolean - ): Promise => { + }): Promise => { const [segment, parallelRoutes] = loaderTreeToFilter const parallelRoutesKeys = Object.keys(parallelRoutes) @@ -1176,10 +1185,12 @@ export async function renderToHTMLOrFlight( await createComponentTree( // This ensures flightRouterPath is valid and filters down the tree { - createSegmentPath: (child) => child, + createSegmentPath: (child) => { + return createSegmentPath(child) + }, loaderTree: loaderTreeToFilter, parentParams: currentParams, - firstItem: true, + firstItem: isFirst, } ) ).Component @@ -1190,12 +1201,22 @@ export async function renderToHTMLOrFlight( // Walk through all parallel routes. for (const parallelRouteKey of parallelRoutesKeys) { const parallelRoute = parallelRoutes[parallelRouteKey] - const path = await walkTreeWithFlightRouterState( - parallelRoute, - currentParams, - flightRouterState && flightRouterState[1][parallelRouteKey], - parentRendered || renderComponentsOnThisLevel - ) + + const currentSegmentPath: FlightSegmentPath = isFirst + ? [parallelRouteKey] + : [actualSegment, parallelRouteKey] + + const path = await walkTreeWithFlightRouterState({ + createSegmentPath: (child) => { + return createSegmentPath([...currentSegmentPath, ...child]) + }, + loaderTreeToFilter: parallelRoute, + parentParams: currentParams, + flightRouterState: + flightRouterState && flightRouterState[1][parallelRouteKey], + parentRendered: parentRendered || renderComponentsOnThisLevel, + isFirst: false, + }) if (typeof path[path.length - 1] !== 'string') { return [actualSegment, parallelRouteKey, ...path] @@ -1210,11 +1231,13 @@ export async function renderToHTMLOrFlight( const flightData: FlightData = [ // TODO-APP: change walk to output without '' ( - await walkTreeWithFlightRouterState( - loaderTree, - {}, - providedFlightRouterState - ) + await walkTreeWithFlightRouterState({ + createSegmentPath: (child) => child, + loaderTreeToFilter: loaderTree, + parentParams: {}, + flightRouterState: providedFlightRouterState, + isFirst: true, + }) ).slice(1), ] diff --git a/test/e2e/app-dir/app/app/nested-navigation/CategoryNav.js b/test/e2e/app-dir/app/app/nested-navigation/CategoryNav.js new file mode 100644 index 0000000000000..718d82ec07ffd --- /dev/null +++ b/test/e2e/app-dir/app/app/nested-navigation/CategoryNav.js @@ -0,0 +1,28 @@ +'client' + +import { TabNavItem } from './TabNavItem' +import { useSelectedLayoutSegment } from 'next/dist/client/components/hooks-client' + +const CategoryNav = ({ categories }) => { + const selectedLayoutSegment = useSelectedLayoutSegment() + + return ( +
+ + Home + + + {categories.map((item) => ( + + {item.name} + + ))} +
+ ) +} + +export default CategoryNav diff --git a/test/e2e/app-dir/app/app/nested-navigation/TabNavItem.js b/test/e2e/app-dir/app/app/nested-navigation/TabNavItem.js new file mode 100644 index 0000000000000..3e1cbf3dd95dd --- /dev/null +++ b/test/e2e/app-dir/app/app/nested-navigation/TabNavItem.js @@ -0,0 +1,9 @@ +import Link from 'next/link' + +export const TabNavItem = ({ children, href }) => { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/SubCategoryNav.js b/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/SubCategoryNav.js new file mode 100644 index 0000000000000..4118c34c77fc4 --- /dev/null +++ b/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/SubCategoryNav.js @@ -0,0 +1,31 @@ +'client' + +import { TabNavItem } from '../TabNavItem' +import { useSelectedLayoutSegment } from 'next/dist/client/components/hooks-client' + +const SubCategoryNav = ({ category }) => { + const selectedLayoutSegment = useSelectedLayoutSegment() + + return ( +
+ + All + + + {category.items.map((item) => ( + + {item.name} + + ))} +
+ ) +} + +export default SubCategoryNav diff --git a/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/[subCategorySlug]/page.js b/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/[subCategorySlug]/page.js new file mode 100644 index 0000000000000..a43f15792c91c --- /dev/null +++ b/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/[subCategorySlug]/page.js @@ -0,0 +1,11 @@ +import { experimental_use as use } from 'react' +import { fetchSubCategory } from '../../getCategories' + +export default function Page({ params }) { + const category = use( + fetchSubCategory(params.categorySlug, params.subCategorySlug) + ) + if (!category) return null + + return

{category.name}

+} diff --git a/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/layout.js b/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/layout.js new file mode 100644 index 0000000000000..63bee78e8711e --- /dev/null +++ b/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/layout.js @@ -0,0 +1,14 @@ +import { experimental_use as use } from 'react' +import { fetchCategoryBySlug } from '../getCategories' +import SubCategoryNav from './SubCategoryNav' + +export default function Layout({ children, params }) { + const category = use(fetchCategoryBySlug(params.categorySlug)) + if (!category) return null + return ( + <> + + {children} + + ) +} diff --git a/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/page.js b/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/page.js new file mode 100644 index 0000000000000..b2e0a866c03fc --- /dev/null +++ b/test/e2e/app-dir/app/app/nested-navigation/[categorySlug]/page.js @@ -0,0 +1,9 @@ +import { experimental_use as use } from 'react' +import { fetchCategoryBySlug } from '../getCategories' + +export default function Page({ params }) { + const category = use(fetchCategoryBySlug(params.categorySlug)) + if (!category) return null + + return

All {category.name}

+} diff --git a/test/e2e/app-dir/app/app/nested-navigation/getCategories.js b/test/e2e/app-dir/app/app/nested-navigation/getCategories.js new file mode 100644 index 0000000000000..c5da031c72b3f --- /dev/null +++ b/test/e2e/app-dir/app/app/nested-navigation/getCategories.js @@ -0,0 +1,50 @@ +export const getCategories = () => [ + { + name: 'Electronics', + slug: 'electronics', + count: 11, + items: [ + { name: 'Phones', slug: 'phones', count: 4 }, + { name: 'Tablets', slug: 'tablets', count: 5 }, + { name: 'Laptops', slug: 'laptops', count: 2 }, + ], + }, + { + name: 'Clothing', + slug: 'clothing', + count: 12, + items: [ + { name: 'Tops', slug: 'tops', count: 3 }, + { name: 'Shorts', slug: 'shorts', count: 4 }, + { name: 'Shoes', slug: 'shoes', count: 5 }, + ], + }, + { + name: 'Books', + slug: 'books', + count: 10, + items: [ + { name: 'Fiction', slug: 'fiction', count: 5 }, + { name: 'Biography', slug: 'biography', count: 2 }, + { name: 'Education', slug: 'education', count: 3 }, + ], + }, +] + +export async function fetchCategoryBySlug(slug) { + // Assuming it always return expected categories + return getCategories().find((category) => category.slug === slug) +} + +export async function fetchCategories() { + return getCategories() +} + +async function findSubCategory(category, subCategorySlug) { + return category?.items.find((category) => category.slug === subCategorySlug) +} + +export async function fetchSubCategory(categorySlug, subCategorySlug) { + const category = await fetchCategoryBySlug(categorySlug) + return findSubCategory(category, subCategorySlug) +} diff --git a/test/e2e/app-dir/app/app/nested-navigation/layout.js b/test/e2e/app-dir/app/app/nested-navigation/layout.js new file mode 100644 index 0000000000000..43a2a79ec9165 --- /dev/null +++ b/test/e2e/app-dir/app/app/nested-navigation/layout.js @@ -0,0 +1,17 @@ +import { experimental_use as use } from 'react' +import { fetchCategories } from './getCategories' +import React from 'react' +import CategoryNav from './CategoryNav' + +export default function Layout({ children }) { + const categories = use(fetchCategories()) + return ( +
+
+ +
+ +
{children}
+
+ ) +} diff --git a/test/e2e/app-dir/app/app/nested-navigation/page.js b/test/e2e/app-dir/app/app/nested-navigation/page.js new file mode 100644 index 0000000000000..9833f2b17c471 --- /dev/null +++ b/test/e2e/app-dir/app/app/nested-navigation/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return

Home

+} diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 40ffd322f0c8b..631a255ecb8a7 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -432,7 +432,7 @@ describe('app dir', () => { }) // TODO-APP: Re-enable this test. - it.skip('should soft push', async () => { + it('should soft push', async () => { const browser = await webdriver(next.url, '/link-soft-push') try { @@ -1637,6 +1637,43 @@ describe('app dir', () => { }) }) }) + + describe('nested navigation', () => { + it('should navigate to nested pages', async () => { + const browser = await webdriver(next.url, '/nested-navigation') + expect(await browser.elementByCss('h1').text()).toBe('Home') + + const pages = [ + ['Electronics', ['Phones', 'Tablets', 'Laptops']], + ['Clothing', ['Tops', 'Shorts', 'Shoes']], + ['Books', ['Fiction', 'Biography', 'Education']], + ] as const + + for (const [category, subCategories] of pages) { + expect( + await browser + .elementByCss( + `a[href="/nested-navigation/${category.toLowerCase()}"]` + ) + .click() + .waitForElementByCss(`#all-${category.toLowerCase()}`) + .text() + ).toBe(`All ${category}`) + + for (const subcategory of subCategories) { + expect( + await browser + .elementByCss( + `a[href="/nested-navigation/${category.toLowerCase()}/${subcategory.toLowerCase()}"]` + ) + .click() + .waitForElementByCss(`#${subcategory.toLowerCase()}`) + .text() + ).toBe(`${subcategory}`) + } + } + }) + }) } runTests() From 45bed96714777c7f8eb908e57a580aeec7bfed45 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Wed, 5 Oct 2022 16:35:59 +0200 Subject: [PATCH 013/135] v12.3.2-canary.20 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-next/package.json | 2 +- packages/font/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 17 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lerna.json b/lerna.json index 46cfa723e8c83..6ba582ddfb3a7 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "12.3.2-canary.19" + "version": "12.3.2-canary.20" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 93ef5ea6f191e..7f64d7b97f41b 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 8400675704f7b..e0786abe08e04 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "description": "ESLint configuration used by NextJS.", "main": "index.js", "license": "MIT", @@ -9,7 +9,7 @@ "directory": "packages/eslint-config-next" }, "dependencies": { - "@next/eslint-plugin-next": "12.3.2-canary.19", + "@next/eslint-plugin-next": "12.3.2-canary.20", "@rushstack/eslint-patch": "^1.1.3", "@typescript-eslint/parser": "^5.21.0", "eslint-import-resolver-node": "^0.3.6", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index b02f8aca6c4cd..118343015e110 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "description": "ESLint plugin for NextJS.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index 3d454280cd30c..d88eb6339bffb 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 6eab5530b3c7c..6d5d65a6568a4 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index a15a6fc651029..87f1436b39305 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index f61809c387bdf..545189127b572 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index d35483549edea..9eb378982d1ca 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 7fc866627f30e..88c7e670b193d 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 4d2dec3f49fb0..4ff570f72a5ac 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 134927b996871..bdf3cc9edac5a 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 45a865a14d780..9f67d168f2a24 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "private": true, "scripts": { "build-native": "napi build --platform -p next-swc-napi --cargo-name next_swc_napi native --features plugin", diff --git a/packages/next/package.json b/packages/next/package.json index 00df6063df68d..ef3a25f06d93c 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -69,7 +69,7 @@ ] }, "dependencies": { - "@next/env": "12.3.2-canary.19", + "@next/env": "12.3.2-canary.20", "@swc/helpers": "0.4.11", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.14", @@ -120,11 +120,11 @@ "@hapi/accept": "5.0.2", "@napi-rs/cli": "2.7.0", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "12.3.2-canary.19", - "@next/polyfill-nomodule": "12.3.2-canary.19", - "@next/react-dev-overlay": "12.3.2-canary.19", - "@next/react-refresh-utils": "12.3.2-canary.19", - "@next/swc": "12.3.2-canary.19", + "@next/polyfill-module": "12.3.2-canary.20", + "@next/polyfill-nomodule": "12.3.2-canary.20", + "@next/react-dev-overlay": "12.3.2-canary.20", + "@next/react-refresh-utils": "12.3.2-canary.20", + "@next/swc": "12.3.2-canary.20", "@segment/ajv-human-errors": "2.1.2", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 9884cb4de844a..bb7a8e371de0b 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index be001c5fddb6c..97c50bbfc44bd 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "12.3.2-canary.19", + "version": "12.3.2-canary.20", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d95082e8b090..7cbc0c641e6d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -397,7 +397,7 @@ importers: packages/eslint-config-next: specifiers: - '@next/eslint-plugin-next': 12.3.2-canary.19 + '@next/eslint-plugin-next': 12.3.2-canary.20 '@rushstack/eslint-patch': ^1.1.3 '@typescript-eslint/parser': ^5.21.0 eslint-import-resolver-node: ^0.3.6 @@ -458,12 +458,12 @@ importers: '@hapi/accept': 5.0.2 '@napi-rs/cli': 2.7.0 '@napi-rs/triples': 1.1.0 - '@next/env': 12.3.2-canary.19 - '@next/polyfill-module': 12.3.2-canary.19 - '@next/polyfill-nomodule': 12.3.2-canary.19 - '@next/react-dev-overlay': 12.3.2-canary.19 - '@next/react-refresh-utils': 12.3.2-canary.19 - '@next/swc': 12.3.2-canary.19 + '@next/env': 12.3.2-canary.20 + '@next/polyfill-module': 12.3.2-canary.20 + '@next/polyfill-nomodule': 12.3.2-canary.20 + '@next/react-dev-overlay': 12.3.2-canary.20 + '@next/react-refresh-utils': 12.3.2-canary.20 + '@next/swc': 12.3.2-canary.20 '@segment/ajv-human-errors': 2.1.2 '@swc/helpers': 0.4.11 '@taskr/clear': 1.1.0 From d5837e03cc9a4377c534ea2ccabc54856b6a15f8 Mon Sep 17 00:00:00 2001 From: Ben Read Date: Wed, 5 Oct 2022 15:39:07 +0100 Subject: [PATCH 014/135] chore(examples): add webiny cms example (#41159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Documentation / Examples - [X] Make sure the linting passes by running `pnpm lint` - [X] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) Co-authored-by: Balázs Orbán --- examples/cms-webiny/.env.local.example | 4 + examples/cms-webiny/.gitignore | 35 ++++ examples/cms-webiny/README.md | 132 +++++++++++++ examples/cms-webiny/components/alert.tsx | 42 ++++ examples/cms-webiny/components/avatar.tsx | 15 ++ examples/cms-webiny/components/container.tsx | 3 + .../cms-webiny/components/cover-image.tsx | 44 +++++ .../cms-webiny/components/date-formatter.tsx | 10 + examples/cms-webiny/components/footer.tsx | 30 +++ examples/cms-webiny/components/header.tsx | 12 ++ examples/cms-webiny/components/hero-post.tsx | 43 ++++ examples/cms-webiny/components/intro.tsx | 28 +++ examples/cms-webiny/components/layout.tsx | 16 ++ .../components/markdown-styles.module.css | 18 ++ examples/cms-webiny/components/meta.tsx | 42 ++++ .../cms-webiny/components/more-stories.tsx | 24 +++ examples/cms-webiny/components/post-body.tsx | 12 ++ .../cms-webiny/components/post-header.tsx | 26 +++ .../cms-webiny/components/post-preview.tsx | 37 ++++ examples/cms-webiny/components/post-title.tsx | 7 + .../components/section-separator.tsx | 3 + examples/cms-webiny/lib/api.ts | 120 ++++++++++++ examples/cms-webiny/lib/constants.ts | 5 + .../cms-webiny/lib/rich-text-renderer.tsx | 185 ++++++++++++++++++ examples/cms-webiny/next.config.js | 5 + examples/cms-webiny/package.json | 23 +++ examples/cms-webiny/pages/_app.tsx | 5 + examples/cms-webiny/pages/api/exit-preview.ts | 8 + examples/cms-webiny/pages/api/preview.ts | 25 +++ examples/cms-webiny/pages/index.tsx | 44 +++++ examples/cms-webiny/pages/posts/[slug].tsx | 81 ++++++++ examples/cms-webiny/postcss.config.js | 8 + .../favicons/android-chrome-192x192.png | Bin 0 -> 4795 bytes .../favicons/android-chrome-512x512.png | Bin 0 -> 14640 bytes .../public/favicons/apple-touch-icon.png | Bin 0 -> 1327 bytes .../public/favicons/browserconfig.xml | 9 + .../public/favicons/favicon-16x16.png | Bin 0 -> 595 bytes .../public/favicons/favicon-32x32.png | Bin 0 -> 880 bytes .../cms-webiny/public/favicons/favicon.ico | Bin 0 -> 15086 bytes .../public/favicons/mstile-150x150.png | Bin 0 -> 3567 bytes .../public/favicons/safari-pinned-tab.svg | 33 ++++ .../public/favicons/site.webmanifest | 19 ++ examples/cms-webiny/styles/globals.css | 3 + examples/cms-webiny/tailwind.config.js | 37 ++++ examples/cms-webiny/tsconfig.json | 20 ++ 45 files changed, 1213 insertions(+) create mode 100644 examples/cms-webiny/.env.local.example create mode 100644 examples/cms-webiny/.gitignore create mode 100644 examples/cms-webiny/README.md create mode 100644 examples/cms-webiny/components/alert.tsx create mode 100644 examples/cms-webiny/components/avatar.tsx create mode 100644 examples/cms-webiny/components/container.tsx create mode 100644 examples/cms-webiny/components/cover-image.tsx create mode 100644 examples/cms-webiny/components/date-formatter.tsx create mode 100644 examples/cms-webiny/components/footer.tsx create mode 100644 examples/cms-webiny/components/header.tsx create mode 100644 examples/cms-webiny/components/hero-post.tsx create mode 100644 examples/cms-webiny/components/intro.tsx create mode 100644 examples/cms-webiny/components/layout.tsx create mode 100644 examples/cms-webiny/components/markdown-styles.module.css create mode 100644 examples/cms-webiny/components/meta.tsx create mode 100644 examples/cms-webiny/components/more-stories.tsx create mode 100644 examples/cms-webiny/components/post-body.tsx create mode 100644 examples/cms-webiny/components/post-header.tsx create mode 100644 examples/cms-webiny/components/post-preview.tsx create mode 100644 examples/cms-webiny/components/post-title.tsx create mode 100644 examples/cms-webiny/components/section-separator.tsx create mode 100644 examples/cms-webiny/lib/api.ts create mode 100644 examples/cms-webiny/lib/constants.ts create mode 100644 examples/cms-webiny/lib/rich-text-renderer.tsx create mode 100644 examples/cms-webiny/next.config.js create mode 100644 examples/cms-webiny/package.json create mode 100644 examples/cms-webiny/pages/_app.tsx create mode 100644 examples/cms-webiny/pages/api/exit-preview.ts create mode 100644 examples/cms-webiny/pages/api/preview.ts create mode 100644 examples/cms-webiny/pages/index.tsx create mode 100644 examples/cms-webiny/pages/posts/[slug].tsx create mode 100644 examples/cms-webiny/postcss.config.js create mode 100644 examples/cms-webiny/public/favicons/android-chrome-192x192.png create mode 100644 examples/cms-webiny/public/favicons/android-chrome-512x512.png create mode 100644 examples/cms-webiny/public/favicons/apple-touch-icon.png create mode 100644 examples/cms-webiny/public/favicons/browserconfig.xml create mode 100644 examples/cms-webiny/public/favicons/favicon-16x16.png create mode 100644 examples/cms-webiny/public/favicons/favicon-32x32.png create mode 100644 examples/cms-webiny/public/favicons/favicon.ico create mode 100644 examples/cms-webiny/public/favicons/mstile-150x150.png create mode 100644 examples/cms-webiny/public/favicons/safari-pinned-tab.svg create mode 100644 examples/cms-webiny/public/favicons/site.webmanifest create mode 100644 examples/cms-webiny/styles/globals.css create mode 100644 examples/cms-webiny/tailwind.config.js create mode 100644 examples/cms-webiny/tsconfig.json diff --git a/examples/cms-webiny/.env.local.example b/examples/cms-webiny/.env.local.example new file mode 100644 index 0000000000000..414d0d3c6b55b --- /dev/null +++ b/examples/cms-webiny/.env.local.example @@ -0,0 +1,4 @@ +PREVIEW_API_SECRET= +WEBINY_API_SECRET= +NEXT_PUBLIC_WEBINY_API_URL= +NEXT_PUBLIC_WEBINY_PREVIEW_API_URL= \ No newline at end of file diff --git a/examples/cms-webiny/.gitignore b/examples/cms-webiny/.gitignore new file mode 100644 index 0000000000000..8f322f0d8f495 --- /dev/null +++ b/examples/cms-webiny/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/cms-webiny/README.md b/examples/cms-webiny/README.md new file mode 100644 index 0000000000000..2d17ed015941f --- /dev/null +++ b/examples/cms-webiny/README.md @@ -0,0 +1,132 @@ +# A statically generated blog example using Next.js and Webiny + +This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [Webiny](https://webiny.com/) as the data source. + +## Demo + +[https://webiny-headlesscms-nextjs-example.vercel.app/](https://webiny-headlesscms-nextjs-example.vercel.app/) + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-webiny&project-name=cms-webiny&repository-name=cms-webiny&env=PREVIEW_API_SECRET,WEBINY_API_SECRET,NEXT_PUBLIC_WEBINY_API_URL,NEXT_PUBLIC_WEBINY_PREVIEW_API_URL&envDescription=Required%20to%20connect%20the%20app%20with%20Webiny&envLink=https://vercel.link/cms-webiny-env) + +### Related examples + +- [WordPress](/examples/cms-wordpress) +- [DatoCMS](/examples/cms-datocms) +- [Sanity](/examples/cms-sanity) +- [TakeShape](/examples/cms-takeshape) +- [Prismic](/examples/cms-prismic) +- [Contentful](/examples/cms-contentful) +- [Strapi](/examples/cms-strapi) +- [Agility CMS](/examples/cms-agilitycms) +- [Cosmic](/examples/cms-cosmic) +- [ButterCMS](/examples/cms-buttercms) +- [Storyblok](/examples/cms-storyblok) +- [GraphCMS](/examples/cms-graphcms) +- [Kontent](/examples/cms-kontent) +- [Umbraco Heartcore](/examples/cms-umbraco-heartcore) +- [Builder.io](/examples/cms-builder-io) + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: + +```bash +npx create-next-app --example cms-webiny cms-webiny-app +``` + +```bash +yarn create next-app --example cms-webiny cms-webiny-app +``` + +```bash +pnpm create next-app --example cms-webiny cms-webiny-app +``` + +### Step 1. Set up a Webiny project + +Follow the [Webiny docs](https://www.webiny.com/docs/tutorials/install-webiny) to install a Webiny project on your cloud hosting provider. Because Webiny is a distributed system we don't run it locally. This also means you don't need to worry about setting up Docker, or installing databases and drivers on your local machine for Postgres, MongoDB or similar. The cloud takes care of that for you. + +If you get stuck or have any questions, please [join the community](http://webiny-community.slack.com 'Webiny slack channel') and reach out for some help. + +Once you have an app up and running click into the "HeadlessCMS" app in the sidebar, click on _models_ and add the following models and fields: + +#### Authors + +- A `text` field with the value "name" +- A `text` field with the value "slug" (optionally add a validator using this regex which will make sure you have valid urls: `^(?!.*--)[a-z0-9\-]+$`) +- a `files` field with the value "picture" + +#### Posts + +- A `text` field with the value "title" +- A `text` field with the value "slug" (optionally use the regex above as a validator) +- A `files` field with the value "featured image" +- A `rich text` field with the value "body" +- A `reference` field with the value "Author" + +Next, choose **API Keys** in the sidebar. Add an API key with any name and description. Select "Headless CMS" and choose a Custom access level for all content model groups with the values `read` and `preview`. Save the API token and the token itself will be revealed. + +You will be able to use the same API token for both published and draft posts. + +### Step 2. Set up environment variables + +Copy the `.env.local.example` file to `.env.local`, then set the variables as follows: + +- `PREVIEW_API_SECRET` can be any random string (but avoid spaces), like `MY_SECRET` - this is used for [Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode). +- WEBINY_API_SECRET this will be your security token generated in Webiny +- You can find the values for `NEXT_PUBLIC_WEBINY_API_URL` and `NEXT_PUBLIC_WEBINY_PREVIEW_API_URL` two ways: From your local Webiny project root, run `yarn webiny info`, alternatively go to **API Playground** in the sidebar. At the top of the GraphQL explorer are four tabs, one for each of our APIs, and you'll see both the Read API and the Preview API on those tabs. The URL for your environment is just below the tab. ([More info here if you get stuck](https://www.webiny.com/docs/headless-cms/basics/graphql-api)) + +### Step 3. Run Next.js in development mode + +Inside the Next.js app directory, run: + +```bash +npm install +npm run dev + +# or + +yarn install +yarn dev +``` + +Your blog should be up and running on [http://localhost:3000](http://localhost:3000)! + +The best place to debug is inside the `fetchAPI` function in `lib/api.js`. If you need help, you can post on [GitHub discussions](https://github.com/vercel/next.js/discussions). + +### Step 4. Try preview mode + +If you go to the `/posts/draft` page on localhost, you won't see this post because it’s not published. However, if you use the **Preview Mode**, you'll be able to see the change ([Documentation](https://nextjs.org/docs/advanced-features/preview-mode)). + +To enable the Preview Mode, go to this URL: + +``` +http://localhost:3000/api/preview?secret=&slug=draft +``` + +- `` should be the string you entered for `PREVIEW_API_SECRET`. +- `` should be the post's `slug` attribute. + +You should now be able to see the draft post. To exit the preview mode, you can click **Click here to exit preview mode** at the top. + +To add more preview pages, create a post and set the **status** as `draft`. + +### Step 5. Deploy on Vercel + +You can deploy this app to the cloud with [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). + +#### Deploy Your Local Project + +To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example). + +**Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file. + +#### Deploy from Our Template + +Alternatively, you can deploy using our template by clicking on the Deploy button below. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-webiny&project-name=cms-webiny&repository-name=cms-webiny&env=PREVIEW_API_SECRET,WEBINY_API_SECRET,NEXT_PUBLIC_WEBINY_API_URL,NEXT_PUBLIC_WEBINY_PREVIEW_API_URL&envDescription=Required%20to%20connect%20the%20app%20with%20Webiny&envLink=https://vercel.link/cms-webiny-env) diff --git a/examples/cms-webiny/components/alert.tsx b/examples/cms-webiny/components/alert.tsx new file mode 100644 index 0000000000000..018eae66ad5aa --- /dev/null +++ b/examples/cms-webiny/components/alert.tsx @@ -0,0 +1,42 @@ +import Container from './container' +import cn from 'classnames' +import { EXAMPLE_PATH } from '../lib/constants' + +export default function Alert({ preview }) { + return ( +
+ +
+ {preview ? ( + <> + This page is a preview.{' '} + + Click here + {' '} + to exit preview mode. + + ) : ( + <> + The source code for this blog is{' '} + + available on GitHub + + . + + )} +
+
+
+ ) +} diff --git a/examples/cms-webiny/components/avatar.tsx b/examples/cms-webiny/components/avatar.tsx new file mode 100644 index 0000000000000..228e60143747f --- /dev/null +++ b/examples/cms-webiny/components/avatar.tsx @@ -0,0 +1,15 @@ +import Image from 'next/image' +export default function Avatar({ name, picture }) { + return ( +
+ {name} +
{name}
+
+ ) +} diff --git a/examples/cms-webiny/components/container.tsx b/examples/cms-webiny/components/container.tsx new file mode 100644 index 0000000000000..c3ed24c0515ba --- /dev/null +++ b/examples/cms-webiny/components/container.tsx @@ -0,0 +1,3 @@ +export default function Container({ children }) { + return
{children}
+} diff --git a/examples/cms-webiny/components/cover-image.tsx b/examples/cms-webiny/components/cover-image.tsx new file mode 100644 index 0000000000000..f99220e78a396 --- /dev/null +++ b/examples/cms-webiny/components/cover-image.tsx @@ -0,0 +1,44 @@ +import cn from 'classnames' +import Link from 'next/link' +import Image from 'next/image' + +export type TCoverImage = { + title: string + src: string + slug?: string + height: number + width: number +} + +const CoverImage: React.FC = ({ + title, + src, + slug, + height, + width, +}) => { + const image = ( + {`Cover + ) + return ( +
+ {slug ? ( + + {image} + + ) : ( + image + )} +
+ ) +} +export default CoverImage diff --git a/examples/cms-webiny/components/date-formatter.tsx b/examples/cms-webiny/components/date-formatter.tsx new file mode 100644 index 0000000000000..f91f2fbcb371f --- /dev/null +++ b/examples/cms-webiny/components/date-formatter.tsx @@ -0,0 +1,10 @@ +import { parseISO, format } from 'date-fns' + +export default function DateFormatter({ dateString }) { + if (!dateString) { + return null + } + const date = parseISO(dateString) + const formattedDate = format(date, 'LLLL d, yyyy') + return +} diff --git a/examples/cms-webiny/components/footer.tsx b/examples/cms-webiny/components/footer.tsx new file mode 100644 index 0000000000000..ca1e8bac51eff --- /dev/null +++ b/examples/cms-webiny/components/footer.tsx @@ -0,0 +1,30 @@ +import Container from './container' +import { EXAMPLE_PATH } from '../lib/constants' + +export default function Footer() { + return ( + + ) +} diff --git a/examples/cms-webiny/components/header.tsx b/examples/cms-webiny/components/header.tsx new file mode 100644 index 0000000000000..eb9c8e1bcf652 --- /dev/null +++ b/examples/cms-webiny/components/header.tsx @@ -0,0 +1,12 @@ +import Link from 'next/link' + +export default function Header() { + return ( +

+ + Blog + + . +

+ ) +} diff --git a/examples/cms-webiny/components/hero-post.tsx b/examples/cms-webiny/components/hero-post.tsx new file mode 100644 index 0000000000000..b6905a9fd122f --- /dev/null +++ b/examples/cms-webiny/components/hero-post.tsx @@ -0,0 +1,43 @@ +import Avatar from '../components/avatar' +import DateFormatter from '../components/date-formatter' +import CoverImage from '../components/cover-image' +import Link from 'next/link' + +export default function HeroPost({ + title, + coverImage, + createdOn, + excerpt, + author, + slug, +}) { + return ( +
+
+ +
+
+
+

+ + {title} + +

+
+ +
+
+
+

{excerpt}

+ +
+
+
+ ) +} diff --git a/examples/cms-webiny/components/intro.tsx b/examples/cms-webiny/components/intro.tsx new file mode 100644 index 0000000000000..01759fd8fad03 --- /dev/null +++ b/examples/cms-webiny/components/intro.tsx @@ -0,0 +1,28 @@ +import { CMS_NAME, CMS_URL } from '../lib/constants' + +export default function Intro() { + return ( +
+

+ Blog. +

+

+ A statically generated blog example using{' '} + + Next.js + {' '} + and{' '} + + {CMS_NAME} + + . +

+
+ ) +} diff --git a/examples/cms-webiny/components/layout.tsx b/examples/cms-webiny/components/layout.tsx new file mode 100644 index 0000000000000..99d95353131e0 --- /dev/null +++ b/examples/cms-webiny/components/layout.tsx @@ -0,0 +1,16 @@ +import Alert from '../components/alert' +import Footer from '../components/footer' +import Meta from '../components/meta' + +export default function Layout({ preview, children }) { + return ( + <> + +
+ +
{children}
+
+