From 06a81414acfd54e41be57ee9749c9119f2c2fb53 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Sat, 10 May 2025 05:51:05 +0800 Subject: [PATCH 01/10] chore: update rspack to 1.3.9 (#78984) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit let Rspack to pass the test cases it was previously able to pass. 1. update Rspack to 1.3.9 2. Moves `moduleSpansByCompilation` setting in `RspackProfilingPlugin` from the `compilation` hook to `thisCompilation`. `TraceEntryPointsPlugin` relies on `moduleSpansByCompilation` during its `thisCompilation` hook. Since the `compilation` hook triggers before `thisCompilation`, initializing the map earlier leads to: ``` Error: × TypeError: Cannot read properties of undefined (reading 'traceChild') ``` relate: https://github.com/vercel/next.js/pull/78011 3. The test case `test/integration/middleware-prefetch/tests/index.test.js` was failing due to an unreliable asset name matching strategy. Asset names follow the pattern: `static/chunks/pages/[pagename]-[hash].js`. Test was checking for `ssg-page-2-[hash].js` by looking for names starting with `ssg-page-2`. This could produce false positives when `ssg-page-[hash].js` happens to have a hash starting with `2`. 4. test dev `test/e2e/app-dir/dynamic-data/dynamic-data.test.ts` Test fails due to Rspack's lack of support for compilation.codeGenerationResults JS API. 5. test dev `test/e2e/app-dir/dynamic-href/dynamic-href.test.ts` Same compatibility issue as above. 6. test dev `test/development/acceptance-app/hydration-error.test.ts` Same compatibility issue as above. 6. test dev `test/development/app-dir/ssr-in-rsc/ssr-in-rsc.test.ts` Test fails due to differing error message formats between implementations. --- package.json | 2 +- packages/next-rspack/package.json | 2 +- .../plugins/rspack-profiling-plugin.ts | 2 +- packages/next/src/compiled/sass-loader/cjs.js | 2 +- pnpm-lock.yaml | 180 +++++++++--------- .../middleware-prefetch/tests/index.test.js | 8 +- 6 files changed, 100 insertions(+), 96 deletions(-) diff --git a/package.json b/package.json index fe4c11a042d4..064b5f0fb69a 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "@next/third-parties": "workspace:*", "@opentelemetry/api": "1.4.1", "@picocss/pico": "1.5.10", - "@rspack/core": "1.3.8", + "@rspack/core": "1.3.9", "@rspack/plugin-react-refresh": "1.2.0", "@swc/cli": "0.1.55", "@swc/core": "1.11.24", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index e70cf7422e17..e402f7687642 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -7,7 +7,7 @@ }, "types": "index.d.ts", "dependencies": { - "@rspack/core": "1.3.8", + "@rspack/core": "1.3.9", "@rspack/plugin-react-refresh": "1.2.0", "react-refresh": "0.12.0" } diff --git a/packages/next/src/build/webpack/plugins/rspack-profiling-plugin.ts b/packages/next/src/build/webpack/plugins/rspack-profiling-plugin.ts index 7a19505814d6..49008e9660a5 100644 --- a/packages/next/src/build/webpack/plugins/rspack-profiling-plugin.ts +++ b/packages/next/src/build/webpack/plugins/rspack-profiling-plugin.ts @@ -16,7 +16,7 @@ export class RspackProfilingPlugin { } apply(compiler: any) { - compiler.hooks.compilation.tap( + compiler.hooks.thisCompilation.tap( { name: pluginName, stage: -Infinity }, (compilation: any) => { const rspack = getRspackCore() diff --git a/packages/next/src/compiled/sass-loader/cjs.js b/packages/next/src/compiled/sass-loader/cjs.js index 898fb42803a7..66cb6ee2f0c3 100644 --- a/packages/next/src/compiled/sass-loader/cjs.js +++ b/packages/next/src/compiled/sass-loader/cjs.js @@ -1 +1 @@ -(function(){"use strict";var __webpack_modules__={906:function(e,t,s){const n=s(909);e.exports=n.default},909:function(e,t,s){Object.defineProperty(t,"__esModule",{value:true});t["default"]=void 0;var n=_interopRequireDefault(s(310));var o=_interopRequireDefault(s(17));var r=_interopRequireDefault(s(970));var a=s(799);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}async function loader(e){const t=this.getOptions(r.default);const s=this.async();let i;try{i=(0,a.getSassImplementation)(this,t.implementation)}catch(e){s(e);return}const c=typeof t.sourceMap==="boolean"?t.sourceMap:this.sourceMap;const l=await(0,a.getSassOptions)(this,t,e,i,c);const p=typeof t.webpackImporter==="boolean"?t.webpackImporter:true;if(p){const e=t.api==="modern"||t.api==="modern-compiler";if(!e){const{includePaths:e}=l;l.importer.push((0,a.getWebpackImporter)(this,i,e))}else{l.importers.push((0,a.getModernWebpackImporter)(this,i,[]))}}let u;try{u=(0,a.getCompileFn)(this,i,t)}catch(e){s(e);return}let d;try{d=await u(l)}catch(e){if(e.span&&typeof e.span.url!=="undefined"){this.addDependency(n.default.fileURLToPath(e.span.url))}else if(typeof e.file!=="undefined"){this.addDependency(o.default.normalize(e.file))}s((0,a.errorFactory)(e));return}let f=d.sourceMap?d.sourceMap:d.map?JSON.parse(d.map):null;if(f&&c){f=(0,a.normalizeSourceMap)(f,this.rootContext)}if(typeof d.loadedUrls!=="undefined"){d.loadedUrls.filter((e=>e.protocol==="file:")).forEach((e=>{const t=n.default.fileURLToPath(e);if(o.default.isAbsolute(t)){this.addDependency(t)}}))}else if(typeof d.stats!=="undefined"&&typeof d.stats.includedFiles!=="undefined"){d.stats.includedFiles.forEach((e=>{const t=o.default.normalize(e);if(o.default.isAbsolute(t)){this.addDependency(t)}}))}s(null,d.css.toString(),f)}var i=t["default"]=loader},799:function(__unused_webpack_module,exports,__nccwpck_require__){Object.defineProperty(exports,"__esModule",{value:true});exports.errorFactory=errorFactory;exports.getCompileFn=getCompileFn;exports.getModernWebpackImporter=getModernWebpackImporter;exports.getSassImplementation=getSassImplementation;exports.getSassOptions=getSassOptions;exports.getWebpackImporter=getWebpackImporter;exports.getWebpackResolver=getWebpackResolver;exports.normalizeSourceMap=normalizeSourceMap;var _url=_interopRequireDefault(__nccwpck_require__(310));var _path=_interopRequireDefault(__nccwpck_require__(17));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}function getDefaultSassImplementation(){let sassImplPkg="sass";try{require.resolve("sass-embedded");sassImplPkg="sass-embedded"}catch(ignoreError){try{eval("require").resolve("sass")}catch(_ignoreError){try{eval("require").resolve("node-sass");sassImplPkg="node-sass"}catch(e){sassImplPkg="sass"}}}return __nccwpck_require__(438)}function getSassImplementation(e,t){let s=t;if(!s){s=getDefaultSassImplementation()}if(typeof s==="string"){s=require(s)}const{info:n}=s;if(!n){throw new Error("Unknown Sass implementation.")}const o=n.split("\t");if(o.length<2){throw new Error(`Unknown Sass implementation "${n}".`)}const[r]=o;if(r==="dart-sass"){return s}else if(r==="node-sass"){return s}else if(r==="sass-embedded"){return s}throw new Error(`Unknown Sass implementation "${r}".`)}function isProductionLikeMode(e){return e.mode==="production"||!e.mode}function proxyCustomImporters(e,t){return[].concat(e).map((e=>function proxyImporter(...s){const n={...this,webpackLoaderContext:t};return e.apply(n,s)}))}async function getSassOptions(e,t,s,n,o){const r=t.sassOptions?typeof t.sassOptions==="function"?t.sassOptions(e)||{}:t.sassOptions:{};const a={...r,data:t.additionalData?typeof t.additionalData==="function"?await t.additionalData(s,e):`${t.additionalData}\n${s}`:s};if(!a.logger){const s=t.warnRuleAsWarning!==false;const n=e.getLogger("sass-loader");const formatSpan=e=>`Warning on line ${e.start.line}, column ${e.start.column} of ${e.url||"-"}:${e.start.line}:${e.start.column}:\n`;const formatDebugSpan=e=>`[debug:${e.start.line}:${e.start.column}] `;a.logger={debug(e,t){let s="";if(t.span){s=formatDebugSpan(t.span)}s+=e;n.debug(s)},warn(t,o){let r="";if(o.deprecation){r+="Deprecation "}if(o.span){r+=formatSpan(o.span)}r+=t;if(o.span&&o.span.context){r+=`\n\n${o.span.start.line} | ${o.span.context}`}if(o.stack&&o.stack!=="null"){r+=`\n\n${o.stack}`}if(s){const t=new Error(r);t.name="SassWarning";t.stack=null;e.emitWarning(t)}else{n.warn(r)}}}}const i=t.api==="modern"||t.api==="modern-compiler";const{resourcePath:c}=e;if(i){a.url=_url.default.pathToFileURL(c);if(!a.style&&isProductionLikeMode(e)){a.style="compressed"}if(o){a.sourceMap=true}if(typeof a.syntax==="undefined"){const e=_path.default.extname(c);if(e&&e.toLowerCase()===".scss"){a.syntax="scss"}else if(e&&e.toLowerCase()===".sass"){a.syntax="indented"}else if(e&&e.toLowerCase()===".css"){a.syntax="css"}}a.loadPaths=[].concat((a.loadPaths?a.loadPaths.slice():[]).map((e=>_path.default.isAbsolute(e)?e:_path.default.join(process.cwd(),e)))).concat(process.env.SASS_PATH?process.env.SASS_PATH.split(process.platform==="win32"?";":":"):[]);a.importers=a.importers?Array.isArray(a.importers)?a.importers.slice():[a.importers]:[]}else{a.file=c;if(!a.outputStyle&&isProductionLikeMode(e)){a.outputStyle="compressed"}if(o){a.sourceMap=true;a.outFile=_path.default.join(e.rootContext,"style.css.map");a.sourceMapContents=true;a.omitSourceMapUrl=true;a.sourceMapEmbed=false}const s=_path.default.extname(c);if(s&&s.toLowerCase()===".sass"&&typeof a.indentedSyntax==="undefined"){a.indentedSyntax=true}else{a.indentedSyntax=Boolean(a.indentedSyntax)}a.importer=a.importer?proxyCustomImporters(Array.isArray(a.importer)?a.importer.slice():[a.importer],e):[];if(t.webpackImporter===false&&a.importer.length===0){a.importer=undefined}a.includePaths=[].concat(process.cwd()).concat((a.includePaths?a.includePaths.slice():[]).map((e=>_path.default.isAbsolute(e)?e:_path.default.join(process.cwd(),e)))).concat(process.env.SASS_PATH?process.env.SASS_PATH.split(process.platform==="win32"?";":":"):[]);if(typeof a.charset==="undefined"){a.charset=true}}return a}const MODULE_REQUEST_REGEX=/^[^?]*~/;const IS_MODULE_IMPORT=/^~([^/]+|[^/]+\/|@[^/]+[/][^/]+|@[^/]+\/?|@[^/]+[/][^/]+\/)$/;const IS_PKG_SCHEME=/^pkg:/i;function getPossibleRequests(e,t=false,s=false){let n=e;if(t){if(MODULE_REQUEST_REGEX.test(e)){n=n.replace(MODULE_REQUEST_REGEX,"")}if(IS_PKG_SCHEME.test(e)){n=`${n.slice(4)}`;return[...new Set([n,e])]}if(IS_MODULE_IMPORT.test(e)||IS_PKG_SCHEME.test(e)){n=n[n.length-1]==="/"?n:`${n}/`;return[...new Set([n,e])]}}const o=_path.default.extname(n).toLowerCase();if(o===".css"){return[]}const r=_path.default.dirname(n);const a=r==="."?"":`${r}/`;const i=_path.default.basename(n);const c=_path.default.basename(n,o);return[...new Set([].concat(s?[`${a}_${c}.import${o}`,`${a}${c}.import${o}`]:[]).concat([`${a}_${i}`,`${a}${i}`]).concat(t?[e]:[]))]}function promiseResolve(e){return(t,s)=>new Promise(((n,o)=>{e(t,s,((e,t)=>{if(e){o(e)}else{n(t)}}))}))}async function startResolving(e){if(e.length===0){return Promise.reject()}const[{possibleRequests:t}]=e;if(t.length===0){return Promise.reject()}const[{resolve:s,context:n}]=e;try{return await s(n,t[0])}catch(s){const[,...n]=t;if(n.length===0){const[,...t]=e;return startResolving(t)}e[0].possibleRequests=n;return startResolving(e)}}const IS_SPECIAL_MODULE_IMPORT=/^~[^/]+$/;const IS_NATIVE_WIN32_PATH=/^[a-z]:[/\\]|^\\\\/i;function getWebpackResolver(e,t,s=[]){const n=t&&(t.info.includes("dart-sass")||t.info.includes("sass-embedded"));const o=promiseResolve(e({alias:[],aliasFields:[],conditionNames:[],descriptionFiles:[],extensions:[".sass",".scss",".css"],exportsFields:[],mainFields:[],mainFiles:["_index","index"],modules:[],restrictions:[/\.((sa|sc|c)ss)$/i],preferRelative:true}));const r=promiseResolve(e({alias:[],aliasFields:[],conditionNames:[],descriptionFiles:[],extensions:[".sass",".scss",".css"],exportsFields:[],mainFields:[],mainFiles:["_index.import","_index","index.import","index"],modules:[],restrictions:[/\.((sa|sc|c)ss)$/i],preferRelative:true}));const a=promiseResolve(e({dependencyType:"sass",conditionNames:["sass","style","..."],mainFields:["sass","style","main","..."],mainFiles:["_index","index","..."],extensions:[".sass",".scss",".css"],restrictions:[/\.((sa|sc|c)ss)$/i],preferRelative:true}));const i=promiseResolve(e({dependencyType:"sass",conditionNames:["sass","style","..."],mainFields:["sass","style","main","..."],mainFiles:["_index.import","_index","index.import","index","..."],extensions:[".sass",".scss",".css"],restrictions:[/\.((sa|sc|c)ss)$/i],preferRelative:true}));return(e,t,c)=>{if(!n&&!_path.default.isAbsolute(e)){return Promise.reject()}const l=t;const p=l.slice(0,5).toLowerCase()==="file:";if(p){try{t=_url.default.fileURLToPath(l)}catch(e){t=t.slice(7)}}let u=[];const d=!IS_SPECIAL_MODULE_IMPORT.test(t)&&!IS_PKG_SCHEME.test(t)&&!p&&!l.startsWith("/")&&!IS_NATIVE_WIN32_PATH.test(l);if(s.length>0&&d){const a=getPossibleRequests(t,false,c);if(!n){u=u.concat({resolve:c?r:o,context:_path.default.dirname(e),possibleRequests:a})}u=u.concat(s.map((e=>({resolve:c?r:o,context:e,possibleRequests:a}))))}const f=getPossibleRequests(t,true,c);u=u.concat({resolve:c?i:a,context:_path.default.dirname(e),possibleRequests:f});return startResolving(u)}}const MATCH_CSS=/\.css$/i;function getModernWebpackImporter(e,t,s){const n=getWebpackResolver(e.getResolve,t,s);return{async canonicalize(t,s){const{fromImport:o}=s;const r=s.containingUrl?_url.default.fileURLToPath(s.containingUrl.toString()):e.resourcePath;let a;try{a=await n(r,t,o)}catch(e){return null}e.addDependency(_path.default.normalize(a));return _url.default.pathToFileURL(a)},async load(t){const s=_path.default.extname(t.pathname);let n;if(s&&s.toLowerCase()===".scss"){n="scss"}else if(s&&s.toLowerCase()===".sass"){n="indented"}else if(s&&s.toLowerCase()===".css"){n="css"}else{n="scss"}try{const s=await new Promise(((s,n)=>{const o=_url.default.fileURLToPath(t);e.fs.readFile(o,"utf8",((e,t)=>{if(e){n(e);return}s(t)}))}));return{contents:s,syntax:n}}catch(e){return null}}}}function getWebpackImporter(e,t,s){const n=getWebpackResolver(e.getResolve,t,s);return function importer(t,s,o){const{fromImport:r}=this;n(s,t,r).then((t=>{e.addDependency(_path.default.normalize(t));o({file:t.replace(MATCH_CSS,"")})})).catch((()=>{o({file:t})}))}}let nodeSassJobQueue=null;const sassModernCompilers=new WeakMap;function getCompileFn(e,t,s){const n=t.info.includes("dart-sass")||t.info.includes("sass-embedded");if(n){if(s.api==="modern"){return e=>{const{data:s,...n}=e;return t.compileStringAsync(s,n)}}if(s.api==="modern-compiler"){return async s=>{const n=e._compiler;const{data:o,...r}=s;if(n){if(!sassModernCompilers.has(n)){const e=await t.initAsyncCompiler();if(!sassModernCompilers.has(n)){sassModernCompilers.set(n,e);n.hooks.shutdown.tap("sass-loader",(()=>{e.dispose()}))}}return sassModernCompilers.get(n).compileStringAsync(o,r)}return t.compileStringAsync(o,r)}}return e=>new Promise(((s,n)=>{t.render(e,((e,t)=>{if(e){n(e);return}s(t)}))}))}if(s.api==="modern"||s.api==="modern-compiler"){throw new Error("Modern API is not supported for 'node-sass'")}if(nodeSassJobQueue===null){const e=Number(process.env.UV_THREADPOOL_SIZE||4);const s=__nccwpck_require__(175);nodeSassJobQueue=s.queue(t.render.bind(t),e-1)}return e=>new Promise(((t,s)=>{nodeSassJobQueue.push.bind(nodeSassJobQueue)(e,((e,n)=>{if(e){s(e);return}t(n)}))}))}const ABSOLUTE_SCHEME=/^[A-Za-z0-9+\-.]+:/;function getURLType(e){if(e[0]==="/"){if(e[1]==="/"){return"scheme-relative"}return"path-absolute"}if(IS_NATIVE_WIN32_PATH.test(e)){return"path-absolute"}return ABSOLUTE_SCHEME.test(e)?"absolute":"path-relative"}function normalizeSourceMap(e,t){const s=e;if(typeof s.file!=="undefined"){delete s.file}s.sourceRoot="";s.sources=s.sources.map((e=>{const s=getURLType(e);if(s==="absolute"&&/^file:/i.test(e)){return _url.default.fileURLToPath(e)}else if(s==="path-relative"){return _path.default.resolve(t,_path.default.normalize(e))}return e}));return s}function errorFactory(e){let t;if(e.formatted){t=e.formatted.replace(/^Error: /,"")}else{({message:t}=e)}const s=new Error(t,{cause:e});s.stack=null;return s}},175:function(e){e.exports=require("next/dist/compiled/neo-async")},17:function(e){e.exports=require("path")},438:function(e){e.exports=require("sass")},310:function(e){e.exports=require("url")},970:function(e){e.exports=JSON.parse('{"title":"Sass Loader options","type":"object","properties":{"implementation":{"description":"The implementation of the sass to be used.","link":"https://github.com/webpack-contrib/sass-loader#implementation","anyOf":[{"type":"string"},{"type":"object"}]},"api":{"description":"Switch between old and modern API for `sass` (`Dart Sass`) and `Sass Embedded` implementations.","link":"https://github.com/webpack-contrib/sass-loader#sassoptions","enum":["legacy","modern","modern-compiler"]},"sassOptions":{"description":"Options for `node-sass` or `sass` (`Dart Sass`) implementation.","link":"https://github.com/webpack-contrib/sass-loader#sassoptions","anyOf":[{"type":"object","additionalProperties":true},{"instanceof":"Function"}]},"additionalData":{"description":"Prepends/Appends `Sass`/`SCSS` code before the actual entry file.","link":"https://github.com/webpack-contrib/sass-loader#additionaldata","anyOf":[{"type":"string"},{"instanceof":"Function"}]},"sourceMap":{"description":"Enables/Disables generation of source maps.","link":"https://github.com/webpack-contrib/sass-loader#sourcemap","type":"boolean"},"webpackImporter":{"description":"Enables/Disables default `webpack` importer.","link":"https://github.com/webpack-contrib/sass-loader#webpackimporter","type":"boolean"},"warnRuleAsWarning":{"description":"Treats the \'@warn\' rule as a webpack warning.","link":"https://github.com/webpack-contrib/sass-loader#warnruleaswarning","type":"boolean"}},"additionalProperties":false}')}};var __webpack_module_cache__={};function __nccwpck_require__(e){var t=__webpack_module_cache__[e];if(t!==undefined){return t.exports}var s=__webpack_module_cache__[e]={exports:{}};var n=true;try{__webpack_modules__[e](s,s.exports,__nccwpck_require__);n=false}finally{if(n)delete __webpack_module_cache__[e]}return s.exports}if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var __webpack_exports__=__nccwpck_require__(906);module.exports=__webpack_exports__})(); \ No newline at end of file +(function(){"use strict";var __webpack_modules__={276:function(e,t,s){const n=s(501);e.exports=n.default},501:function(e,t,s){Object.defineProperty(t,"__esModule",{value:true});t["default"]=void 0;var n=_interopRequireDefault(s(310));var o=_interopRequireDefault(s(17));var r=_interopRequireDefault(s(351));var a=s(147);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}async function loader(e){const t=this.getOptions(r.default);const s=this.async();let i;try{i=(0,a.getSassImplementation)(this,t.implementation)}catch(e){s(e);return}const c=typeof t.sourceMap==="boolean"?t.sourceMap:this.sourceMap;const l=await(0,a.getSassOptions)(this,t,e,i,c);const p=typeof t.webpackImporter==="boolean"?t.webpackImporter:true;if(p){const e=t.api==="modern"||t.api==="modern-compiler";if(!e){const{includePaths:e}=l;l.importer.push((0,a.getWebpackImporter)(this,i,e))}else{l.importers.push((0,a.getModernWebpackImporter)(this,i,[]))}}let u;try{u=(0,a.getCompileFn)(this,i,t)}catch(e){s(e);return}let d;try{d=await u(l)}catch(e){if(e.span&&typeof e.span.url!=="undefined"){this.addDependency(n.default.fileURLToPath(e.span.url))}else if(typeof e.file!=="undefined"){this.addDependency(o.default.normalize(e.file))}s((0,a.errorFactory)(e));return}let f=d.sourceMap?d.sourceMap:d.map?JSON.parse(d.map):null;if(f&&c){f=(0,a.normalizeSourceMap)(f,this.rootContext)}if(typeof d.loadedUrls!=="undefined"){d.loadedUrls.filter((e=>e.protocol==="file:")).forEach((e=>{const t=n.default.fileURLToPath(e);if(o.default.isAbsolute(t)){this.addDependency(t)}}))}else if(typeof d.stats!=="undefined"&&typeof d.stats.includedFiles!=="undefined"){d.stats.includedFiles.forEach((e=>{const t=o.default.normalize(e);if(o.default.isAbsolute(t)){this.addDependency(t)}}))}s(null,d.css.toString(),f)}var i=t["default"]=loader},147:function(__unused_webpack_module,exports,__nccwpck_require__){Object.defineProperty(exports,"__esModule",{value:true});exports.errorFactory=errorFactory;exports.getCompileFn=getCompileFn;exports.getModernWebpackImporter=getModernWebpackImporter;exports.getSassImplementation=getSassImplementation;exports.getSassOptions=getSassOptions;exports.getWebpackImporter=getWebpackImporter;exports.getWebpackResolver=getWebpackResolver;exports.normalizeSourceMap=normalizeSourceMap;var _url=_interopRequireDefault(__nccwpck_require__(310));var _path=_interopRequireDefault(__nccwpck_require__(17));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}function getDefaultSassImplementation(){let sassImplPkg="sass";try{require.resolve("sass-embedded");sassImplPkg="sass-embedded"}catch(ignoreError){try{eval("require").resolve("sass")}catch(_ignoreError){try{eval("require").resolve("node-sass");sassImplPkg="node-sass"}catch(e){sassImplPkg="sass"}}}return __nccwpck_require__(438)}function getSassImplementation(e,t){let s=t;if(!s){s=getDefaultSassImplementation()}if(typeof s==="string"){s=require(s)}const{info:n}=s;if(!n){throw new Error("Unknown Sass implementation.")}const o=n.split("\t");if(o.length<2){throw new Error(`Unknown Sass implementation "${n}".`)}const[r]=o;if(r==="dart-sass"){return s}else if(r==="node-sass"){return s}else if(r==="sass-embedded"){return s}throw new Error(`Unknown Sass implementation "${r}".`)}function isProductionLikeMode(e){return e.mode==="production"||!e.mode}function proxyCustomImporters(e,t){return[].concat(e).map((e=>function proxyImporter(...s){const n={...this,webpackLoaderContext:t};return e.apply(n,s)}))}async function getSassOptions(e,t,s,n,o){const r=t.sassOptions?typeof t.sassOptions==="function"?t.sassOptions(e)||{}:t.sassOptions:{};const a={...r,data:t.additionalData?typeof t.additionalData==="function"?await t.additionalData(s,e):`${t.additionalData}\n${s}`:s};if(!a.logger){const s=t.warnRuleAsWarning!==false;const n=e.getLogger("sass-loader");const formatSpan=e=>`Warning on line ${e.start.line}, column ${e.start.column} of ${e.url||"-"}:${e.start.line}:${e.start.column}:\n`;const formatDebugSpan=e=>`[debug:${e.start.line}:${e.start.column}] `;a.logger={debug(e,t){let s="";if(t.span){s=formatDebugSpan(t.span)}s+=e;n.debug(s)},warn(t,o){let r="";if(o.deprecation){r+="Deprecation "}if(o.span){r+=formatSpan(o.span)}r+=t;if(o.span&&o.span.context){r+=`\n\n${o.span.start.line} | ${o.span.context}`}if(o.stack&&o.stack!=="null"){r+=`\n\n${o.stack}`}if(s){const t=new Error(r);t.name="SassWarning";t.stack=null;e.emitWarning(t)}else{n.warn(r)}}}}const i=t.api==="modern"||t.api==="modern-compiler";const{resourcePath:c}=e;if(i){a.url=_url.default.pathToFileURL(c);if(!a.style&&isProductionLikeMode(e)){a.style="compressed"}if(o){a.sourceMap=true}if(typeof a.syntax==="undefined"){const e=_path.default.extname(c);if(e&&e.toLowerCase()===".scss"){a.syntax="scss"}else if(e&&e.toLowerCase()===".sass"){a.syntax="indented"}else if(e&&e.toLowerCase()===".css"){a.syntax="css"}}a.loadPaths=[].concat((a.loadPaths?a.loadPaths.slice():[]).map((e=>_path.default.isAbsolute(e)?e:_path.default.join(process.cwd(),e)))).concat(process.env.SASS_PATH?process.env.SASS_PATH.split(process.platform==="win32"?";":":"):[]);a.importers=a.importers?Array.isArray(a.importers)?a.importers.slice():[a.importers]:[]}else{a.file=c;if(!a.outputStyle&&isProductionLikeMode(e)){a.outputStyle="compressed"}if(o){a.sourceMap=true;a.outFile=_path.default.join(e.rootContext,"style.css.map");a.sourceMapContents=true;a.omitSourceMapUrl=true;a.sourceMapEmbed=false}const s=_path.default.extname(c);if(s&&s.toLowerCase()===".sass"&&typeof a.indentedSyntax==="undefined"){a.indentedSyntax=true}else{a.indentedSyntax=Boolean(a.indentedSyntax)}a.importer=a.importer?proxyCustomImporters(Array.isArray(a.importer)?a.importer.slice():[a.importer],e):[];if(t.webpackImporter===false&&a.importer.length===0){a.importer=undefined}a.includePaths=[].concat(process.cwd()).concat((a.includePaths?a.includePaths.slice():[]).map((e=>_path.default.isAbsolute(e)?e:_path.default.join(process.cwd(),e)))).concat(process.env.SASS_PATH?process.env.SASS_PATH.split(process.platform==="win32"?";":":"):[]);if(typeof a.charset==="undefined"){a.charset=true}}return a}const MODULE_REQUEST_REGEX=/^[^?]*~/;const IS_MODULE_IMPORT=/^~([^/]+|[^/]+\/|@[^/]+[/][^/]+|@[^/]+\/?|@[^/]+[/][^/]+\/)$/;const IS_PKG_SCHEME=/^pkg:/i;function getPossibleRequests(e,t=false,s=false){let n=e;if(t){if(MODULE_REQUEST_REGEX.test(e)){n=n.replace(MODULE_REQUEST_REGEX,"")}if(IS_PKG_SCHEME.test(e)){n=`${n.slice(4)}`;return[...new Set([n,e])]}if(IS_MODULE_IMPORT.test(e)||IS_PKG_SCHEME.test(e)){n=n[n.length-1]==="/"?n:`${n}/`;return[...new Set([n,e])]}}const o=_path.default.extname(n).toLowerCase();if(o===".css"){return[]}const r=_path.default.dirname(n);const a=r==="."?"":`${r}/`;const i=_path.default.basename(n);const c=_path.default.basename(n,o);return[...new Set([].concat(s?[`${a}_${c}.import${o}`,`${a}${c}.import${o}`]:[]).concat([`${a}_${i}`,`${a}${i}`]).concat(t?[e]:[]))]}function promiseResolve(e){return(t,s)=>new Promise(((n,o)=>{e(t,s,((e,t)=>{if(e){o(e)}else{n(t)}}))}))}async function startResolving(e){if(e.length===0){return Promise.reject()}const[{possibleRequests:t}]=e;if(t.length===0){return Promise.reject()}const[{resolve:s,context:n}]=e;try{return await s(n,t[0])}catch(s){const[,...n]=t;if(n.length===0){const[,...t]=e;return startResolving(t)}e[0].possibleRequests=n;return startResolving(e)}}const IS_SPECIAL_MODULE_IMPORT=/^~[^/]+$/;const IS_NATIVE_WIN32_PATH=/^[a-z]:[/\\]|^\\\\/i;function getWebpackResolver(e,t,s=[]){const n=t&&(t.info.includes("dart-sass")||t.info.includes("sass-embedded"));const o=promiseResolve(e({alias:[],aliasFields:[],conditionNames:[],descriptionFiles:[],extensions:[".sass",".scss",".css"],exportsFields:[],mainFields:[],mainFiles:["_index","index"],modules:[],restrictions:[/\.((sa|sc|c)ss)$/i],preferRelative:true}));const r=promiseResolve(e({alias:[],aliasFields:[],conditionNames:[],descriptionFiles:[],extensions:[".sass",".scss",".css"],exportsFields:[],mainFields:[],mainFiles:["_index.import","_index","index.import","index"],modules:[],restrictions:[/\.((sa|sc|c)ss)$/i],preferRelative:true}));const a=promiseResolve(e({dependencyType:"sass",conditionNames:["sass","style","..."],mainFields:["sass","style","main","..."],mainFiles:["_index","index","..."],extensions:[".sass",".scss",".css"],restrictions:[/\.((sa|sc|c)ss)$/i],preferRelative:true}));const i=promiseResolve(e({dependencyType:"sass",conditionNames:["sass","style","..."],mainFields:["sass","style","main","..."],mainFiles:["_index.import","_index","index.import","index","..."],extensions:[".sass",".scss",".css"],restrictions:[/\.((sa|sc|c)ss)$/i],preferRelative:true}));return(e,t,c)=>{if(!n&&!_path.default.isAbsolute(e)){return Promise.reject()}const l=t;const p=l.slice(0,5).toLowerCase()==="file:";if(p){try{t=_url.default.fileURLToPath(l)}catch(e){t=t.slice(7)}}let u=[];const d=!IS_SPECIAL_MODULE_IMPORT.test(t)&&!IS_PKG_SCHEME.test(t)&&!p&&!l.startsWith("/")&&!IS_NATIVE_WIN32_PATH.test(l);if(s.length>0&&d){const a=getPossibleRequests(t,false,c);if(!n){u=u.concat({resolve:c?r:o,context:_path.default.dirname(e),possibleRequests:a})}u=u.concat(s.map((e=>({resolve:c?r:o,context:e,possibleRequests:a}))))}const f=getPossibleRequests(t,true,c);u=u.concat({resolve:c?i:a,context:_path.default.dirname(e),possibleRequests:f});return startResolving(u)}}const MATCH_CSS=/\.css$/i;function getModernWebpackImporter(e,t,s){const n=getWebpackResolver(e.getResolve,t,s);return{async canonicalize(t,s){const{fromImport:o}=s;const r=s.containingUrl?_url.default.fileURLToPath(s.containingUrl.toString()):e.resourcePath;let a;try{a=await n(r,t,o)}catch(e){return null}e.addDependency(_path.default.normalize(a));return _url.default.pathToFileURL(a)},async load(t){const s=_path.default.extname(t.pathname);let n;if(s&&s.toLowerCase()===".scss"){n="scss"}else if(s&&s.toLowerCase()===".sass"){n="indented"}else if(s&&s.toLowerCase()===".css"){n="css"}else{n="scss"}try{const s=await new Promise(((s,n)=>{const o=_url.default.fileURLToPath(t);e.fs.readFile(o,"utf8",((e,t)=>{if(e){n(e);return}s(t)}))}));return{contents:s,syntax:n}}catch(e){return null}}}}function getWebpackImporter(e,t,s){const n=getWebpackResolver(e.getResolve,t,s);return function importer(t,s,o){const{fromImport:r}=this;n(s,t,r).then((t=>{e.addDependency(_path.default.normalize(t));o({file:t.replace(MATCH_CSS,"")})})).catch((()=>{o({file:t})}))}}let nodeSassJobQueue=null;const sassModernCompilers=new WeakMap;function getCompileFn(e,t,s){const n=t.info.includes("dart-sass")||t.info.includes("sass-embedded");if(n){if(s.api==="modern"){return e=>{const{data:s,...n}=e;return t.compileStringAsync(s,n)}}if(s.api==="modern-compiler"){return async s=>{const n=e._compiler;const{data:o,...r}=s;if(n){if(!sassModernCompilers.has(n)){const e=await t.initAsyncCompiler();if(!sassModernCompilers.has(n)){sassModernCompilers.set(n,e);n.hooks.shutdown.tap("sass-loader",(()=>{e.dispose()}))}}return sassModernCompilers.get(n).compileStringAsync(o,r)}return t.compileStringAsync(o,r)}}return e=>new Promise(((s,n)=>{t.render(e,((e,t)=>{if(e){n(e);return}s(t)}))}))}if(s.api==="modern"||s.api==="modern-compiler"){throw new Error("Modern API is not supported for 'node-sass'")}if(nodeSassJobQueue===null){const e=Number(process.env.UV_THREADPOOL_SIZE||4);const s=__nccwpck_require__(175);nodeSassJobQueue=s.queue(t.render.bind(t),e-1)}return e=>new Promise(((t,s)=>{nodeSassJobQueue.push.bind(nodeSassJobQueue)(e,((e,n)=>{if(e){s(e);return}t(n)}))}))}const ABSOLUTE_SCHEME=/^[A-Za-z0-9+\-.]+:/;function getURLType(e){if(e[0]==="/"){if(e[1]==="/"){return"scheme-relative"}return"path-absolute"}if(IS_NATIVE_WIN32_PATH.test(e)){return"path-absolute"}return ABSOLUTE_SCHEME.test(e)?"absolute":"path-relative"}function normalizeSourceMap(e,t){const s=e;if(typeof s.file!=="undefined"){delete s.file}s.sourceRoot="";s.sources=s.sources.map((e=>{const s=getURLType(e);if(s==="absolute"&&/^file:/i.test(e)){return _url.default.fileURLToPath(e)}else if(s==="path-relative"){return _path.default.resolve(t,_path.default.normalize(e))}return e}));return s}function errorFactory(e){let t;if(e.formatted){t=e.formatted.replace(/^Error: /,"")}else{({message:t}=e)}const s=new Error(t,{cause:e});s.stack=null;return s}},175:function(e){e.exports=require("next/dist/compiled/neo-async")},17:function(e){e.exports=require("path")},438:function(e){e.exports=require("sass")},310:function(e){e.exports=require("url")},351:function(e){e.exports=JSON.parse('{"title":"Sass Loader options","type":"object","properties":{"implementation":{"description":"The implementation of the sass to be used.","link":"https://github.com/webpack-contrib/sass-loader#implementation","anyOf":[{"type":"string"},{"type":"object"}]},"api":{"description":"Switch between old and modern API for `sass` (`Dart Sass`) and `Sass Embedded` implementations.","link":"https://github.com/webpack-contrib/sass-loader#sassoptions","enum":["legacy","modern","modern-compiler"]},"sassOptions":{"description":"Options for `node-sass` or `sass` (`Dart Sass`) implementation.","link":"https://github.com/webpack-contrib/sass-loader#sassoptions","anyOf":[{"type":"object","additionalProperties":true},{"instanceof":"Function"}]},"additionalData":{"description":"Prepends/Appends `Sass`/`SCSS` code before the actual entry file.","link":"https://github.com/webpack-contrib/sass-loader#additionaldata","anyOf":[{"type":"string"},{"instanceof":"Function"}]},"sourceMap":{"description":"Enables/Disables generation of source maps.","link":"https://github.com/webpack-contrib/sass-loader#sourcemap","type":"boolean"},"webpackImporter":{"description":"Enables/Disables default `webpack` importer.","link":"https://github.com/webpack-contrib/sass-loader#webpackimporter","type":"boolean"},"warnRuleAsWarning":{"description":"Treats the \'@warn\' rule as a webpack warning.","link":"https://github.com/webpack-contrib/sass-loader#warnruleaswarning","type":"boolean"}},"additionalProperties":false}')}};var __webpack_module_cache__={};function __nccwpck_require__(e){var t=__webpack_module_cache__[e];if(t!==undefined){return t.exports}var s=__webpack_module_cache__[e]={exports:{}};var n=true;try{__webpack_modules__[e](s,s.exports,__nccwpck_require__);n=false}finally{if(n)delete __webpack_module_cache__[e]}return s.exports}if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var __webpack_exports__=__nccwpck_require__(276);module.exports=__webpack_exports__})(); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c6922695b21..b707742f4158 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,8 +125,8 @@ importers: specifier: 1.5.10 version: 1.5.10 '@rspack/core': - specifier: 1.3.8 - version: 1.3.8(@swc/helpers@0.5.15) + specifier: 1.3.9 + version: 1.3.9(@swc/helpers@0.5.15) '@rspack/plugin-react-refresh': specifier: 1.2.0 version: 1.2.0(react-refresh@0.12.0)(webpack-hot-middleware@2.26.1) @@ -1047,7 +1047,7 @@ importers: version: 8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(react-dom@19.2.0-canary-197d6a04-20250424(react@19.2.0-canary-197d6a04-20250424))(react@19.2.0-canary-197d6a04-20250424)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) '@storybook/react-webpack5': specifier: 8.6.0 - version: 8.6.0(@rspack/core@1.3.8(@swc/helpers@0.5.15))(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-197d6a04-20250424(react@19.2.0-canary-197d6a04-20250424))(react@19.2.0-canary-197d6a04-20250424)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) + version: 8.6.0(@rspack/core@1.3.9(@swc/helpers@0.5.15))(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-197d6a04-20250424(react@19.2.0-canary-197d6a04-20250424))(react@19.2.0-canary-197d6a04-20250424)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) '@storybook/test': specifier: 8.6.0 version: 8.6.0(storybook@8.6.0(prettier@3.3.3)) @@ -1437,7 +1437,7 @@ importers: version: 0.13.4 sass-loader: specifier: 15.0.0 - version: 15.0.0(@rspack/core@1.3.8(@swc/helpers@0.5.15))(sass@1.77.8)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)) + version: 15.0.0(@rspack/core@1.3.9(@swc/helpers@0.5.15))(sass@1.77.8)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)) schema-utils2: specifier: npm:schema-utils@2.7.1 version: schema-utils@2.7.1 @@ -1664,8 +1664,8 @@ importers: packages/next-rspack: dependencies: '@rspack/core': - specifier: 1.3.8 - version: 1.3.8(@swc/helpers@0.5.15) + specifier: 1.3.9 + version: 1.3.9(@swc/helpers@0.5.15) '@rspack/plugin-react-refresh': specifier: 1.2.0 version: 1.2.0(react-refresh@0.12.0)(webpack-hot-middleware@2.26.1) @@ -4238,23 +4238,23 @@ packages: '@types/react': ^19.1.1 react: 19.2.0-canary-197d6a04-20250424 - '@module-federation/error-codes@0.13.0': - resolution: {integrity: sha512-4soAMLr7qcVWuvCsyRmBbiBfuhxmnDeyl+qzjMx8VurQgL+XQDQJapM9RXngNGT4g8FoCq9o7rM5YWNgFFNUiw==} + '@module-federation/error-codes@0.13.1': + resolution: {integrity: sha512-azgGDBnFRfqlivHOl96ZjlFUFlukESz2Rnnz/pINiSqoBBNjUE0fcAZP4X6jgrVITuEg90YkruZa7pW9I3m7Uw==} - '@module-federation/runtime-core@0.13.0': - resolution: {integrity: sha512-Oj/1p0mfxZ+8EbU7ND4gMvRmikFpIvPCbblOgat9N8ZIVAKYpTimCgMhzg4yRqAwzlGCVwnnW7XZ8UlA+Zqrvg==} + '@module-federation/runtime-core@0.13.1': + resolution: {integrity: sha512-TfyKfkSAentKeuvSsAItk8s5tqQSMfIRTPN2e1aoaq/kFhE+7blps719csyWSX5Lg5Es7WXKMsXHy40UgtBtuw==} - '@module-federation/runtime-tools@0.13.0': - resolution: {integrity: sha512-6ECWX18yGrQKcmkrQoNPd5VEpxZP1SMaB/Bp55xlpEhsrpn4zHnriQluxDw6xldjSOLl1qbokfxwCwjS2OaEbg==} + '@module-federation/runtime-tools@0.13.1': + resolution: {integrity: sha512-GEF1pxqLc80osIMZmE8j9UKZSaTm2hX2lql8tgIH/O9yK4wnF06k6LL5Ah+wJt+oJv6Dj55ri/MoxMP4SXoPNA==} - '@module-federation/runtime@0.13.0': - resolution: {integrity: sha512-Ne/3AEVWz6LL6G/i41O5MC6YYlg0SatNNqG/0XbuMAfyGM+llRmB6VKt0o2+JR4isxWuPNp97TbUkkfORit6Eg==} + '@module-federation/runtime@0.13.1': + resolution: {integrity: sha512-ZHnYvBquDm49LiHfv6fgagMo/cVJneijNJzfPh6S0CJrPS2Tay1bnTXzy8VA5sdIrESagYPaskKMGIj7YfnPug==} - '@module-federation/sdk@0.13.0': - resolution: {integrity: sha512-JdMZaPD+EQvMJYS+/8/8QjaAHQ3qljogvioXBsAuedcStu/msn5e1Fswc0G34kXY9ixs2hUPZU2cAllfSKWIBQ==} + '@module-federation/sdk@0.13.1': + resolution: {integrity: sha512-bmf2FGQ0ymZuxYnw9bIUfhV3y6zDhaqgydEjbl4msObKMLGXZqhse2pTIIxBFpIxR1oONKX/y2FAolDCTlWKiw==} - '@module-federation/webpack-bundler-runtime@0.13.0': - resolution: {integrity: sha512-ycgAsFeCTo+3GR8JxkhCyg2UZm6Au98ISdLTdVXYphO4UDcO/KjqyJen1LXEslkpCEohDj68Prei2fUHRruK6g==} + '@module-federation/webpack-bundler-runtime@0.13.1': + resolution: {integrity: sha512-QSuSIGa09S8mthbB1L6xERqrz+AzPlHR6D7RwAzssAc+IHf40U6NiTLPzUqp9mmKDhC5Tm0EISU0ZHNeJpnpBQ==} '@mswjs/cookies@1.1.0': resolution: {integrity: sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==} @@ -4735,56 +4735,56 @@ packages: cpu: [x64] os: [win32] - '@rspack/binding-darwin-arm64@1.3.8': - resolution: {integrity: sha512-FlfWZzwCxDfLwyiqGaCSINHt2Er1Wno9xZrf2QM7Ss00HyocPo4BUYGYBEi4dai/fPFoeYKeEAdsNdrVmFH4+g==} + '@rspack/binding-darwin-arm64@1.3.9': + resolution: {integrity: sha512-lfTmsbUGab9Ak/X6aPLacHLe4MBRra+sLmhoNK8OKEN3qQCjDcomwW5OlmBRV5bcUYWdbK8vgDk2HUUXRuibVg==} cpu: [arm64] os: [darwin] - '@rspack/binding-darwin-x64@1.3.8': - resolution: {integrity: sha512-IGXDKHDHiL7WxE/OZMaeIuHzqOzDam3k8WrseHAdl5upKvCp/snwwGdulB/rqGxwkQIXIsv105vIFbGOAe2g0A==} + '@rspack/binding-darwin-x64@1.3.9': + resolution: {integrity: sha512-rYuOUINhnhLDbG5LHHKurRSuKIsw0LKUHcd6AAsFmijo4RMnGBJ4NOI4tOLAQvkoSTQ+HU5wiTGSQOgHVhYreQ==} cpu: [x64] os: [darwin] - '@rspack/binding-linux-arm64-gnu@1.3.8': - resolution: {integrity: sha512-PU9fv8knPvbxQb8NrDmTrLVpy8QY0vuhzk69/ZuLRW89c0P14HovYeHV+38cQHho4++avUQgVp6vnJI9vSQjtg==} + '@rspack/binding-linux-arm64-gnu@1.3.9': + resolution: {integrity: sha512-pBKnS2Fbn9cDtWe1KcD1qRjQlJwQhP9pFW2KpxdjE7qXbaO11IHtem6dLZwdpNqbDn9QgyfdVGXBDvBaP1tGwA==} cpu: [arm64] os: [linux] - '@rspack/binding-linux-arm64-musl@1.3.8': - resolution: {integrity: sha512-UMZBuTw5iXeA6gmtZYQvAb7g56odfoIkU6YvfqV67AMU0EY2y52sc7ABFloDzURJ1xd2om01Nlru8y48S2lMPw==} + '@rspack/binding-linux-arm64-musl@1.3.9': + resolution: {integrity: sha512-0B+iiINW0qOEkBE9exsRcdmcHtYIWAoJGnXrz9tUiiewRxX0Cmm0MjD2HAVUAggJZo+9IN8RGz5PopCjJ/dn1g==} cpu: [arm64] os: [linux] - '@rspack/binding-linux-x64-gnu@1.3.8': - resolution: {integrity: sha512-48hfwVsD2/Caa0HgZiqE1T20H89cnomcaP92++x8t4IQ2uKA9xCeBW87RD/AaKXcb78aM987ctE+asKjN8OVjw==} + '@rspack/binding-linux-x64-gnu@1.3.9': + resolution: {integrity: sha512-82izGJw/qxJ4xaHJy/A4MF7aTRT9tE6VlWoWM4rJmqRszfujN/w54xJRie9jkt041TPvJWGNpYD4Hjpt0/n/oA==} cpu: [x64] os: [linux] - '@rspack/binding-linux-x64-musl@1.3.8': - resolution: {integrity: sha512-Jx+JlVnLzzVL/62NbEFaVcM2HU4QtNEF+wzo+yODNprx78ZLe3PJT/LdtwLMvE77K2PlGn5CZcmBay6Xwkd/2A==} + '@rspack/binding-linux-x64-musl@1.3.9': + resolution: {integrity: sha512-V9nDg63iPI6Z7kM11UPV5kBdOdLXPIu3IgI2ObON5Rd4KEZr7RLo/Q4HKzj0IH27Zwl5qeBJdx69zZdu66eOqg==} cpu: [x64] os: [linux] - '@rspack/binding-win32-arm64-msvc@1.3.8': - resolution: {integrity: sha512-84tifCsYhir/p5GH0knBOXtLpfRzIFDxF4nF4bHsuwaA1uqwyk0WlWGt4ZwRUtyzh0TN4cJdnqJl/f5209BdLw==} + '@rspack/binding-win32-arm64-msvc@1.3.9': + resolution: {integrity: sha512-owWCJTezFkiBOSRzH+eOTN15H5QYyThHE5crZ0I30UmpoSEchcPSCvddliA0W62ZJIOgG4IUSNamKBiiTwdjLQ==} cpu: [arm64] os: [win32] - '@rspack/binding-win32-ia32-msvc@1.3.8': - resolution: {integrity: sha512-Grrcfr95gRhJ7FbKtIxfhNAzSM+hvtD2jAMs9fmw/UrgiNsXeaWwJaYgImqHGirKIx8iygZ0t1q7ePIVM+SKMg==} + '@rspack/binding-win32-ia32-msvc@1.3.9': + resolution: {integrity: sha512-YUuNA8lkGSXJ07fOjkX+yuWrWcsU5x5uGFuAYsglw+rDTWCS6m9HSwQjbCp7HUp81qPszjSk+Ore5XVh07FKeQ==} cpu: [ia32] os: [win32] - '@rspack/binding-win32-x64-msvc@1.3.8': - resolution: {integrity: sha512-wW+Ig3kVqcRcY+3mxZnruN4AdeJYjbEBd2zvheEAOvx/DC+xEQ6czvDXbZEZQQ9rU/znhuKl0Z+898q8l3LwzA==} + '@rspack/binding-win32-x64-msvc@1.3.9': + resolution: {integrity: sha512-E0gtYBVt5vRj0zBeplEf8wsVDPDQ6XBdRiFVUgmgwYUYYkXaalaIvbD1ioB8cA05vfz8HrPGXcMrgletUP4ojA==} cpu: [x64] os: [win32] - '@rspack/binding@1.3.8': - resolution: {integrity: sha512-0oGrPgnwDsrDN7Swk7OZGvee8y/AdvDXF3f1QewkueJ5uyDaGszDxipEpf644HWIcj11fgNJQEphGEhaAVjofw==} + '@rspack/binding@1.3.9': + resolution: {integrity: sha512-3FFen1/0F2aP5uuCm8vPaJOrzM3karCPNMsc5gLCGfEy2rsK38Qinf9W4p1bw7+FhjOTzoSdkX+LFHeMDVxJhw==} - '@rspack/core@1.3.8': - resolution: {integrity: sha512-1zefymDypUROYzGGNa553JR1Ah8En25npwSRIZCuZvfjo6nME6XvjkMxQwhjzMStoqRmFD9+nKUHSiN5jVWWyw==} + '@rspack/core@1.3.9': + resolution: {integrity: sha512-u7usd9srCBPBfNJCSvsfh14AOPq6LCVna0Vb/aA2nyJTawHqzfAMz1QRb/e27nP3NrV6RPiwx03W494Dd6r6wg==} engines: {node: '>=16.0.0'} peerDependencies: '@swc/helpers': '>=0.5.1' @@ -19548,30 +19548,30 @@ snapshots: '@types/react': 19.1.1 react: 19.2.0-canary-197d6a04-20250424 - '@module-federation/error-codes@0.13.0': {} + '@module-federation/error-codes@0.13.1': {} - '@module-federation/runtime-core@0.13.0': + '@module-federation/runtime-core@0.13.1': dependencies: - '@module-federation/error-codes': 0.13.0 - '@module-federation/sdk': 0.13.0 + '@module-federation/error-codes': 0.13.1 + '@module-federation/sdk': 0.13.1 - '@module-federation/runtime-tools@0.13.0': + '@module-federation/runtime-tools@0.13.1': dependencies: - '@module-federation/runtime': 0.13.0 - '@module-federation/webpack-bundler-runtime': 0.13.0 + '@module-federation/runtime': 0.13.1 + '@module-federation/webpack-bundler-runtime': 0.13.1 - '@module-federation/runtime@0.13.0': + '@module-federation/runtime@0.13.1': dependencies: - '@module-federation/error-codes': 0.13.0 - '@module-federation/runtime-core': 0.13.0 - '@module-federation/sdk': 0.13.0 + '@module-federation/error-codes': 0.13.1 + '@module-federation/runtime-core': 0.13.1 + '@module-federation/sdk': 0.13.1 - '@module-federation/sdk@0.13.0': {} + '@module-federation/sdk@0.13.1': {} - '@module-federation/webpack-bundler-runtime@0.13.0': + '@module-federation/webpack-bundler-runtime@0.13.1': dependencies: - '@module-federation/runtime': 0.13.0 - '@module-federation/sdk': 0.13.0 + '@module-federation/runtime': 0.13.1 + '@module-federation/sdk': 0.13.1 '@mswjs/cookies@1.1.0': {} @@ -20139,49 +20139,49 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.39.0': optional: true - '@rspack/binding-darwin-arm64@1.3.8': + '@rspack/binding-darwin-arm64@1.3.9': optional: true - '@rspack/binding-darwin-x64@1.3.8': + '@rspack/binding-darwin-x64@1.3.9': optional: true - '@rspack/binding-linux-arm64-gnu@1.3.8': + '@rspack/binding-linux-arm64-gnu@1.3.9': optional: true - '@rspack/binding-linux-arm64-musl@1.3.8': + '@rspack/binding-linux-arm64-musl@1.3.9': optional: true - '@rspack/binding-linux-x64-gnu@1.3.8': + '@rspack/binding-linux-x64-gnu@1.3.9': optional: true - '@rspack/binding-linux-x64-musl@1.3.8': + '@rspack/binding-linux-x64-musl@1.3.9': optional: true - '@rspack/binding-win32-arm64-msvc@1.3.8': + '@rspack/binding-win32-arm64-msvc@1.3.9': optional: true - '@rspack/binding-win32-ia32-msvc@1.3.8': + '@rspack/binding-win32-ia32-msvc@1.3.9': optional: true - '@rspack/binding-win32-x64-msvc@1.3.8': + '@rspack/binding-win32-x64-msvc@1.3.9': optional: true - '@rspack/binding@1.3.8': + '@rspack/binding@1.3.9': optionalDependencies: - '@rspack/binding-darwin-arm64': 1.3.8 - '@rspack/binding-darwin-x64': 1.3.8 - '@rspack/binding-linux-arm64-gnu': 1.3.8 - '@rspack/binding-linux-arm64-musl': 1.3.8 - '@rspack/binding-linux-x64-gnu': 1.3.8 - '@rspack/binding-linux-x64-musl': 1.3.8 - '@rspack/binding-win32-arm64-msvc': 1.3.8 - '@rspack/binding-win32-ia32-msvc': 1.3.8 - '@rspack/binding-win32-x64-msvc': 1.3.8 - - '@rspack/core@1.3.8(@swc/helpers@0.5.15)': - dependencies: - '@module-federation/runtime-tools': 0.13.0 - '@rspack/binding': 1.3.8 + '@rspack/binding-darwin-arm64': 1.3.9 + '@rspack/binding-darwin-x64': 1.3.9 + '@rspack/binding-linux-arm64-gnu': 1.3.9 + '@rspack/binding-linux-arm64-musl': 1.3.9 + '@rspack/binding-linux-x64-gnu': 1.3.9 + '@rspack/binding-linux-x64-musl': 1.3.9 + '@rspack/binding-win32-arm64-msvc': 1.3.9 + '@rspack/binding-win32-ia32-msvc': 1.3.9 + '@rspack/binding-win32-x64-msvc': 1.3.9 + + '@rspack/core@1.3.9(@swc/helpers@0.5.15)': + dependencies: + '@module-federation/runtime-tools': 0.13.1 + '@rspack/binding': 1.3.9 '@rspack/lite-tapable': 1.0.1 caniuse-lite: 1.0.30001579 optionalDependencies: @@ -20350,7 +20350,7 @@ snapshots: react: 19.2.0-canary-197d6a04-20250424 react-dom: 19.2.0-canary-197d6a04-20250424(react@19.2.0-canary-197d6a04-20250424) - '@storybook/builder-webpack5@8.6.0(@rspack/core@1.3.8(@swc/helpers@0.5.15))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2)': + '@storybook/builder-webpack5@8.6.0(@rspack/core@1.3.9(@swc/helpers@0.5.15))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2)': dependencies: '@storybook/core-webpack': 8.6.0(storybook@8.6.0(prettier@3.3.3)) '@types/semver': 7.5.6 @@ -20358,10 +20358,10 @@ snapshots: case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.1 constants-browserify: 1.0.0 - css-loader: 6.11.0(@rspack/core@1.3.8(@swc/helpers@0.5.15))(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)) + css-loader: 6.11.0(@rspack/core@1.3.9(@swc/helpers@0.5.15))(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)) es-module-lexer: 1.6.0 fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.2)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)) - html-webpack-plugin: 5.6.3(@rspack/core@1.3.8(@swc/helpers@0.5.15))(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)) + html-webpack-plugin: 5.6.3(@rspack/core@1.3.9(@swc/helpers@0.5.15))(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)) magic-string: 0.30.17 path-browserify: 1.0.1 process: 0.11.10 @@ -20492,9 +20492,9 @@ snapshots: react-dom: 19.2.0-canary-197d6a04-20250424(react@19.2.0-canary-197d6a04-20250424) storybook: 8.6.0(prettier@3.3.3) - '@storybook/react-webpack5@8.6.0(@rspack/core@1.3.8(@swc/helpers@0.5.15))(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-197d6a04-20250424(react@19.2.0-canary-197d6a04-20250424))(react@19.2.0-canary-197d6a04-20250424)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2)': + '@storybook/react-webpack5@8.6.0(@rspack/core@1.3.9(@swc/helpers@0.5.15))(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-197d6a04-20250424(react@19.2.0-canary-197d6a04-20250424))(react@19.2.0-canary-197d6a04-20250424)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2)': dependencies: - '@storybook/builder-webpack5': 8.6.0(@rspack/core@1.3.8(@swc/helpers@0.5.15))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) + '@storybook/builder-webpack5': 8.6.0(@rspack/core@1.3.9(@swc/helpers@0.5.15))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) '@storybook/preset-react-webpack': 8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-197d6a04-20250424(react@19.2.0-canary-197d6a04-20250424))(react@19.2.0-canary-197d6a04-20250424)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) '@storybook/react': 8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(react-dom@19.2.0-canary-197d6a04-20250424(react@19.2.0-canary-197d6a04-20250424))(react@19.2.0-canary-197d6a04-20250424)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) react: 19.2.0-canary-197d6a04-20250424 @@ -23435,7 +23435,7 @@ snapshots: postcss: 8.4.31 postcss-selector-parser: 6.0.11 - css-loader@6.11.0(@rspack/core@1.3.8(@swc/helpers@0.5.15))(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)): + css-loader@6.11.0(@rspack/core@1.3.9(@swc/helpers@0.5.15))(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)): dependencies: icss-utils: 5.1.0(postcss@8.4.49) postcss: 8.4.49 @@ -23446,7 +23446,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.6.3 optionalDependencies: - '@rspack/core': 1.3.8(@swc/helpers@0.5.15) + '@rspack/core': 1.3.9(@swc/helpers@0.5.15) webpack: 5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2) css-prefers-color-scheme@6.0.3(postcss@8.4.31): @@ -26513,7 +26513,7 @@ snapshots: transitivePeerDependencies: - supports-color - html-webpack-plugin@5.6.3(@rspack/core@1.3.8(@swc/helpers@0.5.15))(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)): + html-webpack-plugin@5.6.3(@rspack/core@1.3.9(@swc/helpers@0.5.15))(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -26521,7 +26521,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - '@rspack/core': 1.3.8(@swc/helpers@0.5.15) + '@rspack/core': 1.3.9(@swc/helpers@0.5.15) webpack: 5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2) htmlparser2@3.10.1: @@ -32462,11 +32462,11 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@15.0.0(@rspack/core@1.3.8(@swc/helpers@0.5.15))(sass@1.77.8)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)): + sass-loader@15.0.0(@rspack/core@1.3.9(@swc/helpers@0.5.15))(sass@1.77.8)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)): dependencies: neo-async: 2.6.2 optionalDependencies: - '@rspack/core': 1.3.8(@swc/helpers@0.5.15) + '@rspack/core': 1.3.9(@swc/helpers@0.5.15) sass: 1.77.8 webpack: 5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2) diff --git a/test/integration/middleware-prefetch/tests/index.test.js b/test/integration/middleware-prefetch/tests/index.test.js index ce264cfbbdaa..ece627800f42 100644 --- a/test/integration/middleware-prefetch/tests/index.test.js +++ b/test/integration/middleware-prefetch/tests/index.test.js @@ -60,7 +60,11 @@ describe('Middleware Production Prefetch', () => { const attrs = await Promise.all( scripts.map((script) => script.getAttribute('src')) ) - return attrs.find((src) => src.includes('/ssg-page')) ? 'yes' : 'nope' + // Check if the filename follows the format `static/chunks/pages/[pagename]-[hash].js` + // and specifically targets files containing '/ssg-page-' in their path. + return attrs.find((src) => src.includes('/ssg-page-')) + ? 'yes' + : 'nope' }, 'yes') }) @@ -72,7 +76,7 @@ describe('Middleware Production Prefetch', () => { const attrs = await Promise.all( scripts.map((script) => script.getAttribute('src')) ) - return attrs.find((src) => src.includes('/ssg-page-2')) + return attrs.find((src) => src.includes('/ssg-page-2-')) ? 'nope' : 'yes' }, 'yes') From 5c3a298dc75b727ad5bcab8d18b5d8e468f4c12f Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 10 May 2025 00:33:17 +0200 Subject: [PATCH 02/10] [not-found] Add global-not-found convention (#78783) ### Feature Introducing a new convention `global-not-found.js`, which let you build your own 404 page within app router. This convention is similar to `global-error.js` but for not found in general case, it's similar to `not-found.js` but including the layout. App router has an implicit convention of building the 404 route, which is composing the top-level root `layout.js` and the root `not-found.js` to build up a 404 page. Since it's so implicit so that users don't have much control over it, which also lead to some confusing errors like `/_not-found` route is build failed due to various reason. The `global-not-found.js` convention is aimed to help users to build 404 route in App Router easily and convenience. Currently it will be bound with a flag `experimental.globalNotFound` and later it will become the stable feature in next major release as it introduces a minor breaking change that we won't use root layout and root not-found to compose the 404 page. Instead, we'll either choose the builtin 404 page or users customized `global-not-found.js` to generate the 404 page when you enable the feature. #### Advantages Good new that if you were having struggles with creating 404 page for the multi root layouts app, such as the case below. Then `global-not-found.js` is your choose. ``` - (2024)/ /layout.js /... # routes - (2025)/ /layout.js /... # routes ``` Closes NDX-1036 Related #59180 #### Follow up The metadata and deployment will be followed up in new PRs --- crates/next-api/src/app.rs | 7 +- crates/next-core/src/app_page_loader_tree.rs | 16 +++ crates/next-core/src/app_structure.rs | 71 ++++++++-- crates/next-core/src/base_loader_tree.rs | 2 + crates/next-core/src/next_config.rs | 7 + packages/next/src/build/entries.ts | 15 +- packages/next/src/build/utils.ts | 2 +- .../webpack/loaders/next-app-loader/index.ts | 132 ++++++++++++++---- .../plugins/flight-client-entry-plugin.ts | 14 ++ .../client/components/global-not-found.tsx | 16 +++ .../http-access-fallback/error-fallback.tsx | 37 +---- .../components/styles/access-error-styles.ts | 34 +++++ .../next/src/server/app-render/app-render.tsx | 8 +- packages/next/src/server/config-schema.ts | 1 + packages/next/src/server/config-shared.ts | 7 + .../src/server/dev/hot-reloader-turbopack.ts | 3 +- .../src/server/dev/hot-reloader-webpack.ts | 30 ++-- .../src/server/dev/on-demand-entry-handler.ts | 52 +++++-- packages/next/types/$$compiled.internal.d.ts | 1 + .../basic/app/call-not-found/page.tsx | 7 + .../global-not-found/basic/app/client.tsx | 5 + .../basic/app/global-not-found.tsx | 13 ++ .../global-not-found/basic/app/layout.tsx | 11 ++ .../global-not-found/basic/app/page.tsx | 3 + .../basic/global-not-found-basic.test.ts | 44 ++++++ .../global-not-found/basic/next.config.js | 10 ++ .../both-present/app/call-not-found/page.tsx | 7 + .../both-present/app/global-not-found.tsx | 10 ++ .../both-present/app/layout.tsx | 11 ++ .../both-present/app/not-found.tsx | 3 + .../both-present/both-present.test.ts | 31 ++++ .../both-present/next.config.js | 10 ++ .../no-root-layout/app/(bar)/bar/layout.tsx | 16 +++ .../no-root-layout/app/(bar)/bar/page.tsx | 3 + .../no-root-layout/app/(foo)/foo/layout.tsx | 16 +++ .../no-root-layout/app/(foo)/foo/page.tsx | 3 + .../no-root-layout/app/global-not-found.tsx | 10 ++ .../no-root-layout/next.config.js | 10 ++ .../no-root-layout/no-root-layout.test.ts | 30 ++++ .../not-present/app/call-not-found/page.tsx | 7 + .../not-present/app/layout.tsx | 16 +++ .../not-present/app/not-found.tsx | 3 + .../global-not-found/not-present/app/page.tsx | 3 + .../not-present/next.config.js | 10 ++ .../not-present/not-present.test.ts | 24 ++++ 45 files changed, 664 insertions(+), 107 deletions(-) create mode 100644 packages/next/src/client/components/global-not-found.tsx create mode 100644 packages/next/src/client/components/styles/access-error-styles.ts create mode 100644 test/e2e/app-dir/global-not-found/basic/app/call-not-found/page.tsx create mode 100644 test/e2e/app-dir/global-not-found/basic/app/client.tsx create mode 100644 test/e2e/app-dir/global-not-found/basic/app/global-not-found.tsx create mode 100644 test/e2e/app-dir/global-not-found/basic/app/layout.tsx create mode 100644 test/e2e/app-dir/global-not-found/basic/app/page.tsx create mode 100644 test/e2e/app-dir/global-not-found/basic/global-not-found-basic.test.ts create mode 100644 test/e2e/app-dir/global-not-found/basic/next.config.js create mode 100644 test/e2e/app-dir/global-not-found/both-present/app/call-not-found/page.tsx create mode 100644 test/e2e/app-dir/global-not-found/both-present/app/global-not-found.tsx create mode 100644 test/e2e/app-dir/global-not-found/both-present/app/layout.tsx create mode 100644 test/e2e/app-dir/global-not-found/both-present/app/not-found.tsx create mode 100644 test/e2e/app-dir/global-not-found/both-present/both-present.test.ts create mode 100644 test/e2e/app-dir/global-not-found/both-present/next.config.js create mode 100644 test/e2e/app-dir/global-not-found/no-root-layout/app/(bar)/bar/layout.tsx create mode 100644 test/e2e/app-dir/global-not-found/no-root-layout/app/(bar)/bar/page.tsx create mode 100644 test/e2e/app-dir/global-not-found/no-root-layout/app/(foo)/foo/layout.tsx create mode 100644 test/e2e/app-dir/global-not-found/no-root-layout/app/(foo)/foo/page.tsx create mode 100644 test/e2e/app-dir/global-not-found/no-root-layout/app/global-not-found.tsx create mode 100644 test/e2e/app-dir/global-not-found/no-root-layout/next.config.js create mode 100644 test/e2e/app-dir/global-not-found/no-root-layout/no-root-layout.test.ts create mode 100644 test/e2e/app-dir/global-not-found/not-present/app/call-not-found/page.tsx create mode 100644 test/e2e/app-dir/global-not-found/not-present/app/layout.tsx create mode 100644 test/e2e/app-dir/global-not-found/not-present/app/not-found.tsx create mode 100644 test/e2e/app-dir/global-not-found/not-present/app/page.tsx create mode 100644 test/e2e/app-dir/global-not-found/not-present/next.config.js create mode 100644 test/e2e/app-dir/global-not-found/not-present/not-present.test.ts diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index de3079b64da2..af3a7314644a 100644 --- a/crates/next-api/src/app.rs +++ b/crates/next-api/src/app.rs @@ -194,7 +194,12 @@ impl AppProject { #[turbo_tasks::function] fn app_entrypoints(&self) -> Vc { - get_entrypoints(*self.app_dir, self.project.next_config().page_extensions()) + let conf = self.project.next_config(); + get_entrypoints( + *self.app_dir, + conf.page_extensions(), + conf.is_global_not_found_enabled(), + ) } #[turbo_tasks::function] diff --git a/crates/next-core/src/app_page_loader_tree.rs b/crates/next-core/src/app_page_loader_tree.rs index d92b6efead50..38a974688a87 100644 --- a/crates/next-core/src/app_page_loader_tree.rs +++ b/crates/next-core/src/app_page_loader_tree.rs @@ -335,6 +335,7 @@ impl AppPageLoaderTreeBuilder { default, error, global_error, + global_not_found, layout, loading, template, @@ -375,6 +376,8 @@ impl AppPageLoaderTreeBuilder { .await?; self.write_modules_entry(AppDirModuleType::GlobalError, *global_error) .await?; + self.write_modules_entry(AppDirModuleType::GlobalNotFound, *global_not_found) + .await?; let modules_code = replace(&mut self.loader_tree_code, temp_loader_tree_code); @@ -399,6 +402,7 @@ impl AppPageLoaderTreeBuilder { let loader_tree = &*loader_tree.await?; let modules = &loader_tree.modules; + // load global-error module if let Some(global_error) = modules.global_error { let module = self .base @@ -407,6 +411,17 @@ impl AppPageLoaderTreeBuilder { .await?; self.base.inner_assets.insert(GLOBAL_ERROR.into(), module); }; + // load global-not-found module + if let Some(global_not_found) = modules.global_not_found { + let module = self + .base + .process_source(Vc::upcast(FileSource::new(*global_not_found))) + .to_resolved() + .await?; + self.base + .inner_assets + .insert(GLOBAL_NOT_FOUND.into(), module); + }; self.walk_tree(loader_tree, true).await?; Ok(AppPageLoaderTreeModule { @@ -439,3 +454,4 @@ impl AppPageLoaderTreeModule { } pub const GLOBAL_ERROR: &str = "GLOBAL_ERROR_MODULE"; +pub const GLOBAL_NOT_FOUND: &str = "GLOBAL_NOT_FOUND_MODULE"; diff --git a/crates/next-core/src/app_structure.rs b/crates/next-core/src/app_structure.rs index dd2ed86854fc..4c5dc5505262 100644 --- a/crates/next-core/src/app_structure.rs +++ b/crates/next-core/src/app_structure.rs @@ -39,6 +39,8 @@ pub struct AppDirModules { #[serde(skip_serializing_if = "Option::is_none")] pub global_error: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub global_not_found: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub loading: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub template: Option>, @@ -63,6 +65,7 @@ impl AppDirModules { layout: self.layout, error: self.error, global_error: self.global_error, + global_not_found: self.global_not_found, loading: self.loading, template: self.template, not_found: self.not_found, @@ -322,6 +325,7 @@ async fn get_directory_tree_internal( "layout" => modules.layout = Some(file), "error" => modules.error = Some(file), "global-error" => modules.global_error = Some(file), + "global-not-found" => modules.global_not_found = Some(file), "loading" => modules.loading = Some(file), "template" => modules.template = Some(file), "forbidden" => modules.forbidden = Some(file), @@ -730,11 +734,13 @@ fn add_app_metadata_route( pub fn get_entrypoints( app_dir: Vc, page_extensions: Vc>, + is_global_not_found_enabled: Vc, ) -> Vc { directory_tree_to_entrypoints( app_dir, get_directory_tree(app_dir, page_extensions), get_global_metadata(app_dir, page_extensions), + is_global_not_found_enabled, Default::default(), ) } @@ -744,11 +750,13 @@ fn directory_tree_to_entrypoints( app_dir: Vc, directory_tree: Vc, global_metadata: Vc, + is_global_not_found_enabled: Vc, root_layouts: Vc, ) -> Vc { directory_tree_to_entrypoints_internal( app_dir, global_metadata, + is_global_not_found_enabled, "".into(), directory_tree, AppPage::new(), @@ -1124,6 +1132,7 @@ async fn default_route_tree( async fn directory_tree_to_entrypoints_internal( app_dir: ResolvedVc, global_metadata: Vc, + is_global_not_found_enabled: Vc, directory_name: RcStr, directory_tree: Vc, app_page: AppPage, @@ -1133,6 +1142,7 @@ async fn directory_tree_to_entrypoints_internal( directory_tree_to_entrypoints_internal_untraced( app_dir, global_metadata, + is_global_not_found_enabled, directory_name, directory_tree, app_page, @@ -1145,6 +1155,7 @@ async fn directory_tree_to_entrypoints_internal( async fn directory_tree_to_entrypoints_internal_untraced( app_dir: ResolvedVc, global_metadata: Vc, + is_global_not_found_enabled: Vc, directory_name: RcStr, directory_tree: Vc, app_page: AppPage, @@ -1284,6 +1295,13 @@ async fn directory_tree_to_entrypoints_internal_untraced( // Next.js has this logic in "collect-app-paths", where the root not-found page // is considered as its own entry point. + + // Determine if we enable the global not-found feature. + let is_global_not_found_enabled = *is_global_not_found_enabled.await?; + let use_global_not_found = + is_global_not_found_enabled || modules.global_not_found.is_some(); + + let not_found_root_modules = modules.without_leafs(); let not_found_tree = AppPageLoaderTree { page: app_page.clone(), segment: directory_name.clone(), @@ -1296,24 +1314,54 @@ async fn directory_tree_to_entrypoints_internal_untraced( page: app_page.clone(), segment: "__PAGE__".into(), parallel_routes: FxIndexMap::default(), - modules: AppDirModules { - page: match modules.not_found { - Some(v) => Some(v), - None => Some(get_next_package(*app_dir) - .join("dist/client/components/not-found-error.js".into()) - .to_resolved() - .await?), - }, - ..Default::default() + modules: if use_global_not_found { + // if global-not-found.js is present: + // we use it for the page and no layout, since layout is included in global-not-found.js; + AppDirModules { + layout: None, + page: match modules.global_not_found { + Some(v) => Some(v), + None => Some(get_next_package(*app_dir) + .join("dist/client/components/global-not-found.js".into()) + .to_resolved() + .await?), + }, + ..Default::default() + } + } else { + // if global-not-found.js is not present: + // we search if we can compose root layout with the root not-found.js; + AppDirModules { + page: match modules.not_found { + Some(v) => Some(v), + None => Some(get_next_package(*app_dir) + .join("dist/client/components/not-found-error.js".into()) + .to_resolved() + .await?), + }, + ..Default::default() + } }, global_metadata: global_metadata.to_resolved().await?, } }, - modules: AppDirModules::default(), + modules: AppDirModules { + ..Default::default() + }, global_metadata: global_metadata.to_resolved().await?, }, }, - modules: modules.without_leafs(), + modules: AppDirModules { + // `global-not-found.js` does not need a layout since it's included. + // Skip it if it's present. + // Otherwise, we need to compose it with the root layout to compose with not-found.js boundary. + layout: if use_global_not_found { + None + } else { + modules.layout + }, + ..not_found_root_modules + }, global_metadata: global_metadata.to_resolved().await?, } .resolved_cell(); @@ -1345,6 +1393,7 @@ async fn directory_tree_to_entrypoints_internal_untraced( let map = directory_tree_to_entrypoints_internal( *app_dir, global_metadata, + is_global_not_found_enabled, subdir_name.clone(), *subdirectory, child_app_page.clone(), diff --git a/crates/next-core/src/base_loader_tree.rs b/crates/next-core/src/base_loader_tree.rs index 20020a503ad7..f6ece7e1d51e 100644 --- a/crates/next-core/src/base_loader_tree.rs +++ b/crates/next-core/src/base_loader_tree.rs @@ -32,6 +32,7 @@ pub enum AppDirModuleType { Forbidden, Unauthorized, GlobalError, + GlobalNotFound, } impl AppDirModuleType { @@ -47,6 +48,7 @@ impl AppDirModuleType { AppDirModuleType::Forbidden => "forbidden", AppDirModuleType::Unauthorized => "unauthorized", AppDirModuleType::GlobalError => "global-error", + AppDirModuleType::GlobalNotFound => "global-not-found", } } } diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index 9fdd199c3d21..1fca5a2a772e 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -799,6 +799,8 @@ pub struct ExperimentalConfig { turbopack_persistent_caching: Option, turbopack_source_maps: Option, turbopack_tree_shaking: Option, + // Whether to enable the global-not-found convention + global_not_found: Option, } #[derive( @@ -1189,6 +1191,11 @@ impl NextConfig { Vc::cell(self.page_extensions.clone()) } + #[turbo_tasks::function] + pub fn is_global_not_found_enabled(&self) -> Vc { + Vc::cell(self.experimental.global_not_found.unwrap_or_default()) + } + #[turbo_tasks::function] pub fn transpile_packages(&self) -> Vc> { Vc::cell(self.transpile_packages.clone().unwrap_or_default()) diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index f9d615d4e281..64c3cc213bf5 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -301,11 +301,12 @@ export async function createPagesMapping({ page.endsWith('/page') ) return { - // If there's any app pages existed, add a default not-found page. - // If there's any custom not-found page existed, it will override the default one. + // If there's any app pages existed, add a default /_not-found route as 404. + // If there's any custom /_not-found page, it will override the default one. ...(hasAppPages && { - [UNDERSCORE_NOT_FOUND_ROUTE_ENTRY]: - 'next/dist/client/components/not-found-error', + [UNDERSCORE_NOT_FOUND_ROUTE_ENTRY]: require.resolve( + 'next/dist/client/components/global-not-found' + ), }), ...pages, } @@ -720,6 +721,9 @@ export async function createEntrypoints( : undefined, preferredRegion: staticInfo.preferredRegion, middlewareConfig: encodeToBase64(staticInfo.middleware || {}), + isGlobalNotFoundEnabled: config.experimental.globalNotFound + ? true + : undefined, }) } else if (isInstrumentation) { server[serverBundlePath.replace('src/', '')] = @@ -799,6 +803,9 @@ export async function createEntrypoints( middlewareConfig: Buffer.from( JSON.stringify(staticInfo.middleware || {}) ).toString('base64'), + isGlobalNotFoundEnabled: config.experimental.globalNotFound + ? true + : undefined, }).import } edgeServer[serverBundlePath] = getEdgeServerEntry({ diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 845a43a7a469..475dd6b8fd63 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1751,7 +1751,7 @@ export function isReservedPage(page: string) { } export function isAppBuiltinNotFoundPage(page: string) { - return /next[\\/]dist[\\/]client[\\/]components[\\/]not-found-error/.test( + return /next[\\/]dist[\\/]client[\\/]components[\\/](not-found-error|global-not-found)/.test( page ) } diff --git a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts index c9b70eb64f30..dfbe103fe87b 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts @@ -50,6 +50,7 @@ export type AppLoaderOptions = { nextConfigOutput?: NextConfig['output'] nextConfigExperimentalUseEarlyImport?: true middlewareConfig: string + isGlobalNotFoundEnabled: true | undefined } type AppLoader = webpack.LoaderDefinitionFunction @@ -70,15 +71,19 @@ const FILE_TYPES = { error: 'error', loading: 'loading', 'global-error': 'global-error', + 'global-not-found': 'global-not-found', ...HTTP_ACCESS_FALLBACKS, } as const const GLOBAL_ERROR_FILE_TYPE = 'global-error' +const GLOBAL_NOT_FOUND_FILE_TYPE = 'global-not-found' const PAGE_SEGMENT = 'page$' const PARALLEL_CHILDREN_SEGMENT = 'children$' const defaultGlobalErrorPath = 'next/dist/client/components/global-error' +const defaultNotFoundPath = 'next/dist/client/components/not-found-error' const defaultLayoutPath = 'next/dist/client/components/default-layout' +const defaultGlobalNotFoundPath = 'next/dist/client/components/global-not-found' type DirResolver = (pathToResolve: string) => string type PathResolver = ( @@ -123,6 +128,7 @@ async function createTreeCodeFromPath( pageExtensions, basePath, collectedDeclarations, + isGlobalNotFoundEnabled, }: { page: string resolveDir: DirResolver @@ -135,22 +141,25 @@ async function createTreeCodeFromPath( pageExtensions: PageExtensions basePath: string collectedDeclarations: [string, string][] + isGlobalNotFoundEnabled: boolean } ): Promise<{ treeCode: string pages: string rootLayout: string | undefined globalError: string + globalNotFound: string }> { const splittedPath = pagePath.split(/[\\/]/, 1) const isNotFoundRoute = page === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY - const isDefaultNotFound = isAppBuiltinNotFoundPage(pagePath) + const appDirPrefix = isDefaultNotFound ? APP_DIR_ALIAS : splittedPath[0] const pages: string[] = [] let rootLayout: string | undefined let globalError: string | undefined + let globalNotFound: string | undefined async function resolveAdjacentParallelSegments( segmentPath: string @@ -210,9 +219,7 @@ async function createTreeCodeFromPath( null const routerDirPath = `${appDirPrefix}${segmentPath}` // For default not-found, don't traverse the directory to find metadata. - const resolvedRouteDir = isDefaultNotFound - ? '' - : await resolveDir(routerDirPath) + const resolvedRouteDir = isDefaultNotFound ? '' : resolveDir(routerDirPath) if (resolvedRouteDir) { metadata = await createStaticMetadataFromRoute(resolvedRouteDir, { @@ -294,7 +301,7 @@ async function createTreeCodeFromPath( }) ) - const definedFilePaths = filePaths.filter( + let definedFilePaths = filePaths.filter( ([, filePath]) => filePath !== undefined ) as [ValueOf, string][] @@ -325,7 +332,9 @@ async function createTreeCodeFromPath( !hasLayerFallbackFile ) { const defaultFallbackPath = defaultHTTPAccessFallbackPaths[type] - definedFilePaths.push([type, defaultFallbackPath]) + if (!(isDefaultNotFound && type === 'not-found')) { + definedFilePaths.push([type, defaultFallbackPath]) + } } } } @@ -336,7 +345,15 @@ async function createTreeCodeFromPath( )?.[1] rootLayout = layoutPath - if (isDefaultNotFound && !layoutPath && !rootLayout) { + // When `global-not-found` is disabled, we insert a default layout if + // root layout is presented. This logic and the default layout will be removed + // once `global-not-found` is stabilized. + if ( + !isGlobalNotFoundEnabled && + isDefaultNotFound && + !layoutPath && + !rootLayout + ) { rootLayout = defaultLayoutPath definedFilePaths.push(['layout', rootLayout]) } @@ -350,6 +367,16 @@ async function createTreeCodeFromPath( globalError = resolvedGlobalErrorPath } } + // TODO(global-not-found): remove this flag assertion condition + // once global-not-found is stable + if (isGlobalNotFoundEnabled && !globalNotFound) { + const resolvedGlobalNotFoundPath = await resolver( + `${appDirPrefix}/${GLOBAL_NOT_FOUND_FILE_TYPE}` + ) + if (resolvedGlobalNotFoundPath) { + globalNotFound = resolvedGlobalNotFoundPath + } + } let parallelSegmentKey = Array.isArray(parallelSegment) ? parallelSegment[0] @@ -368,23 +395,57 @@ async function createTreeCodeFromPath( const normalizedParallelKey = normalizeParallelKey(parallelKey) let subtreeCode // If it's root not found page, set not-found boundary as children page - if (isNotFoundRoute && normalizedParallelKey === 'children') { - const notFoundPath = - definedFilePaths.find(([type]) => type === 'not-found')?.[1] ?? - defaultHTTPAccessFallbackPaths['not-found'] - - const varName = `notFound${nestedCollectedDeclarations.length}` - nestedCollectedDeclarations.push([varName, notFoundPath]) - subtreeCode = `{ - children: [${JSON.stringify(UNDERSCORE_NOT_FOUND_ROUTE)}, { - children: ['${PAGE_SEGMENT_KEY}', {}, { - page: [ - ${varName}, - ${JSON.stringify(notFoundPath)} - ] - }] - }, {}] - }` + if (isNotFoundRoute) { + if (normalizedParallelKey === 'children') { + const matchedGlobalNotFound = isGlobalNotFoundEnabled + ? definedFilePaths.find( + ([type]) => type === GLOBAL_NOT_FOUND_FILE_TYPE + )?.[1] ?? defaultGlobalNotFoundPath + : undefined + + // If custom global-not-found.js is defined, use global-not-found.js + if (matchedGlobalNotFound) { + const varName = `notFound${nestedCollectedDeclarations.length}` + nestedCollectedDeclarations.push([varName, matchedGlobalNotFound]) + subtreeCode = `{ + children: [${JSON.stringify(UNDERSCORE_NOT_FOUND_ROUTE)}, { + children: ['${PAGE_SEGMENT_KEY}', {}, { + page: [ + ${varName}, + ${JSON.stringify(matchedGlobalNotFound)} + ] + }] + }, {}] + }` + } else { + // If custom not-found.js is found, use it and layout to compose the page, + // and fallback to built-in not-found component if doesn't exist. + const notFoundPath = + definedFilePaths.find(([type]) => type === 'not-found')?.[1] ?? + defaultNotFoundPath + const varName = `notFound${nestedCollectedDeclarations.length}` + nestedCollectedDeclarations.push([varName, notFoundPath]) + subtreeCode = `{ + children: [${JSON.stringify(UNDERSCORE_NOT_FOUND_ROUTE)}, { + children: ['${PAGE_SEGMENT_KEY}', {}, { + page: [ + ${varName}, + ${JSON.stringify(notFoundPath)} + ] + }] + }, {}] + }` + } + } + } + + // For 404 route + // if global-not-found is in definedFilePaths, remove root layout for /_not-found + // TODO: remove this once global-not-found is stable. + if (isNotFoundRoute && isGlobalNotFoundEnabled) { + definedFilePaths = definedFilePaths.filter( + ([type]) => type !== 'layout' + ) } const modulesCode = `{ @@ -461,6 +522,7 @@ async function createTreeCodeFromPath( pages: `${JSON.stringify(pages)};`, rootLayout, globalError: globalError ?? defaultGlobalErrorPath, + globalNotFound: globalNotFound ?? defaultNotFoundPath, } } @@ -495,6 +557,14 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { nextConfigExperimentalUseEarlyImport, } = loaderOptions + const isGlobalNotFoundEnabled = !!loaderOptions.isGlobalNotFoundEnabled + + // Update FILE_TYPES on the very top-level of the loader + if (!isGlobalNotFoundEnabled) { + // @ts-expect-error this delete is only necessary while experimental + delete FILE_TYPES['global-not-found'] + } + const buildInfo = getModuleBuildInfo((this as any)._module) const collectedDeclarations: [string, string][] = [] const page = name.replace(/^app/, '') @@ -509,7 +579,10 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { relatedModules: [], } - const extensions = pageExtensions.map((extension) => `.${extension}`) + const extensions = + typeof pageExtensions === 'string' + ? [pageExtensions] + : pageExtensions.map((extension) => `.${extension}`) const normalizedAppPaths = typeof appPaths === 'string' ? [appPaths] : appPaths || [] @@ -687,9 +760,15 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { pageExtensions, basePath, collectedDeclarations, + isGlobalNotFoundEnabled, }) - if (!treeCodeResult.rootLayout) { + const isGlobalNotFoundPath = + page === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY && + !!treeCodeResult.globalNotFound && + isGlobalNotFoundEnabled + + if (!treeCodeResult.rootLayout && !isGlobalNotFoundPath) { if (!isDev) { // If we're building and missing a root layout, exit the build Log.error( @@ -736,6 +815,7 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { pageExtensions, basePath, collectedDeclarations, + isGlobalNotFoundEnabled, }) } } diff --git a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts index 24e53380fa99..6d48186a7352 100644 --- a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts @@ -400,6 +400,20 @@ export class FlightClientEntryPlugin { absolutePagePath: entryRequest, }) } + + if ( + name === `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}` && + bundlePath === 'app/global-not-found' + ) { + clientEntriesToInject.push({ + compiler, + compilation, + entryName: name, + clientComponentImports, + bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, + absolutePagePath: entryRequest, + }) + } } // Make sure CSS imports are deduplicated before injecting the client entry diff --git a/packages/next/src/client/components/global-not-found.tsx b/packages/next/src/client/components/global-not-found.tsx new file mode 100644 index 000000000000..1d903ed6d6df --- /dev/null +++ b/packages/next/src/client/components/global-not-found.tsx @@ -0,0 +1,16 @@ +import { HTTPAccessErrorFallback } from './http-access-fallback/error-fallback' + +function GlobalNotFound() { + return ( + + + + + + ) +} + +export default GlobalNotFound diff --git a/packages/next/src/client/components/http-access-fallback/error-fallback.tsx b/packages/next/src/client/components/http-access-fallback/error-fallback.tsx index 81668495161c..ca1805de03d8 100644 --- a/packages/next/src/client/components/http-access-fallback/error-fallback.tsx +++ b/packages/next/src/client/components/http-access-fallback/error-fallback.tsx @@ -1,39 +1,4 @@ -import React from 'react' - -const styles: Record = { - error: { - // https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css#L38-L52 - fontFamily: - 'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"', - height: '100vh', - textAlign: 'center', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - }, - - desc: { - display: 'inline-block', - }, - - h1: { - display: 'inline-block', - margin: '0 20px 0 0', - padding: '0 23px 0 0', - fontSize: 24, - fontWeight: 500, - verticalAlign: 'top', - lineHeight: '49px', - }, - - h2: { - fontSize: 14, - fontWeight: 400, - lineHeight: '49px', - margin: 0, - }, -} +import { styles } from '../styles/access-error-styles' export function HTTPAccessErrorFallback({ status, diff --git a/packages/next/src/client/components/styles/access-error-styles.ts b/packages/next/src/client/components/styles/access-error-styles.ts new file mode 100644 index 000000000000..6c0e52c633d8 --- /dev/null +++ b/packages/next/src/client/components/styles/access-error-styles.ts @@ -0,0 +1,34 @@ +export const styles: Record = { + error: { + // https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css#L38-L52 + fontFamily: + 'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"', + height: '100vh', + textAlign: 'center', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + + desc: { + display: 'inline-block', + }, + + h1: { + display: 'inline-block', + margin: '0 20px 0 0', + padding: '0 23px 0 0', + fontSize: 24, + fontWeight: 500, + verticalAlign: 'top', + lineHeight: '49px', + }, + + h2: { + fontSize: 14, + fontWeight: 400, + lineHeight: '49px', + margin: 0, + }, +} diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 4ddbdfc067b0..e9b49811d7b2 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -325,8 +325,8 @@ function parseRequestHeaders( } function createNotFoundLoaderTree(loaderTree: LoaderTree): LoaderTree { - // Align the segment with parallel-route-default in next-app-loader const components = loaderTree[2] + const hasGlobalNotFound = !!components['global-not-found'] return [ '', { @@ -334,11 +334,12 @@ function createNotFoundLoaderTree(loaderTree: LoaderTree): LoaderTree { PAGE_SEGMENT_KEY, {}, { - page: components['not-found'], + page: components['global-not-found'] ?? components['not-found'], }, ], }, - components, + // When global-not-found is present, skip layout from components + hasGlobalNotFound ? components : {}, ] } @@ -1282,7 +1283,6 @@ async function renderToHTMLOrFlightImpl( // Pull out the hooks/references from the component. const { tree: loaderTree, taintObjectReference } = ComponentMod - if (enableTainting) { taintObjectReference( 'Do not pass process.env to Client Components since it will leak sensitive data', diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 83b701116d27..ab492a9e5ceb 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -493,6 +493,7 @@ export const configSchema: zod.ZodType = z.lazy(() => buildTimeThresholdMs: z.number().int(), }) .optional(), + globalNotFound: z.boolean().optional(), }) .optional(), exportPathMap: z diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 0f271f03cfc2..729372374151 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -671,6 +671,12 @@ export interface ExperimentalConfig { * Note: Use with caution as this can negatively impact page loading performance. */ clientInstrumentationHook?: boolean + + /** + * Enables using the global-not-found.js file in the app directory + * + */ + globalNotFound?: boolean } export type ExportPathMap = { @@ -1366,6 +1372,7 @@ export const defaultConfig: NextConfig = { inlineCss: false, useCache: undefined, slowModuleDetection: undefined, + globalNotFound: false, }, htmlLimitedBots: undefined, bundlePagesRouterDependencies: false, diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index 33471bc56fa2..8f89197850a6 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -979,7 +979,8 @@ export async function createHotReloaderTurbopack( inputPage, nextConfig.pageExtensions, opts.pagesDir, - opts.appDir + opts.appDir, + !!nextConfig.experimental.globalNotFound )) // If the route is actually an app page route, then we should have access diff --git a/packages/next/src/server/dev/hot-reloader-webpack.ts b/packages/next/src/server/dev/hot-reloader-webpack.ts index 0f08d04c08fa..abd4dcaa9a16 100644 --- a/packages/next/src/server/dev/hot-reloader-webpack.ts +++ b/packages/next/src/server/dev/hot-reloader-webpack.ts @@ -12,7 +12,7 @@ import { getSourceMapMiddleware, } from '../../client/components/react-dev-overlay/server/middleware-webpack' import { WebpackHotMiddleware } from './hot-middleware' -import { join, relative, isAbsolute, posix } from 'path' +import { join, relative, isAbsolute, posix, dirname } from 'path' import { createEntrypoints, createPagesMapping, @@ -962,6 +962,10 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { middlewareConfig: Buffer.from( JSON.stringify(staticInfo?.middleware || {}) ).toString('base64'), + isGlobalNotFoundEnabled: this.config.experimental + .globalNotFound + ? true + : undefined, }).import : undefined @@ -1052,17 +1056,23 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { preferredRegion: staticInfo?.preferredRegion, }) } else if (isAppPath) { + // This path normalization is critical for webpack to resolve the next internals as entry. + const pagePath = entryData.absolutePagePath.startsWith( + dirname(require.resolve('next/package.json')) + ) + ? entryData.absolutePagePath + : posix.join( + APP_DIR_ALIAS, + relative( + this.appDir!, + entryData.absolutePagePath + ).replace(/\\/g, '/') + ) value = getAppEntry({ name: bundlePath, page, appPaths: entryData.appPaths, - pagePath: posix.join( - APP_DIR_ALIAS, - relative( - this.appDir!, - entryData.absolutePagePath - ).replace(/\\/g, '/') - ), + pagePath, appDir: this.appDir!, pageExtensions: this.config.pageExtensions, rootDir: this.dir, @@ -1075,6 +1085,10 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { middlewareConfig: Buffer.from( JSON.stringify(staticInfo?.middleware || {}) ).toString('base64'), + isGlobalNotFoundEnabled: this.config.experimental + .globalNotFound + ? true + : undefined, }) } else if (isAPIRoute(page)) { value = getRouteLoaderEntry({ diff --git a/packages/next/src/server/dev/on-demand-entry-handler.ts b/packages/next/src/server/dev/on-demand-entry-handler.ts index ef555c5ed47c..5f6f26c59b3e 100644 --- a/packages/next/src/server/dev/on-demand-entry-handler.ts +++ b/packages/next/src/server/dev/on-demand-entry-handler.ts @@ -393,8 +393,9 @@ export async function findPagePathData( rootDir: string, page: string, extensions: string[], - pagesDir?: string, - appDir?: string + pagesDir: string | undefined, + appDir: string | undefined, + isGlobalNotFoundEnabled: boolean ): Promise { const normalizedPagePath = tryToNormalizePagePath(page) let pagePath: string | null = null @@ -436,23 +437,43 @@ export async function findPagePathData( // Check appDir first falling back to pagesDir if (appDir) { if (page === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY) { - const notFoundPath = await findPageFile( - appDir, - 'not-found', - extensions, - true - ) - if (notFoundPath) { - return { - filename: join(appDir, notFoundPath), - bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, - page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, + // Load `global-not-found` when global-not-found is enabled. + // Prefer to load it when both `global-not-found` and root `not-found` present. + if (isGlobalNotFoundEnabled) { + const globalNotFoundPath = await findPageFile( + appDir, + 'global-not-found', + extensions, + true + ) + if (globalNotFoundPath) { + return { + filename: join(appDir, globalNotFoundPath), + bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, + page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, + } + } + } else { + // Then if global-not-found.js doesn't exist then load not-found.js + const notFoundPath = await findPageFile( + appDir, + 'not-found', + extensions, + true + ) + if (notFoundPath) { + return { + filename: join(appDir, notFoundPath), + bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, + page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, + } } } + // If they're not presented, then fallback to global-not-found return { filename: require.resolve( - 'next/dist/client/components/not-found-error' + 'next/dist/client/components/global-not-found' ), bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, @@ -726,7 +747,8 @@ export function onDemandEntryHandler({ page, nextConfig.pageExtensions, pagesDir, - appDir + appDir, + !!nextConfig.experimental.globalNotFound ) } diff --git a/packages/next/types/$$compiled.internal.d.ts b/packages/next/types/$$compiled.internal.d.ts index 1817701d60ee..dec7ffddcdd0 100644 --- a/packages/next/types/$$compiled.internal.d.ts +++ b/packages/next/types/$$compiled.internal.d.ts @@ -337,6 +337,7 @@ declare module 'react-server-dom-webpack/client.edge' { } declare module 'VAR_MODULE_GLOBAL_ERROR' +declare module 'VAR_MODULE_GLOBAL_NOT_FOUND' declare module 'VAR_USERLAND' declare module 'VAR_MODULE_DOCUMENT' declare module 'VAR_MODULE_APP' diff --git a/test/e2e/app-dir/global-not-found/basic/app/call-not-found/page.tsx b/test/e2e/app-dir/global-not-found/basic/app/call-not-found/page.tsx new file mode 100644 index 000000000000..6f7be9458297 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/basic/app/call-not-found/page.tsx @@ -0,0 +1,7 @@ +'use client' + +import { notFound } from 'next/navigation' + +export default function Page() { + notFound() +} diff --git a/test/e2e/app-dir/global-not-found/basic/app/client.tsx b/test/e2e/app-dir/global-not-found/basic/app/client.tsx new file mode 100644 index 000000000000..f5733496f458 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/basic/app/client.tsx @@ -0,0 +1,5 @@ +'use client' + +export function Client() { + return
client
+} diff --git a/test/e2e/app-dir/global-not-found/basic/app/global-not-found.tsx b/test/e2e/app-dir/global-not-found/basic/app/global-not-found.tsx new file mode 100644 index 000000000000..6ac8620dd99d --- /dev/null +++ b/test/e2e/app-dir/global-not-found/basic/app/global-not-found.tsx @@ -0,0 +1,13 @@ +import { Client } from './client' + +export default function GlobalNotFound() { + return ( + // html tag is different from actual page's layout + + +

global-not-found

+ + + + ) +} diff --git a/test/e2e/app-dir/global-not-found/basic/app/layout.tsx b/test/e2e/app-dir/global-not-found/basic/app/layout.tsx new file mode 100644 index 000000000000..dbce4ea8e3ae --- /dev/null +++ b/test/e2e/app-dir/global-not-found/basic/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/global-not-found/basic/app/page.tsx b/test/e2e/app-dir/global-not-found/basic/app/page.tsx new file mode 100644 index 000000000000..ff7159d9149f --- /dev/null +++ b/test/e2e/app-dir/global-not-found/basic/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/global-not-found/basic/global-not-found-basic.test.ts b/test/e2e/app-dir/global-not-found/basic/global-not-found-basic.test.ts new file mode 100644 index 000000000000..bded21893893 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/basic/global-not-found-basic.test.ts @@ -0,0 +1,44 @@ +import { nextTestSetup } from 'e2e-utils' +import { assertNoRedbox } from 'next-test-utils' + +describe('global-not-found - basic', () => { + const { next, isNextDev } = nextTestSetup({ + files: __dirname, + }) + + it('should render global-not-found for 404', async () => { + const browser = await next.browser('/does-not-exist') + if (isNextDev) { + await assertNoRedbox(browser) + } + + const errorTitle = await browser.elementByCss('#global-error-title').text() + expect(errorTitle).toBe('global-not-found') + const notFoundHtmlProp = await browser + .elementByCss('html') + .getAttribute('data-global-not-found') + expect(notFoundHtmlProp).toBe('true') + }) + + it('should ssr global-not-found for 404', async () => { + const $ = await next.render$('/does-not-exist') + const errorTitle = $('#global-error-title').text() + expect(errorTitle).toBe('global-not-found') + const notFoundHtmlProp = $('html').attr('data-global-not-found') + expect(notFoundHtmlProp).toBe('true') + }) + + it('should render not-found boundary when calling notFound() in a page', async () => { + const browser = await next.browser('/call-not-found') + // Still using the root layout + expect( + await browser.elementByCss('html').getAttribute('data-global-not-found') + ).toBeNull() + expect(await browser.elementByCss('html').getAttribute('lang')).toBe('en') + + // There's no not-found boundary in the root layout, show the default not-found.js + expect(await browser.elementByCss('body').text()).toBe( + '404\nThis page could not be found.' + ) + }) +}) diff --git a/test/e2e/app-dir/global-not-found/basic/next.config.js b/test/e2e/app-dir/global-not-found/basic/next.config.js new file mode 100644 index 000000000000..a198cf5a769a --- /dev/null +++ b/test/e2e/app-dir/global-not-found/basic/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + globalNotFound: true, + }, +} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/global-not-found/both-present/app/call-not-found/page.tsx b/test/e2e/app-dir/global-not-found/both-present/app/call-not-found/page.tsx new file mode 100644 index 000000000000..6f7be9458297 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/both-present/app/call-not-found/page.tsx @@ -0,0 +1,7 @@ +'use client' + +import { notFound } from 'next/navigation' + +export default function Page() { + notFound() +} diff --git a/test/e2e/app-dir/global-not-found/both-present/app/global-not-found.tsx b/test/e2e/app-dir/global-not-found/both-present/app/global-not-found.tsx new file mode 100644 index 000000000000..69bea87e0c5b --- /dev/null +++ b/test/e2e/app-dir/global-not-found/both-present/app/global-not-found.tsx @@ -0,0 +1,10 @@ +export default function GlobalNotFound() { + return ( + // html tag is different from actual page's layout + + +

global-not-found

+ + + ) +} diff --git a/test/e2e/app-dir/global-not-found/both-present/app/layout.tsx b/test/e2e/app-dir/global-not-found/both-present/app/layout.tsx new file mode 100644 index 000000000000..dbce4ea8e3ae --- /dev/null +++ b/test/e2e/app-dir/global-not-found/both-present/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/global-not-found/both-present/app/not-found.tsx b/test/e2e/app-dir/global-not-found/both-present/app/not-found.tsx new file mode 100644 index 000000000000..ea2bfb546029 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/both-present/app/not-found.tsx @@ -0,0 +1,3 @@ +export default function NotFound() { + return
not-found.js
+} diff --git a/test/e2e/app-dir/global-not-found/both-present/both-present.test.ts b/test/e2e/app-dir/global-not-found/both-present/both-present.test.ts new file mode 100644 index 000000000000..035dcd193c96 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/both-present/both-present.test.ts @@ -0,0 +1,31 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('global-not-found - both-present', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should render global-not-found for 404 routes', async () => { + const $ = await next.render$('/does-not-exist') + expect($('html').attr('data-global-not-found')).toBe('true') + expect($('#global-error-title').text()).toBe('global-not-found') + + const browser = await next.browser('/does-not-exist') + expect(await browser.elementByCss('#global-error-title').text()).toBe( + 'global-not-found' + ) + expect( + await browser.elementByCss('html').getAttribute('data-global-not-found') + ).toBe('true') + }) + + it('should render not-found boundary when calling notFound() in a page', async () => { + const browser = await next.browser('/call-not-found') + expect(await browser.elementByCss('#not-found-boundary').text()).toBe( + 'not-found.js' + ) + expect( + await browser.elementByCss('html').getAttribute('data-global-not-found') + ).toBeNull() + }) +}) diff --git a/test/e2e/app-dir/global-not-found/both-present/next.config.js b/test/e2e/app-dir/global-not-found/both-present/next.config.js new file mode 100644 index 000000000000..a198cf5a769a --- /dev/null +++ b/test/e2e/app-dir/global-not-found/both-present/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + globalNotFound: true, + }, +} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/global-not-found/no-root-layout/app/(bar)/bar/layout.tsx b/test/e2e/app-dir/global-not-found/no-root-layout/app/(bar)/bar/layout.tsx new file mode 100644 index 000000000000..a14e64fcd5e3 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/no-root-layout/app/(bar)/bar/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/global-not-found/no-root-layout/app/(bar)/bar/page.tsx b/test/e2e/app-dir/global-not-found/no-root-layout/app/(bar)/bar/page.tsx new file mode 100644 index 000000000000..51fdbbc49cc1 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/no-root-layout/app/(bar)/bar/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

bar

+} diff --git a/test/e2e/app-dir/global-not-found/no-root-layout/app/(foo)/foo/layout.tsx b/test/e2e/app-dir/global-not-found/no-root-layout/app/(foo)/foo/layout.tsx new file mode 100644 index 000000000000..a14e64fcd5e3 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/no-root-layout/app/(foo)/foo/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/global-not-found/no-root-layout/app/(foo)/foo/page.tsx b/test/e2e/app-dir/global-not-found/no-root-layout/app/(foo)/foo/page.tsx new file mode 100644 index 000000000000..7e900dc1e891 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/no-root-layout/app/(foo)/foo/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

foo

+} diff --git a/test/e2e/app-dir/global-not-found/no-root-layout/app/global-not-found.tsx b/test/e2e/app-dir/global-not-found/no-root-layout/app/global-not-found.tsx new file mode 100644 index 000000000000..69bea87e0c5b --- /dev/null +++ b/test/e2e/app-dir/global-not-found/no-root-layout/app/global-not-found.tsx @@ -0,0 +1,10 @@ +export default function GlobalNotFound() { + return ( + // html tag is different from actual page's layout + + +

global-not-found

+ + + ) +} diff --git a/test/e2e/app-dir/global-not-found/no-root-layout/next.config.js b/test/e2e/app-dir/global-not-found/no-root-layout/next.config.js new file mode 100644 index 000000000000..a198cf5a769a --- /dev/null +++ b/test/e2e/app-dir/global-not-found/no-root-layout/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + globalNotFound: true, + }, +} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/global-not-found/no-root-layout/no-root-layout.test.ts b/test/e2e/app-dir/global-not-found/no-root-layout/no-root-layout.test.ts new file mode 100644 index 000000000000..2f86336fcbe9 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/no-root-layout/no-root-layout.test.ts @@ -0,0 +1,30 @@ +import { nextTestSetup } from 'e2e-utils' +import { assertNoRedbox } from 'next-test-utils' + +describe('global-not-found - no-root-layout', () => { + const { next, isNextDev } = nextTestSetup({ + files: __dirname, + }) + + it('should render global-not-found for 404', async () => { + const browser = await next.browser('/does-not-exist') + if (isNextDev) { + await assertNoRedbox(browser) + } + + const errorTitle = await browser.elementByCss('#global-error-title').text() + expect(errorTitle).toBe('global-not-found') + const notFoundHtmlProp = await browser + .elementByCss('html') + .getAttribute('data-global-not-found') + expect(notFoundHtmlProp).toBe('true') + }) + + it('should ssr global-not-found for 404', async () => { + const $ = await next.render$('/does-not-exist') + const errorTitle = $('#global-error-title').text() + expect(errorTitle).toBe('global-not-found') + const notFoundHtmlProp = $('html').attr('data-global-not-found') + expect(notFoundHtmlProp).toBe('true') + }) +}) diff --git a/test/e2e/app-dir/global-not-found/not-present/app/call-not-found/page.tsx b/test/e2e/app-dir/global-not-found/not-present/app/call-not-found/page.tsx new file mode 100644 index 000000000000..6f7be9458297 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/not-present/app/call-not-found/page.tsx @@ -0,0 +1,7 @@ +'use client' + +import { notFound } from 'next/navigation' + +export default function Page() { + notFound() +} diff --git a/test/e2e/app-dir/global-not-found/not-present/app/layout.tsx b/test/e2e/app-dir/global-not-found/not-present/app/layout.tsx new file mode 100644 index 000000000000..a14e64fcd5e3 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/not-present/app/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/global-not-found/not-present/app/not-found.tsx b/test/e2e/app-dir/global-not-found/not-present/app/not-found.tsx new file mode 100644 index 000000000000..ea2bfb546029 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/not-present/app/not-found.tsx @@ -0,0 +1,3 @@ +export default function NotFound() { + return
not-found.js
+} diff --git a/test/e2e/app-dir/global-not-found/not-present/app/page.tsx b/test/e2e/app-dir/global-not-found/not-present/app/page.tsx new file mode 100644 index 000000000000..ff7159d9149f --- /dev/null +++ b/test/e2e/app-dir/global-not-found/not-present/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/global-not-found/not-present/next.config.js b/test/e2e/app-dir/global-not-found/not-present/next.config.js new file mode 100644 index 000000000000..a198cf5a769a --- /dev/null +++ b/test/e2e/app-dir/global-not-found/not-present/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + globalNotFound: true, + }, +} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/global-not-found/not-present/not-present.test.ts b/test/e2e/app-dir/global-not-found/not-present/not-present.test.ts new file mode 100644 index 000000000000..0c1ce88b61f9 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/not-present/not-present.test.ts @@ -0,0 +1,24 @@ +import { nextTestSetup } from 'e2e-utils' + +// TODO(global-not-found): remove this test when the feature is stable +describe('global-not-found - not-present', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should render default 404 when global-not-found is not defined but enabled', async () => { + const browser = await next.browser('/does-not-exist') + const bodyText = await browser.elementByCss('body').text() + expect(bodyText).toBe('404\nThis page could not be found.') + }) + + it('should render custom not-found.js boundary when global-not-found is not defined but enabled', async () => { + const browser = await next.browser('/call-not-found') + const bodyText = await browser.elementByCss('body').text() + const htmlLang = await browser.elementByCss('html').getAttribute('lang') + // Render the root layout + expect(htmlLang).toBe('en') + // Render the not-found.js boundary + expect(bodyText).toBe('not-found.js') + }) +}) From 72120847d2635a28e9ee9658778958b42d2c3bd9 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 10 May 2025 00:51:26 +0200 Subject: [PATCH 03/10] [not-found] support metadata exports of global-not-found (#78961) --- .../webpack/loaders/next-app-loader/index.ts | 3 +- .../next/src/server/app-render/app-render.tsx | 8 ++++- .../metadata/app/call-not-found/page.tsx | 7 +++++ .../metadata/app/global-not-found.tsx | 15 ++++++++++ .../global-not-found/metadata/app/icon.svg | 1 + .../global-not-found/metadata/app/layout.tsx | 11 +++++++ .../global-not-found/metadata/app/page.tsx | 3 ++ .../metadata/metadata.test.ts | 30 +++++++++++++++++++ .../global-not-found/metadata/next.config.js | 10 +++++++ 9 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 test/e2e/app-dir/global-not-found/metadata/app/call-not-found/page.tsx create mode 100644 test/e2e/app-dir/global-not-found/metadata/app/global-not-found.tsx create mode 100644 test/e2e/app-dir/global-not-found/metadata/app/icon.svg create mode 100644 test/e2e/app-dir/global-not-found/metadata/app/layout.tsx create mode 100644 test/e2e/app-dir/global-not-found/metadata/app/page.tsx create mode 100644 test/e2e/app-dir/global-not-found/metadata/metadata.test.ts create mode 100644 test/e2e/app-dir/global-not-found/metadata/next.config.js diff --git a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts index dfbe103fe87b..eef6c6daa407 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts @@ -218,8 +218,7 @@ async function createTreeCodeFromPath( let metadata: Awaited> = null const routerDirPath = `${appDirPrefix}${segmentPath}` - // For default not-found, don't traverse the directory to find metadata. - const resolvedRouteDir = isDefaultNotFound ? '' : resolveDir(routerDirPath) + const resolvedRouteDir = resolveDir(routerDirPath) if (resolvedRouteDir) { metadata = await createStaticMetadataFromRoute(resolvedRouteDir, { diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index e9b49811d7b2..4e468248394b 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -808,6 +808,7 @@ async function getRSCPayload( query ) const serveStreamingMetadata = !!ctx.renderOpts.serveStreamingMetadata + const hasGlobalNotFound = !!tree[2]['global-not-found'] const { ViewportTree, @@ -817,7 +818,12 @@ async function getRSCPayload( StreamingMetadataOutlet, } = createMetadataComponents({ tree, - errorType: is404 ? 'not-found' : undefined, + // When it's using global-not-found, metadata errorType is undefined, which will retrieve the + // metadata from the page. + // When it's using not-found, metadata errorType is 'not-found', which will retrieve the + // metadata from the not-found.js boundary. + // TODO: remove this condition and keep it undefined when global-not-found is stabilized. + errorType: is404 && !hasGlobalNotFound ? 'not-found' : undefined, parsedQuery: query, metadataContext: createTrackedMetadataContext( url.pathname, diff --git a/test/e2e/app-dir/global-not-found/metadata/app/call-not-found/page.tsx b/test/e2e/app-dir/global-not-found/metadata/app/call-not-found/page.tsx new file mode 100644 index 000000000000..6f7be9458297 --- /dev/null +++ b/test/e2e/app-dir/global-not-found/metadata/app/call-not-found/page.tsx @@ -0,0 +1,7 @@ +'use client' + +import { notFound } from 'next/navigation' + +export default function Page() { + notFound() +} diff --git a/test/e2e/app-dir/global-not-found/metadata/app/global-not-found.tsx b/test/e2e/app-dir/global-not-found/metadata/app/global-not-found.tsx new file mode 100644 index 000000000000..2655663a3bef --- /dev/null +++ b/test/e2e/app-dir/global-not-found/metadata/app/global-not-found.tsx @@ -0,0 +1,15 @@ +export default function GlobalNotFound() { + return ( + // html tag is different from actual page's layout + + +

global-not-found

+ + + ) +} + +export const metadata = { + title: 'global-not-found', + description: 'global-not-found description', +} diff --git a/test/e2e/app-dir/global-not-found/metadata/app/icon.svg b/test/e2e/app-dir/global-not-found/metadata/app/icon.svg new file mode 100644 index 000000000000..76bea97e3e9a --- /dev/null +++ b/test/e2e/app-dir/global-not-found/metadata/app/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/e2e/app-dir/global-not-found/metadata/app/layout.tsx b/test/e2e/app-dir/global-not-found/metadata/app/layout.tsx new file mode 100644 index 000000000000..dbce4ea8e3ae --- /dev/null +++ b/test/e2e/app-dir/global-not-found/metadata/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/global-not-found/metadata/app/page.tsx b/test/e2e/app-dir/global-not-found/metadata/app/page.tsx new file mode 100644 index 000000000000..ff7159d9149f --- /dev/null +++ b/test/e2e/app-dir/global-not-found/metadata/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/global-not-found/metadata/metadata.test.ts b/test/e2e/app-dir/global-not-found/metadata/metadata.test.ts new file mode 100644 index 000000000000..cc1f2ee4d9ef --- /dev/null +++ b/test/e2e/app-dir/global-not-found/metadata/metadata.test.ts @@ -0,0 +1,30 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('global-not-found - metadata', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should render metadata of global-not-found for 404', async () => { + // assert SSR metadata + const $ = await next.render$('/does-not-exist') + expect($('title').text()).toBe('global-not-found') + expect($('meta[name="description"]').attr('content')).toBe( + 'global-not-found description' + ) + // pick up static icon svg + expect($('link[rel="icon"]').attr('type')).toBe('image/svg+xml') + + // assert hydrated metadata + const browser = await next.browser('/does-not-exist') + const title = await browser.elementByCss('title') + const description = await browser.elementByCss('meta[name="description"]') + expect(await title.text()).toBe('global-not-found') + expect(await description.getAttribute('content')).toBe( + 'global-not-found description' + ) + // pick up static icon svg + const icon = await browser.elementByCss('link[rel="icon"]') + expect(await icon.getAttribute('type')).toBe('image/svg+xml') + }) +}) diff --git a/test/e2e/app-dir/global-not-found/metadata/next.config.js b/test/e2e/app-dir/global-not-found/metadata/next.config.js new file mode 100644 index 000000000000..a198cf5a769a --- /dev/null +++ b/test/e2e/app-dir/global-not-found/metadata/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + globalNotFound: true, + }, +} + +module.exports = nextConfig From 9bb904d10a9b1cadf33e7da99291be33191e4a0b Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Fri, 9 May 2025 16:01:45 -0700 Subject: [PATCH 04/10] chore(CI): Add a few more turbopack paths to labeler config (#78980) Looks like we never added this after the turbopack repo migration. --- .github/labeler.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/labeler.json b/.github/labeler.json index 690aefd45ab7..5bd4b4e34462 100644 --- a/.github/labeler.json +++ b/.github/labeler.json @@ -5,7 +5,7 @@ "examples": ["examples/**"], "Font (next/font)": ["**/*font*"], "tests": ["test/**", "bench/**"], - "Turbopack": ["crates/next-*/**"], + "Turbopack": ["crates/next-*/**", "crates/napi/**", "turbopack/**"], "created-by: Chrome Aurora": [ { "type": "user", "pattern": "atcastle" }, { "type": "user", "pattern": "devknoll" }, From c1ea3122574a6ad02ea3c8c89e619d7f42916258 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Fri, 9 May 2025 16:10:21 -0700 Subject: [PATCH 05/10] chore(CI): Add Rspack labeler config (#79004) This should automatically add the `Rspack` label in most cases, which should let us run the rspack CI in theses cases. There's an issue with the current CI config where the build-and-test job isn't triggered by label changes, so I'll need to make some changes there, but this is a start. --- .github/labeler.json | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/labeler.json b/.github/labeler.json index 5bd4b4e34462..e76ba629c991 100644 --- a/.github/labeler.json +++ b/.github/labeler.json @@ -1,3 +1,4 @@ +// This is a JSON5 file, processed by https://github.com/vercel/next-labeler-webhook/ { "labels": { "create-next-app": ["packages/create-next-app/**"], @@ -6,6 +7,37 @@ "Font (next/font)": ["**/*font*"], "tests": ["test/**", "bench/**"], "Turbopack": ["crates/next-*/**", "crates/napi/**", "turbopack/**"], + "Rspack": [ + // Usernames sourced from: https://rspack.dev/misc/team/core-team + // This label determines whether or not to run the Rspack CI alongside the normal CI jobs + { "type": "user", "pattern": "9aoy" }, + { "type": "user", "pattern": "ahabhgk" }, + { "type": "user", "pattern": "bvanjoi" }, + { "type": "user", "pattern": "chenjiahan" }, + { "type": "user", "pattern": "CPunisher" }, + { "type": "user", "pattern": "easy1090" }, + { "type": "user", "pattern": "fi3ework" }, + { "type": "user", "pattern": "GiveMe-A-Name" }, + { "type": "user", "pattern": "h-a-n-a" }, + { "type": "user", "pattern": "hardfist" }, + { "type": "user", "pattern": "inottn" }, + { "type": "user", "pattern": "jerrykingxyz" }, + { "type": "user", "pattern": "JSerFeng" }, + { "type": "user", "pattern": "lingyucoder" }, + { "type": "user", "pattern": "nyqykk" }, + { "type": "user", "pattern": "sanyuan0704" }, + { "type": "user", "pattern": "ScriptedAlchemy" }, + { "type": "user", "pattern": "SoonIter" }, + { "type": "user", "pattern": "stormslowly" }, + { "type": "user", "pattern": "SyMind" }, + { "type": "user", "pattern": "Timeless0911" }, + { "type": "user", "pattern": "valorkin" }, + { "type": "user", "pattern": "xc2" }, + { "type": "user", "pattern": "zackarychapple" }, + { "type": "user", "pattern": "zoolsher" }, + // these files are mostly webpack plugins/configs + "packages/next/src/build/**" + ], "created-by: Chrome Aurora": [ { "type": "user", "pattern": "atcastle" }, { "type": "user", "pattern": "devknoll" }, From 650523d85dedd132d6852ee3b46af5eebd5e09b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Fri, 9 May 2025 16:11:59 -0700 Subject: [PATCH 06/10] perf(turbopack): Remove needless clone of SWC AST (#79007) ### What? Part of the memory usage reduction task. ### Why? We don't need to clone the AST. --- .../crates/turbopack-ecmascript/src/references/esm/export.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs index 4af63d3d54a7..18c3bd4a96a1 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs @@ -680,7 +680,6 @@ impl EsmExports { Ok(CodeGeneration::new( vec![], [dynamic_stmt - .clone() .map(|stmt| CodeGenerationHoistedStmt::new("__turbopack_dynamic__".into(), stmt))] .into_iter() .flatten() @@ -689,7 +688,7 @@ impl EsmExports { "__turbopack_esm__".into(), quote!("$turbopack_esm($getters);" as Stmt, turbopack_esm: Expr = TURBOPACK_ESM.into(), - getters: Expr = getters.clone() + getters: Expr = getters ), )], )) From 13a867daec151e127acbda3ef2b61f2c5a3765ca Mon Sep 17 00:00:00 2001 From: vercel-release-bot Date: Fri, 9 May 2025 23:24:08 +0000 Subject: [PATCH 07/10] v15.4.0-canary.30 --- 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-rspack/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 16 ++++++++-------- 18 files changed, 33 insertions(+), 33 deletions(-) diff --git a/lerna.json b/lerna.json index 4a28676246ec..6227c16e2833 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "15.4.0-canary.29" + "version": "15.4.0-canary.30" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 0ad5e29d799e..79f81d38cfab 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 0755ff1b3a67..99367feb7a59 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/api-reference/config/eslint", "dependencies": { - "@next/eslint-plugin-next": "15.4.0-canary.29", + "@next/eslint-plugin-next": "15.4.0-canary.30", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 1901e65801c1..a4e614cfddfa 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": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/font/package.json b/packages/font/package.json index e8a718804145..240222d2458e 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "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 8467d22bbb73..140061281920 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 4456def82dbc..28a85596125b 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 27f4af6a3490..97c01d2791c8 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index af712a81a26c..828753c1e6dd 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index cd730d2c6ae5..00c371d02287 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "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 15fb38b6f6dd..4f51cbe5e325 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "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 db0b1f7fd980..b9b7a6542049 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index e402f7687642..3a14ad2a3ece 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 3a9c9b3c0b63..5d95eaa8df79 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index b2e797bf24f9..b0bf9631f515 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -100,7 +100,7 @@ ] }, "dependencies": { - "@next/env": "15.4.0-canary.29", + "@next/env": "15.4.0-canary.30", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -162,11 +162,11 @@ "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "15.4.0-canary.29", - "@next/polyfill-module": "15.4.0-canary.29", - "@next/polyfill-nomodule": "15.4.0-canary.29", - "@next/react-refresh-utils": "15.4.0-canary.29", - "@next/swc": "15.4.0-canary.29", + "@next/font": "15.4.0-canary.30", + "@next/polyfill-module": "15.4.0-canary.30", + "@next/polyfill-nomodule": "15.4.0-canary.30", + "@next/react-refresh-utils": "15.4.0-canary.30", + "@next/swc": "15.4.0-canary.30", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.41.2", "@storybook/addon-a11y": "8.6.0", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 72b3939ab0b2..abeaadaf8983 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": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index 9296f0faf8b4..706ffb3d86d7 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "15.4.0-canary.29", + "version": "15.4.0-canary.30", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "15.4.0-canary.29", + "next": "15.4.0-canary.30", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "5.8.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b707742f4158..84c094063c02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -824,7 +824,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 15.4.0-canary.29 + specifier: 15.4.0-canary.30 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.10.3 @@ -888,7 +888,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 15.4.0-canary.29 + specifier: 15.4.0-canary.30 version: link:../next-env '@swc/helpers': specifier: 0.5.15 @@ -1007,19 +1007,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 15.4.0-canary.29 + specifier: 15.4.0-canary.30 version: link:../font '@next/polyfill-module': - specifier: 15.4.0-canary.29 + specifier: 15.4.0-canary.30 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 15.4.0-canary.29 + specifier: 15.4.0-canary.30 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 15.4.0-canary.29 + specifier: 15.4.0-canary.30 version: link:../react-refresh-utils '@next/swc': - specifier: 15.4.0-canary.29 + specifier: 15.4.0-canary.30 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1704,7 +1704,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 15.4.0-canary.29 + specifier: 15.4.0-canary.30 version: link:../next outdent: specifier: 0.8.0 From cf1eea49e33d7130fe233afecca502fc13837f53 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Fri, 9 May 2025 16:33:42 -0700 Subject: [PATCH 08/10] chore(CI): Split next-form tests into smaller suites/files that can be run more-in-parallel (#78891) Same basic idea as https://github.com/vercel/next.js/pull/78787 This test suite has a lot of timeouts with rspack, which causes it to take a long time to run (though not quite long enough that it breaks the job). In general, large test suites are good to break up, because they benefit from parallelism. --- .../next-form/default/next-form.test.ts | 285 ---------------- .../basepath/app/forms/basic/page.tsx | 0 .../app/forms/button-formaction/page.tsx | 0 .../next-form/basepath/app/layout.tsx | 0 .../next-form/basepath/app/search/loading.tsx | 0 .../next-form/basepath/app/search/page.tsx | 0 .../basepath/next-form-basepath.test.ts | 0 .../next-form/basepath/next.config.js | 0 test/e2e/next-form/default/app-dir.test.ts | 3 + .../default/app/forms/basic/page.tsx | 0 .../button-formaction-unsupported/page.tsx | 0 .../app/forms/button-formaction/page.tsx | 0 .../default/app/forms/prefetch-false/page.tsx | 0 .../app/forms/with-file-input/page.tsx | 0 .../with-function/action-client/page.tsx | 0 .../action-server-closure/page.tsx | 0 .../with-function/action-server/page.tsx | 0 .../button-formaction-client/page.tsx | 0 .../button-formaction-server-closure/page.tsx | 0 .../button-formaction-server/page.tsx | 0 .../with-onsubmit-preventdefault/page.tsx | 0 .../default/app/forms/with-replace/page.tsx | 0 .../next-form/default/app/layout.tsx | 0 .../app/redirected-from-action/page.tsx | 0 .../next-form/default/app/search/loading.tsx | 0 .../next-form/default/app/search/page.tsx | 0 .../default/next-form-prefetch.test.ts | 9 +- test/e2e/next-form/default/pages-dir.test.ts | 3 + .../pages/pages-dir/forms/basic/index.tsx | 0 .../button-formaction-unsupported/index.tsx | 0 .../forms/button-formaction/index.tsx | 0 .../pages-dir/forms/with-file-input/index.tsx | 0 .../with-function/action-client/index.tsx | 0 .../button-formaction-client/index.tsx | 0 .../with-onsubmit-preventdefault/index.tsx | 0 .../pages-dir/forms/with-replace/index.tsx | 0 .../redirected-from-action/index.tsx | 0 .../default/pages/pages-dir/search/index.tsx | 0 .../next-form/default/shared-tests.util.ts | 303 ++++++++++++++++++ 39 files changed, 316 insertions(+), 287 deletions(-) delete mode 100644 test/e2e/app-dir/next-form/default/next-form.test.ts rename test/e2e/{app-dir => }/next-form/basepath/app/forms/basic/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/basepath/app/forms/button-formaction/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/basepath/app/layout.tsx (100%) rename test/e2e/{app-dir => }/next-form/basepath/app/search/loading.tsx (100%) rename test/e2e/{app-dir => }/next-form/basepath/app/search/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/basepath/next-form-basepath.test.ts (100%) rename test/e2e/{app-dir => }/next-form/basepath/next.config.js (100%) create mode 100644 test/e2e/next-form/default/app-dir.test.ts rename test/e2e/{app-dir => }/next-form/default/app/forms/basic/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/button-formaction-unsupported/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/button-formaction/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/prefetch-false/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/with-file-input/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/with-function/action-client/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/with-function/action-server-closure/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/with-function/action-server/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/with-function/button-formaction-client/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/with-function/button-formaction-server-closure/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/with-function/button-formaction-server/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/with-onsubmit-preventdefault/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/forms/with-replace/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/layout.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/redirected-from-action/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/search/loading.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/app/search/page.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/next-form-prefetch.test.ts (97%) create mode 100644 test/e2e/next-form/default/pages-dir.test.ts rename test/e2e/{app-dir => }/next-form/default/pages/pages-dir/forms/basic/index.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/pages/pages-dir/forms/button-formaction-unsupported/index.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/pages/pages-dir/forms/button-formaction/index.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/pages/pages-dir/forms/with-file-input/index.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/pages/pages-dir/forms/with-function/action-client/index.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/pages/pages-dir/forms/with-function/button-formaction-client/index.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/pages/pages-dir/forms/with-onsubmit-preventdefault/index.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/pages/pages-dir/forms/with-replace/index.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/pages/pages-dir/redirected-from-action/index.tsx (100%) rename test/e2e/{app-dir => }/next-form/default/pages/pages-dir/search/index.tsx (100%) create mode 100644 test/e2e/next-form/default/shared-tests.util.ts diff --git a/test/e2e/app-dir/next-form/default/next-form.test.ts b/test/e2e/app-dir/next-form/default/next-form.test.ts deleted file mode 100644 index 7638a47c2420..000000000000 --- a/test/e2e/app-dir/next-form/default/next-form.test.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { nextTestSetup } from 'e2e-utils' -import { Playwright } from '../../../../lib/next-webdriver' - -const isReact18 = parseInt(process.env.NEXT_TEST_REACT_VERSION) === 18 - -describe.each(['app', 'pages'])('%s dir - form', (type) => { - const { next, isNextDev } = nextTestSetup({ - files: __dirname, - }) - - const isAppDir = type === 'app' - const pathPrefix = isAppDir ? '' : '/pages-dir' - - it( - 'should soft-navigate on submit' + - (isAppDir ? ' and show the prefetched loading state' : ''), - async () => { - const session = await next.browser(pathPrefix + '/forms/basic') - const navigationTracker = await trackMpaNavs(session) - - const searchInput = await session.elementByCss('input[name="query"]') - await searchInput.fill('my search') - - const submitButton = await session.elementByCss('[type="submit"]') - await submitButton.click() - - if (isAppDir) { - // we should have prefetched a loading state, so it should be displayed - await session.waitForElementByCss('#loading') - } - - const result = await session.waitForElementByCss('#search-results').text() - expect(result).toMatch(/query: "my search"/) - - expect(await navigationTracker.didMpaNavigate()).toBe(false) - } - ) - - it('should soft-navigate to the formAction url of the submitter', async () => { - const session = await next.browser(pathPrefix + '/forms/button-formaction') - const navigationTracker = await trackMpaNavs(session) - - const searchInput = await session.elementByCss('input[name="query"]') - await searchInput.fill('my search') - - const submitButton = await session.elementByCss('[type="submit"]') - await submitButton.click() - - // we didn't prefetch a loading state, so we don't know if it'll be displayed - // TODO: is this correct? it'll probably be there in dev, but what about prod? - // await session.waitForElementByCss('#loading') - - const result = await session.waitForElementByCss('#search-results').text() - expect(result).toMatch(/query: "my search"/) - - expect(await navigationTracker.didMpaNavigate()).toBe(false) - }) - - // `
` is only supported in React 19.x - ;(isReact18 ? describe.skip : describe)('functions passed to action', () => { - it.each([ - { - name: 'client action', - path: '/forms/with-function/action-client', - }, - ...(isAppDir - ? [ - { - name: 'server action', - path: '/forms/with-function/action-server', - }, - { - name: 'server action (closure)', - path: '/forms/with-function/action-server-closure', - }, - ] - : []), - ])('runs $name', async ({ path }) => { - const session = await next.browser(pathPrefix + path) - const navigationTracker = await trackMpaNavs(session) // actions should not MPA-navigate either. - - const searchInput = await session.elementByCss('input[name="query"]') - await searchInput.fill('will not be a search') - - const submitButton = await session.elementByCss('[type="submit"]') - await submitButton.click() - - const result = await session - .waitForElementByCss('#redirected-results') - .text() - expect(result).toMatch(/query: "will not be a search"/) - - expect(await navigationTracker.didMpaNavigate()).toBe(false) - }) - }) - - // `