@thomas.f/prnext 0.1.18-alpha → 0.1.20-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +2 -0
  2. package/build/actions.mjs +26 -14
  3. package/build/app.mjs +90 -101
  4. package/build/build-lock.mjs +2 -71
  5. package/build/build-retention.mjs +119 -0
  6. package/build/bundle-runtime.mjs +9 -8
  7. package/build/cache-command.mjs +31 -0
  8. package/build/check.mjs +41 -3
  9. package/build/compilation-cache.mjs +32 -12
  10. package/build/compilation-storage.mjs +190 -0
  11. package/build/compiler.mjs +8 -7
  12. package/build/config.mjs +25 -2
  13. package/build/create.mjs +1 -1
  14. package/build/css-modules.mjs +4 -0
  15. package/build/dev-action-graph.mjs +21 -0
  16. package/build/dev-css-assets.mjs +173 -0
  17. package/build/dev-css-update.mjs +14 -12
  18. package/build/dev-lazy-build.mjs +77 -0
  19. package/build/dynamic.mjs +13 -6
  20. package/build/font-cache.mjs +9 -6
  21. package/build/font-files.mjs +1 -1
  22. package/build/incremental.mjs +112 -18
  23. package/build/index.mjs +61 -15
  24. package/build/lazy-dynamic-imports.mjs +137 -0
  25. package/build/migrate.mjs +4 -4
  26. package/build/migration-audit.mjs +19 -31
  27. package/build/migration-imports.mjs +245 -0
  28. package/build/migration-scenarios-runner.mjs +73 -0
  29. package/build/migration-scenarios.mjs +72 -0
  30. package/build/native-discovery.mjs +65 -5
  31. package/build/native-packages.mjs +2 -0
  32. package/build/native-ppr.mjs +32 -0
  33. package/build/npm-packages.mjs +1 -0
  34. package/build/og.mjs +121 -1
  35. package/build/parallel-plan.mjs +55 -0
  36. package/build/persistent-cache.mjs +29 -24
  37. package/build/production-builder.mjs +1 -1
  38. package/build/production-check.mjs +34 -0
  39. package/build/react-compiler-report.mjs +31 -8
  40. package/build/react-compiler.mjs +101 -10
  41. package/build/server-config.mjs +35 -0
  42. package/build/source-analysis.mjs +54 -4
  43. package/build/standalone.mjs +21 -3
  44. package/build/static-adapter.mjs +7 -3
  45. package/build/transform.mjs +38 -6
  46. package/build/typescript-dev.mjs +3 -1
  47. package/build/webpack-graph.mjs +13 -3
  48. package/cli.mjs +24 -6
  49. package/compat/app-dynamic.cjs +12 -1
  50. package/compat/app-layout-context.cjs +7 -3
  51. package/compat/cache-handlers.cjs +3 -1
  52. package/compat/flight-delta.cjs +50 -13
  53. package/compat/index.d.ts +24 -1
  54. package/compat/link.cjs +2 -2
  55. package/compat/navigation-authorization.cjs +8 -1
  56. package/docs/ai/compatibility.md +187 -7
  57. package/docs/ai/deployment.md +141 -0
  58. package/docs/ai/quickstart.md +5 -0
  59. package/docs/ai/routing.md +11 -0
  60. package/docs/ai/troubleshooting.md +79 -0
  61. package/package.json +26 -11
  62. package/release/fontkit.mjs +31 -0
  63. package/release/verify.mjs +3 -0
  64. package/runtime/api.mjs +5 -2
  65. package/runtime/app-client.mjs +254 -53
  66. package/runtime/app-partial.mjs +2 -10
  67. package/runtime/app-prefetch.mjs +2 -2
  68. package/runtime/app-render.mjs +11 -7
  69. package/runtime/app-stream-channel.mjs +3 -2
  70. package/runtime/app-styles.mjs +22 -4
  71. package/runtime/atomic-file.mjs +27 -0
  72. package/runtime/cache-components-rsc.mjs +3 -1
  73. package/runtime/compile-cache-retention.mjs +53 -0
  74. package/runtime/compile-cache.mjs +28 -1
  75. package/runtime/dev-client.mjs +13 -5
  76. package/runtime/dev-file-changes.mjs +123 -8
  77. package/runtime/dev-gateway.mjs +3 -2
  78. package/runtime/dev-lazy-imports.mjs +121 -0
  79. package/runtime/dev-lazy-staging.mjs +14 -0
  80. package/runtime/dev-route-selection.mjs +64 -0
  81. package/runtime/dev-runtime-pool.mjs +3 -2
  82. package/runtime/dev-runtime-spare.mjs +1 -1
  83. package/runtime/dev-session-retention.mjs +154 -0
  84. package/runtime/dev-style-retention.mjs +15 -0
  85. package/runtime/development.mjs +86 -26
  86. package/runtime/env.mjs +9 -0
  87. package/runtime/flight-delta.mjs +59 -9
  88. package/runtime/fs.mjs +15 -8
  89. package/runtime/lazy-import-client.mjs +38 -0
  90. package/runtime/native-ppr.mjs +35 -0
  91. package/runtime/og/NOTICE.txt +1 -1
  92. package/runtime/og/index.node.mjs +76 -31216
  93. package/runtime/og/upstream.json +115 -0
  94. package/runtime/persistent/client.mjs +4 -2
  95. package/runtime/persistent/daemon.mjs +3 -11
  96. package/runtime/persistent/health.mjs +58 -0
  97. package/runtime/project-lock.mjs +71 -0
  98. package/runtime/request-transport.mjs +112 -32
  99. package/runtime/storage-path.mjs +36 -0
  100. package/runtime/trace.mjs +44 -4
  101. package/runtime/worker.mjs +9 -4
  102. package/vendor/fontkit/NOTICE.txt +5 -0
  103. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  104. package/vendor/fontkit/index.mjs +3 -0
  105. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  106. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  107. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  108. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  109. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  110. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  111. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  112. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  113. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  114. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  115. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  116. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  117. package/vendor/fontkit/upstream.json +215 -0
  118. package/vendor/nft/LICENSE +7 -0
  119. package/vendor/nft/README.md +16 -0
  120. package/vendor/nft/out/analyze.d.ts +8 -0
  121. package/vendor/nft/out/analyze.js +1216 -0
  122. package/vendor/nft/out/analyze.js.map +1 -0
  123. package/vendor/nft/out/cli.d.ts +2 -0
  124. package/vendor/nft/out/cli.js +94 -0
  125. package/vendor/nft/out/cli.js.map +1 -0
  126. package/vendor/nft/out/fs.d.ts +22 -0
  127. package/vendor/nft/out/fs.js +106 -0
  128. package/vendor/nft/out/fs.js.map +1 -0
  129. package/vendor/nft/out/index.d.ts +4 -0
  130. package/vendor/nft/out/index.js +25 -0
  131. package/vendor/nft/out/index.js.map +1 -0
  132. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  133. package/vendor/nft/out/node-file-trace.js +380 -0
  134. package/vendor/nft/out/node-file-trace.js.map +1 -0
  135. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  136. package/vendor/nft/out/resolve-dependency.js +427 -0
  137. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  138. package/vendor/nft/out/types.d.ts +66 -0
  139. package/vendor/nft/out/types.js +2 -0
  140. package/vendor/nft/out/types.js.map +1 -0
  141. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  142. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  143. package/vendor/nft/out/utils/binary-locators.js +88 -0
  144. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  145. package/vendor/nft/out/utils/get-package-base.js +30 -0
  146. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  147. package/vendor/nft/out/utils/interop-require.js +24 -0
  148. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  149. package/vendor/nft/out/utils/node-version.js +10 -0
  150. package/vendor/nft/out/utils/node-version.js.map +1 -0
  151. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  152. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  153. package/vendor/nft/out/utils/special-cases.js +347 -0
  154. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  155. package/vendor/nft/out/utils/static-eval.js +579 -0
  156. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  157. package/vendor/nft/out/utils/types.js +2 -0
  158. package/vendor/nft/out/utils/types.js.map +1 -0
  159. package/vendor/nft/out/utils/wrappers.js +659 -0
  160. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  161. package/vendor/nft/package.json +4 -0
  162. package/vendor/nft/upstream.json +51 -0
@@ -16,7 +16,7 @@ import {withSourceMaps,configureSourceMaps,inlineSourceMap} from './source-maps.
16
16
  import { optimizeImportsPlugins } from './optimize-imports.mjs';
17
17
  import {recordBuildAnalysis} from './analyze.mjs';
18
18
  import {recordDevDependencies,developmentDependencies,developmentStylePlans,developmentStyleDependencies,developmentModules} from './dev-dependencies.mjs';
19
- import {normalizeReactCompilerConfig,REACT_COMPILER_VERSION} from './react-compiler.mjs';
19
+ import {normalizeReactCompilerConfig,reactCompilerVersion} from './react-compiler.mjs';
20
20
  import {withReactCompilerReporting,configureReactCompilerReporting,withReactCompilerGraph,recordReactCompilerGraph} from './react-compiler-report.mjs';
21
21
  import {withCompilationGraphCacheReporting,cachedGraphBuild} from './compilation-cache.mjs';
22
22
  export {reactCompilerReport} from './react-compiler-report.mjs';
@@ -27,7 +27,7 @@ const compilation = new AsyncLocalStorage();
27
27
  export const withCompilation = callback => withCompilationGraphCacheReporting(()=>withReactCompilerReporting(()=>withSourceMaps(()=>compilation.run({ targets: new Map(), loaded: new Map() }, callback))));
28
28
  export function configureCompiler(options) {
29
29
  Object.assign(compilation.getStore() || {}, options);configureSourceMaps(options.dev || options.config?.productionBrowserSourceMaps);
30
- configureReactCompilerReporting({...options,config:{reactCompiler:options.config?.reactCompiler ? normalizeReactCompilerConfig(options.config.reactCompiler) : false},compilerVersion:REACT_COMPILER_VERSION});
30
+ configureReactCompilerReporting({...options,config:{reactCompiler:options.config?.reactCompiler ? normalizeReactCompilerConfig(options.config.reactCompiler) : false},compilerVersion:reactCompilerVersion(options.config?.reactCompiler,options.root)});
31
31
  }
32
32
  export const buildAnalysisRecords=()=>compilation.getStore()?.analysis;
33
33
  export const buildAnalysisRoutes=()=>compilation.getStore()?.analysisRoutes;
@@ -145,18 +145,19 @@ export async function validateCompilerConfig(config, root, {dev=false,edge=false
145
145
  const state={config,root,dev,buildId:'prnext-check',targets:new Map()};
146
146
  for (const target of ['node','browser',...(edge?['edge']:[])]) await targetConfig(state,target);
147
147
  }
148
- export async function build(options) {
148
+ export async function build(options, { role = 'main' } = {}) {
149
+ if (role !== 'main' && role !== 'styles') throw new TypeError('Invalid compiler graph role');
149
150
  const state=compilation.getStore();
150
151
  const target=options.define?.['process.env.NEXT_RUNTIME']==='"edge"'?'edge':options.platform==='browser'?'browser':'node';
151
152
  return withReactCompilerGraph(target,async()=>{
152
- const result=await compileGraph(options);
153
+ const result=await compileGraph(options,role);
153
154
  recordReactCompilerGraph(result);
154
155
  recordBuildAnalysis(state,options,result);
155
156
  recordDevDependencies(state,options,result);
156
157
  return result;
157
158
  });
158
159
  }
159
- async function compileGraph(options) {
160
+ async function compileGraph(options,role) {
160
161
  const state = compilation.getStore();
161
162
  // The transform is captured by the framework's per-build callbacks. Give both
162
163
  // esbuild and the webpack bridge a new graph identity when its options change,
@@ -164,7 +165,7 @@ async function compileGraph(options) {
164
165
  // no hooks or runtime output and leaves the opt-out graph exactly as before.
165
166
  if ((options.plugins || []).some(plugin => framework.test(plugin.name)) && state?.config?.reactCompiler) {
166
167
  const settings=normalizeReactCompilerConfig(state.config.reactCompiler);
167
- const fingerprint=createHash('sha256').update(JSON.stringify([REACT_COMPILER_VERSION,settings])).digest('hex').slice(0,24);
168
+ const fingerprint=createHash('sha256').update(JSON.stringify([reactCompilerVersion(settings,state.root),settings])).digest('hex').slice(0,24);
168
169
  options={...options,plugins:[...options.plugins,{name:'prnext-react-compiler-'+fingerprint,setup(){}}]};
169
170
  }
170
171
  if ((options.plugins || []).some(plugin => framework.test(plugin.name))) options = { ...options, plugins: optimizeImportsPlugins(options.plugins, state?.config?.experimental?.optimizePackageImports) };
@@ -183,7 +184,7 @@ async function compileGraph(options) {
183
184
  // Runtime vendor bundles are framework-owned, not project webpack targets.
184
185
  const projectGraph = (options.plugins || []).some(plugin => framework.test(plugin.name));
185
186
  if (projectGraph && state?.tsconfig) options = { ...options, tsconfig: state.tsconfig };
186
- const nativeCompile = input => state?.incremental && (projectGraph || !input.plugins?.length) ? incrementalBuild(input,{root:state.root,stage:state.stage,group:projectGraph?'project':'auxiliary',run:callback=>compilation.run(state,callback)}) : esbuild(input);
187
+ const nativeCompile = input => state?.incremental && (projectGraph || !input.plugins?.length) ? incrementalBuild(input,{root:state.root,stage:state.stage,group:projectGraph?'project':'auxiliary',role:projectGraph?role:'auxiliary',run:callback=>compilation.run(state,callback)}) : esbuild(input);
187
188
  const compile = input => state?.root && state?.stage ? cachedGraphBuild(input,state,nativeCompile) : nativeCompile(input);
188
189
  if (!(state?.config?.webpack || Object.keys(state?.config?.turbopack?.rules || {}).length) || !projectGraph) return compile(options);
189
190
  const target = options.define?.['process.env.NEXT_RUNTIME'] === '"edge"' ? 'edge' : options.platform === 'browser' ? 'browser' : 'node';
package/build/config.mjs CHANGED
@@ -25,6 +25,8 @@ import {validatePrerenderConfig} from './prerender-config.mjs';
25
25
  import {validateHtmlOnly} from './html-only-config.mjs';
26
26
  import { validateProxyBodySize } from './proxy-body-config.mjs';
27
27
  import { normalizeReactCompilerConfig } from './react-compiler.mjs';
28
+ import {validateCompilationCache} from './compilation-storage.mjs';
29
+ import {validateServerConfig} from './server-config.mjs';
28
30
 
29
31
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
30
32
  const configName = /^(?:next|prnext)\.config\.(?:js|mjs|cjs|ts)$/;
@@ -37,6 +39,8 @@ supported.add('workerMemory');
37
39
  supported.add('workerRetirement');
38
40
  supported.add('workerPreload');
39
41
  supported.add('deploymentRetention');
42
+ supported.add('compilationCache');
43
+ supported.add('server');
40
44
  supported.add('prerender');
41
45
  supported.add('htmlOnly');
42
46
  supported.add('cssEngine');
@@ -107,6 +111,7 @@ export function validateProjectConfig(input, filename = 'project config') {
107
111
  config.workerRetirement = validateWorkerRetirement(config.workerRetirement);
108
112
  config.workerPreload=validateWorkerPreload(config.workerPreload);
109
113
  config.deploymentRetention=validateDeploymentRetention(config.deploymentRetention);
114
+ config.compilationCache=validateCompilationCache(config.compilationCache);
110
115
  config.prerender=validatePrerenderConfig(config.prerender);
111
116
  config.htmlOnly=validateHtmlOnly(config.htmlOnly);
112
117
  config.cssEngine ??= 'auto';
@@ -127,6 +132,7 @@ export function validateProjectConfig(input, filename = 'project config') {
127
132
  }
128
133
  if (config.webpack !== undefined && typeof config.webpack !== 'function') throw new TypeError('webpack must be a configuration function');
129
134
  config.turbopack = validateTurbopack(config.turbopack);
135
+ config.server = validateServerConfig(config.server);
130
136
  config.i18n = validateI18n(config.i18n);
131
137
  if (config.i18n && config.output === 'export') throw new Error("i18n and output: 'export' cannot be combined; use explicit locale routes for static exports");
132
138
  validateStandaloneConfig(config);
@@ -164,11 +170,28 @@ export function validateProjectConfig(input, filename = 'project config') {
164
170
  if (config.sassOptions !== undefined && (!config.sassOptions || typeof config.sassOptions !== 'object' || Array.isArray(config.sassOptions))) throw new Error(`${filename}: sassOptions must be an object.`);
165
171
  if (!config.experimental || typeof config.experimental !== 'object' || Array.isArray(config.experimental)) throw new Error(`${filename}: experimental must be an object.`);
166
172
  for (const key of Object.keys(config.experimental)) {
167
- if (!['nextScriptWorkers', 'serverActions', 'optimizePackageImports', 'proxyClientMaxBodySize'].includes(key)) throw new Error(`${filename}: experimental.${key} is not supported by PRNext. It cannot be silently ignored.`);
173
+ if (!['nextScriptWorkers', 'serverActions', 'optimizePackageImports', 'proxyClientMaxBodySize', 'lazyDynamicImports', 'streamingRequestBodies', 'nativePprShell', 'parallelBuilds'].includes(key)) throw new Error(`${filename}: experimental.${key} is not supported by PRNext. It cannot be silently ignored.`);
168
174
  }
169
175
  const nextScriptWorkers = config.experimental.nextScriptWorkers === undefined ? false : config.experimental.nextScriptWorkers;
170
176
  if (typeof nextScriptWorkers !== 'boolean') throw new Error(`${filename}: experimental.nextScriptWorkers must be a boolean.`);
171
- config.experimental = { nextScriptWorkers, serverActions: validateServerActions(config.experimental.serverActions), optimizePackageImports: validateOptimizePackageImports(config.experimental.optimizePackageImports), proxyClientMaxBodySize: validateProxyBodySize(config.experimental.proxyClientMaxBodySize) };
177
+ const lazyDynamicImports=config.experimental.lazyDynamicImports===undefined?false:config.experimental.lazyDynamicImports;
178
+ if(typeof lazyDynamicImports!=='boolean')throw new Error(`${filename}: experimental.lazyDynamicImports must be a boolean.`);
179
+ const streamingRequestBodies = config.experimental.streamingRequestBodies === undefined ? false : config.experimental.streamingRequestBodies;
180
+ const nativePprShell = config.experimental.nativePprShell === undefined ? false : config.experimental.nativePprShell;
181
+ for (const [key, value] of Object.entries({streamingRequestBodies, nativePprShell})) {
182
+ if (typeof value !== 'boolean') throw new Error(`${filename}: experimental.${key} must be a boolean.`);
183
+ }
184
+ if (nativePprShell && !config.cacheComponents) throw new Error(`${filename}: experimental.nativePprShell requires cacheComponents: true.`);
185
+ let parallelBuilds = config.experimental.parallelBuilds === undefined ? false : config.experimental.parallelBuilds;
186
+ if (parallelBuilds !== false) {
187
+ if (parallelBuilds === true) parallelBuilds = {};
188
+ if (!parallelBuilds || typeof parallelBuilds !== 'object' || Array.isArray(parallelBuilds)) throw new Error(`${filename}: experimental.parallelBuilds must be a boolean or an object.`);
189
+ for (const key of Object.keys(parallelBuilds)) if (!['maxWorkers', 'maxMemoryMb'].includes(key)) throw new Error(`${filename}: unknown parallelBuilds option ${key}.`);
190
+ parallelBuilds = {maxWorkers: parallelBuilds.maxWorkers === undefined ? 4 : parallelBuilds.maxWorkers, maxMemoryMb: parallelBuilds.maxMemoryMb === undefined ? 1024 : parallelBuilds.maxMemoryMb};
191
+ if (!Number.isInteger(parallelBuilds.maxWorkers) || parallelBuilds.maxWorkers < 1 || parallelBuilds.maxWorkers > 8) throw new Error(`${filename}: parallelBuilds.maxWorkers must be between 1 and 8.`);
192
+ if (!Number.isInteger(parallelBuilds.maxMemoryMb) || parallelBuilds.maxMemoryMb < 256 || parallelBuilds.maxMemoryMb > 8192) throw new Error(`${filename}: parallelBuilds.maxMemoryMb must be between 256 and 8192.`);
193
+ }
194
+ config.experimental = { nextScriptWorkers, lazyDynamicImports, streamingRequestBodies, nativePprShell, parallelBuilds, serverActions: validateServerActions(config.experimental.serverActions), optimizePackageImports: validateOptimizePackageImports(config.experimental.optimizePackageImports), proxyClientMaxBodySize: validateProxyBodySize(config.experimental.proxyClientMaxBodySize) };
172
195
  for (const key of ['basePath', 'assetPrefix']) {
173
196
  if (typeof config[key] !== 'string' || Buffer.byteLength(config[key]) > 4096) throw new Error(`${filename}: ${key} must be a string of at most 4096 bytes.`);
174
197
  }
package/build/create.mjs CHANGED
@@ -129,7 +129,7 @@ h1 { font-size: clamp(2.5rem, 8vw, 4rem); letter-spacing: -.04em; }
129
129
  p { line-height: 1.6; }
130
130
  code { font-size: .95em; }
131
131
  `],
132
- ['.gitignore', 'node_modules/\n.prnext/\n.prnext-types/\n.prnext-dev/\n.prnext-cache/\n.env*\n!.env.example\n*.tsbuildinfo\n'],
132
+ ['.gitignore', 'node_modules/\n.prnext/\n.prnext-types/\n.prnext-dev/\n.prnext-cache/\n.prnext-gc/\n.env*\n!.env.example\n*.tsbuildinfo\n'],
133
133
  ['README.md', `# ${name}\n\nA minimal PRNext application.\n\nRun \`npm install\` if dependencies have not been installed, then:\n\n\`\`\`sh\nnpm run dev\n\`\`\`\n\nEdit \`app/page.${extension}\`. Use the local URL printed by the server.\n\n\`\`\`sh\nnpm run check\n${typescript ? 'npm run typecheck\n' : ''}npm run build\nnpm start\n\`\`\`\n\nUse another port with \`npm run dev -- --port 3001\` when needed.\n\n[PRNext documentation](https://prnext.dev) · Installed AI guide: \`npx --no-install prnext docs --ai\`.\nPRNext is experimental; Next.js compatibility is partial.\n`],
134
134
  ['AGENTS.md', `# PRNext project\n\nThis application uses \`@thomas.f/prnext\`. Before changing routing, configuration, data fetching or deployment, read the installed guide at \`node_modules/@thomas.f/prnext/AI.md\` or run:\n\n\`\`\`sh\nnpx --no-install prnext docs --ai\nnpx --no-install prnext docs --ai --list\n\`\`\`\n\nRead only the relevant topic with \`npx --no-install prnext docs --ai <topic>\`. The installed documentation is version-specific; do not assume full Next.js compatibility. Preserve the project's package manager and lockfile.\n`],
135
135
  ]);
@@ -138,6 +138,10 @@ export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/ass
138
138
 
139
139
  return {
140
140
  compile:get,
141
+ async parallelSafe() {
142
+ eligibility ??= cacheEligible();
143
+ return typeof sassOptions?.additionalData !== 'function' && await eligibility;
144
+ },
141
145
  async mappings(){return Promise.all([...cache].map(async([file,result])=>({file,mapping:(await result).mapping,watchFiles:(await result).watchFiles})));},
142
146
  plugin(browser, { emitStyles = () => true } = {}) {
143
147
  return {
@@ -0,0 +1,21 @@
1
+ import path from 'node:path';
2
+
3
+ // Build-private provenance, never Action arguments, source text or encryption
4
+ // material. Unknown/external owners deliberately disable selective refresh.
5
+ export function developmentActionMetadata(root, actions) {
6
+ const records = [], identities = new Set(); let bytes = 0;
7
+ for (const action of actions) {
8
+ const file = path.relative(root, action.file).replaceAll(path.sep, '/');
9
+ const runtimes = [...(action.runtimes?.size ? action.runtimes : ['nodejs'])].sort();
10
+ const record = { id: action.id, file, binding: action.binding, runtimes };
11
+ const identity = file + '\0' + action.binding;
12
+ if (!/^[a-f0-9]{64}$/.test(action.id) || !file || file.startsWith('../') || path.isAbsolute(file) || /[:?\0]/.test(file) || file.startsWith('node_modules/') ||
13
+ typeof action.binding !== 'string' || !action.binding || action.binding.length > 1024 || identities.has(identity) ||
14
+ runtimes.some(runtime => !['nodejs', 'edge'].includes(runtime)) || records.length >= 10000 ||
15
+ (bytes += Buffer.byteLength(JSON.stringify(record))) > 1024 * 1024)
16
+ return { version: 1, complete: false, actions: [] };
17
+ identities.add(identity); records.push(record);
18
+ }
19
+ records.sort((a, b) => a.file.localeCompare(b.file) || a.binding.localeCompare(b.binding));
20
+ return { version: 1, complete: true, actions: records };
21
+ }
@@ -0,0 +1,173 @@
1
+ import { copyFile, link, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { rename } from '../runtime/fs.mjs';
5
+
6
+ export const STYLE_OVERLAY_LIMITS = Object.freeze({ files: 4096, bytes: 64 * 1024 * 1024, catalogueBytes: 4 * 1024 * 1024, references: 16384 });
7
+ const digest = data => createHash('sha256').update(data).digest('hex');
8
+ const filename = name => typeof name === 'string' && name.length > 0 && name.length <= 255 && name !== '.' && name !== '..'
9
+ && !/[\\/\x00-\x1f<>:"|?*#]/.test(name) && path.basename(name) === name;
10
+ const inside = (root, file) => { const relative = path.relative(root, file); return relative !== '' && !path.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..' + path.sep); };
11
+
12
+ export class StyleOverlayFallback extends Error {
13
+ constructor(code) { super(code); this.code = code; }
14
+ }
15
+ const fallback = code => { throw new StyleOverlayFallback(code); };
16
+
17
+ function assetsFor(bundles) {
18
+ const assets = new Map();
19
+ let references = 0, bytes = 0;
20
+ for (const files of bundles.values()) for (const file of files) {
21
+ if (++references > STYLE_OVERLAY_LIMITS.references) fallback('style-overlay-budget');
22
+ if (!file || !filename(file.name) || !Number.isSafeInteger(file.bytes) || file.bytes < 0 || file.bytes > STYLE_OVERLAY_LIMITS.bytes || !/^[a-f0-9]{64}$/.test(file.sha256)) fallback('invalid-style-overlay');
23
+ const previous = assets.get(file.name);
24
+ if (previous && (previous.bytes !== file.bytes || previous.sha256 !== file.sha256)) fallback('conflicting-style-overlay');
25
+ if (!previous) {
26
+ assets.set(file.name, file);
27
+ bytes += file.bytes;
28
+ if (assets.size > STYLE_OVERLAY_LIMITS.files || bytes > STYLE_OVERLAY_LIMITS.bytes) fallback('style-overlay-budget');
29
+ }
30
+ }
31
+ return assets;
32
+ }
33
+
34
+ function catalogue(previous, plans) {
35
+ const bundles = new Map();
36
+ if (!previous) return bundles;
37
+ if (previous.version !== 1 || typeof previous.baseDirectory !== 'string' || previous.baseDirectory.length > 4096
38
+ || !Array.isArray(previous.bundles) || previous.bundles.length > STYLE_OVERLAY_LIMITS.files) fallback('invalid-style-overlay');
39
+ let references = 0;
40
+ for (const entry of previous.bundles) {
41
+ if (!Array.isArray(entry) || entry.length !== 2 || !plans.has(entry[0]) || bundles.has(entry[0]) || !Array.isArray(entry[1]) || entry[1].length > STYLE_OVERLAY_LIMITS.files) fallback('invalid-style-overlay');
42
+ references += entry[1].length;
43
+ if (references > STYLE_OVERLAY_LIMITS.references) fallback('style-overlay-budget');
44
+ if (!entry[1].some(file => file?.name === entry[0])) fallback('invalid-style-overlay');
45
+ bundles.set(entry[0], entry[1]);
46
+ }
47
+ assetsFor(bundles);
48
+ if (Buffer.byteLength(JSON.stringify(previous)) > STYLE_OVERLAY_LIMITS.catalogueBytes) fallback('style-overlay-budget');
49
+ return bundles;
50
+ }
51
+
52
+ async function inspect(file) {
53
+ const info = await lstat(file);
54
+ if (!info.isFile() || info.size > STYLE_OVERLAY_LIMITS.bytes) fallback('invalid-style-overlay');
55
+ const data = await readFile(file);
56
+ return { name: path.basename(file), bytes: data.byteLength, sha256: digest(data) };
57
+ }
58
+
59
+ /** A CSS generation owns only explicitly catalogued stylesheet assets. */
60
+ export async function createStyleOverlay({ root, source, destination, baseSource = source, manifest, plans, selected }) {
61
+ const previous = manifest.devStyles.overlay;
62
+ if (!previous && manifest.devStyles.baseClient) fallback('missing-style-overlay');
63
+ if (previous && !manifest.devStyles.baseClient) fallback('invalid-style-overlay');
64
+ if (!Array.isArray(plans) || plans.length > STYLE_OVERLAY_LIMITS.files) fallback('style-overlay-budget');
65
+ const names = new Set(plans.map(([name]) => name));
66
+ if (names.size !== plans.length || ![...names].every(filename)) fallback('invalid-style-overlay');
67
+ const bundles = catalogue(previous, names), previousAssets = assetsFor(bundles);
68
+ const selectedNames = new Set(selected.map(([name]) => name));
69
+ if ([...selectedNames].some(name => !names.has(name))) fallback('invalid-style-overlay');
70
+ for (const route of manifest.routes || []) for (const url of route.css || []) {
71
+ if (url.includes('?v=') && !bundles.has(path.posix.basename(url.split('?')[0]))) fallback('missing-style-overlay');
72
+ }
73
+ const [canonicalRoot, canonicalSource, canonicalBase, parent, sourceAssets] = await Promise.all([realpath(root), realpath(source), realpath(baseSource), realpath(path.dirname(destination)), realpath(path.join(source, 'assets'))]).catch(error => {
74
+ if (error.code === 'ENOENT') fallback('missing-style-overlay');
75
+ throw error;
76
+ });
77
+ // The destination does not exist yet. Its existing parent supplies the real
78
+ // identity, including macOS /var aliases and Windows short-name prefixes.
79
+ const canonicalDestination = path.join(parent, path.basename(destination));
80
+ if (!inside(canonicalRoot, canonicalSource) || !inside(canonicalRoot, canonicalBase)
81
+ || parent !== canonicalRoot && !inside(canonicalRoot, parent) || !inside(canonicalRoot, canonicalDestination)
82
+ || canonicalDestination === canonicalSource || canonicalDestination === canonicalBase
83
+ || inside(canonicalDestination, canonicalSource) || inside(canonicalSource, canonicalDestination)
84
+ || inside(canonicalDestination, canonicalBase) || inside(canonicalBase, canonicalDestination)
85
+ || sourceAssets !== path.join(canonicalSource, 'assets')) fallback('invalid-style-overlay-directory');
86
+ const baseDirectory = path.relative(canonicalRoot, canonicalBase).replaceAll(path.sep, '/');
87
+ if (previous ? previous.baseDirectory !== baseDirectory : canonicalSource !== canonicalBase) fallback('invalid-style-overlay-base');
88
+ const assetsDirectory = path.join(destination, 'assets');
89
+ let owned = false;
90
+ async function inherit(file) {
91
+ const from = path.join(source, 'assets', file.name), target = path.join(assetsDirectory, file.name);
92
+ let actual;
93
+ try { actual = await inspect(from); } catch (error) { if (error.code === 'ENOENT') fallback('missing-style-overlay-asset'); throw error; }
94
+ if (actual.bytes !== file.bytes || actual.sha256 !== file.sha256) fallback('invalid-style-overlay-asset');
95
+ try { await link(from, target); }
96
+ catch (error) {
97
+ if (!['EXDEV', 'EPERM', 'ENOTSUP', 'EACCES'].includes(error.code)) throw error;
98
+ await copyFile(from, target);
99
+ }
100
+ }
101
+ try {
102
+ await mkdir(destination);
103
+ owned = true;
104
+ await mkdir(assetsDirectory);
105
+ for (const name of selected.map(([name]) => name)) bundles.delete(name);
106
+ for (const file of assetsFor(bundles).values()) await inherit(file);
107
+ } catch (error) {
108
+ if (owned) await rm(destination, { recursive: true, force: true });
109
+ throw error;
110
+ }
111
+
112
+ return {
113
+ async recordBundle(name, result) {
114
+ if (!selectedNames.has(name)) fallback('invalid-style-overlay-output');
115
+ const files = new Map();
116
+ for (const output of result.outputFiles) {
117
+ if (path.dirname(path.resolve(output.path)) !== path.resolve(assetsDirectory) || !filename(path.basename(output.path))) fallback('invalid-style-overlay-output');
118
+ const file = { name: path.basename(output.path), bytes: output.contents.byteLength, sha256: digest(output.contents) };
119
+ files.set(file.name, file);
120
+ }
121
+ if (!files.has(name)) fallback('invalid-style-overlay-output');
122
+ // CSS Module compilation emits its URL assets before the outer CSS
123
+ // bundle. Its absolute, external URL-token imports identify exactly the
124
+ // emitted images/fonts to retain, without scanning the JavaScript build.
125
+ const prefix = manifest.config.assetBase.replace(/\/$/, '') + '/';
126
+ for (const output of Object.values(result.metafile.outputs)) for (const item of output.imports || []) {
127
+ if (item.kind !== 'url-token' || !item.external || !item.path.startsWith(prefix)) continue;
128
+ let name;
129
+ try { name = decodeURIComponent(item.path.slice(prefix.length).split(/[?#]/)[0]); }
130
+ catch { fallback('invalid-style-overlay-output'); }
131
+ if (!filename(name) || files.has(name)) continue;
132
+ let file;
133
+ try { file = await inspect(path.join(assetsDirectory, name)); }
134
+ catch (error) {
135
+ if (error.code !== 'ENOENT') throw error;
136
+ const previous = previousAssets.get(name);
137
+ if (!previous) continue; // An unchanged base or public asset stays in its original generation.
138
+ await inherit(previous);
139
+ file = previous;
140
+ }
141
+ files.set(name, file);
142
+ }
143
+ bundles.set(name, [...files.values()]);
144
+ assetsFor(bundles); // Validate the aggregate budget before publishing bytes.
145
+ for (const output of result.outputFiles) {
146
+ const file = files.get(path.basename(output.path));
147
+ let existing;
148
+ try { existing = await inspect(output.path); } catch (error) { if (error.code !== 'ENOENT') throw error; }
149
+ if (existing) {
150
+ if (existing.bytes !== file.bytes || existing.sha256 !== file.sha256) fallback('conflicting-style-overlay');
151
+ continue;
152
+ }
153
+ const temporary = output.path + '.' + randomUUID() + '.tmp';
154
+ try { await writeFile(temporary, output.contents); await rename(temporary, output.path); }
155
+ finally { await rm(temporary, { force: true }); }
156
+ }
157
+ },
158
+ async finish() {
159
+ const assets = assetsFor(bundles);
160
+ const value = { version: 1, baseDirectory, bundles: [...bundles] };
161
+ if (Buffer.byteLength(JSON.stringify(value)) > STYLE_OVERLAY_LIMITS.catalogueBytes) fallback('style-overlay-budget');
162
+ // This directory was created above for CSS only. Remove module assets
163
+ // that no current bundle references, rather than accumulating old URLs.
164
+ const entries = await readdir(assetsDirectory, { withFileTypes: true });
165
+ if (entries.length > STYLE_OVERLAY_LIMITS.references) fallback('style-overlay-budget');
166
+ for (const entry of entries) {
167
+ if (!entry.isFile() || !filename(entry.name)) fallback('invalid-style-overlay-output');
168
+ if (!assets.has(entry.name)) await rm(path.join(assetsDirectory, entry.name));
169
+ }
170
+ return value;
171
+ },
172
+ };
173
+ }
@@ -1,14 +1,15 @@
1
1
  import {build as bundle} from 'esbuild';
2
- import {readFile,writeFile,rm,stat,readdir} from 'node:fs/promises';
2
+ import {readFile,writeFile,rm,stat} from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import {randomUUID} from 'node:crypto';
5
5
  import {rename} from '../runtime/fs.mjs';
6
6
  import {createCssModules} from './css-modules.mjs';
7
7
  import {recordDevDependencies,developmentStyleDependencies} from './dev-dependencies.mjs';
8
8
  import {writeBuildOutput} from './output-contract.mjs';
9
+ import {createStyleOverlay,StyleOverlayFallback} from './dev-css-assets.mjs';
9
10
 
10
11
  /** Publish only styles when all JS-visible class maps remain identical. */
11
- export async function tryDevCssUpdate({root,source,destination,manifest,changed,sequence,cssEngine,snapshot,onFallback=()=>{}}){
12
+ export async function tryDevCssUpdate({root,source,baseSource=source,destination,manifest,changed,sequence,cssEngine,onFallback=()=>{}}){
12
13
  const fallback=code=>{onFallback(code);return undefined;};
13
14
  if(!changed.length||changed.some(file=>!file.endsWith('.css'))||!manifest.devStyles?.bundles.length||manifest.app?.globalError?.css?.length)return;
14
15
  if(!manifest.config.assetBase.startsWith('/'))return fallback('external-asset-base');
@@ -32,8 +33,9 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
32
33
  if(changed.some(file=>!known.has(path.join(root,file))))return fallback('unknown-style-dependency');
33
34
  if(!selected.length)return fallback('unknown-style-dependency');
34
35
  const stage=destination,modules=createCssModules({projectRoot:root,stage,assetBase:manifest.config.assetBase,cssEngine,dev:true});
36
+ let overlay;
35
37
  try{
36
- await snapshot(source,stage);
38
+ overlay=await createStyleOverlay({root,source,baseSource,destination,manifest,plans,selected});
37
39
  for(const previous of manifest.devStyles.modules.filter(module=>(module.watchFiles||[module.file]).some(file=>changedPaths.has(file)))){
38
40
  if(!previous.file.endsWith('.css')){await rm(stage,{recursive:true,force:true});return fallback('stylesheet-language');}
39
41
  if((await modules.compile(previous.file)).mapping!==previous.mapping){await rm(stage,{recursive:true,force:true});return fallback('css-module-exports');}
@@ -42,7 +44,7 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
42
44
  for(const [name,files]of selected){
43
45
  const options={absWorkingDir:root,stdin:{contents:files.map(file=>`@import ${JSON.stringify(file.replace(/^prnext-compiled-css:/,'prnext-css:'))};`).join('\n'),resolveDir:root,loader:'css'},outfile:path.join(stage,'assets',name),bundle:true,write:false,metafile:true,logLevel:'silent',publicPath:manifest.config.assetBase,assetNames:'[name]-[hash]',loader:Object.fromEntries(['.png','.jpg','.jpeg','.svg','.gif','.webp','.avif','.woff','.woff2','.ttf'].map(ext=>[ext,'file'])),plugins:[modules.plugin(true)]};
44
46
  const result=await bundle(options);recordDevDependencies(state,options,result);
45
- for(const output of result.outputFiles){const temporary=output.path+'.'+randomUUID()+'.tmp';try{await writeFile(temporary,output.contents);await rename(temporary,output.path);}finally{await rm(temporary,{force:true});}}
47
+ await overlay.recordBundle(name,result);
46
48
  }
47
49
  const compiled=await modules.mappings();
48
50
  if(compiled.some(value=>!manifest.devStyles.modules.some(previous=>previous.file===value.file&&previous.mapping===value.mapping))){await rm(stage,{recursive:true,force:true});return fallback('css-module-exports');}
@@ -51,7 +53,8 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
51
53
  for(const module of updated)if(files.includes(module.file))for(const file of module.watchFiles)if(!files.includes(file))files.push(file);
52
54
  dependencies.set(name,files);
53
55
  }
54
- const buildId=randomUUID(),baseClient=manifest.devStyles.baseClient||manifest.devClient,next={...manifest,buildId,devSequence:sequence,devClient:manifest.config.assetBase+'/dev-manifest-'+buildId+'.json',outputDirectory:stage,devStyles:{baseClient,bundles:plans,dependencies:[...dependencies],modules:updated}};
56
+ const assets=await overlay.finish();
57
+ const buildId=randomUUID(),baseClient=manifest.devStyles.baseClient||manifest.devClient,next={...manifest,buildId,devSequence:sequence,devClient:manifest.config.assetBase+'/dev-manifest-'+buildId+'.json',outputDirectory:stage,devStyles:{baseClient,overlay:assets,bundles:plans,dependencies:[...dependencies],modules:updated}};
55
58
  // Keep route invalidation conservative as CSS imports are added or removed.
56
59
  // Unchanged routes retain their original dependency arrays.
57
60
  next.devDependencies=(manifest.devDependencies||[]).map(route=>{
@@ -66,7 +69,10 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
66
69
  }
67
70
  return {...route,files:[...files]};
68
71
  });
69
- const old=JSON.parse(await readFile(path.join(source,'assets',path.basename(manifest.devClient)),'utf8'));
72
+ let old;
73
+ try{old=JSON.parse(await readFile(path.join(source,'assets',path.basename(manifest.devClient)),'utf8'));}
74
+ catch(error){if(error.code==='ENOENT'||error instanceof SyntaxError)throw new StyleOverlayFallback('missing-style-manifest');throw error;}
75
+ if(!Array.isArray(old?.pages)||old.pages.some(page=>!Array.isArray(page.css))||old.app&&(!Array.isArray(old.app.css)||!Array.isArray(old.app.routes)||old.app.routes.some(route=>!Array.isArray(route.css))))throw new StyleOverlayFallback('invalid-style-manifest');
70
76
  old.buildId=buildId;
71
77
  const changedNames=new Set(selected.map(([name])=>name));
72
78
  const revise=urls=>urls.map(url=>changedNames.has(path.posix.basename(url.split('?')[0]))?url.split('?')[0]+'?v='+buildId:url);
@@ -74,12 +80,8 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
74
80
  for(const page of old.pages)page.css=revise(page.css);if(old.app){old.app.css=revise(old.app.css);for(const route of old.app.routes||[]){route.css=revise(route.css);if(route.cssScopes)route.cssScopes=reviseScopes(route.cssScopes);}}
75
81
  next.routes=next.routes.map(route=>({...route,css:revise(route.css||[]),...(route.cssScopes?{cssScopes:reviseScopes(route.cssScopes)}:{})}));
76
82
  await writeFile(path.join(stage,'assets',path.basename(next.devClient)),JSON.stringify(old));
77
- const protectedNames=new Set([path.basename(baseClient),path.basename(next.devClient)]),history=[];
78
- for(const name of await readdir(path.join(stage,'assets')))if(/^dev-manifest-.*\.json$/.test(name)&&!protectedNames.has(name)){const info=await stat(path.join(stage,'assets',name));history.push({name,size:info.size,time:info.mtimeMs});}
79
- history.sort((a,b)=>b.time-a.time);let bytes=0;
80
- for(let i=0;i<history.length;i++){bytes+=history[i].size;if(i>=32||bytes>4*1024*1024)await rm(path.join(stage,'assets',history[i].name));}
81
83
  const temporary=path.join(stage,'manifest.'+buildId+'.tmp');await writeFile(temporary,JSON.stringify(next));await rename(temporary,path.join(stage,'manifest.json'));
82
- await rm(path.join(stage,'build-output.json'));await writeBuildOutput(stage,next);
84
+ await writeBuildOutput(stage,next);
83
85
  return next;
84
- }catch(error){await rm(stage,{recursive:true,force:true});throw error;}
86
+ }catch(error){if(overlay)await rm(stage,{recursive:true,force:true});if(error instanceof StyleOverlayFallback)return fallback(error.code);throw error;}
85
87
  }
@@ -0,0 +1,77 @@
1
+ import { mkdir, readdir, lstat } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { compileApp } from './app.mjs';
5
+ import { configureCompiler } from './compiler.mjs';
6
+ import { createCssModules } from './css-modules.mjs';
7
+ import { createFonts } from './fonts.mjs';
8
+ import { createImages } from './images.mjs';
9
+ import { discardLazyBuild } from '../runtime/dev-lazy-staging.mjs';
10
+ import { loadEnvConfig } from '../runtime/env.mjs';
11
+
12
+ // Only the latest successful ordinary generation per project is reusable.
13
+ // This private compiler-process registry is not part of browser metadata or IPC.
14
+ const contexts = new Map();
15
+ const CONTEXT_LIMIT = 8, FILE_LIMIT = 16384, BYTE_LIMIT = 128 * 1024 * 1024;
16
+ export function forgetLazyBuildContext(root) {
17
+ if (root === undefined) contexts.clear(); else contexts.delete(root);
18
+ }
19
+ export function rememberLazyBuildContext(root, { manifest, config, environment, tsconfig, envMode }) {
20
+ contexts.delete(root);
21
+ if (!manifest.devLazy || !manifest.dev || !config.experimental.lazyDynamicImports) return;
22
+ while (contexts.size >= CONTEXT_LIMIT) contexts.delete(contexts.keys().next().value);
23
+ contexts.set(root, { config, environment, tsconfig, envMode, cacheId: manifest.cacheId,
24
+ buildId: manifest.buildId, session: manifest.devSession,
25
+ manifestConfig: structuredClone(manifest.config) });
26
+ }
27
+ /** Reuse the validated configuration of the running generation, not its routes. */
28
+ export async function tryBuildLazyGraph(root, { base, request, devInputs, devSession,
29
+ devActionSecret, incremental = true } = {}) {
30
+ if (!base || typeof base.cacheId !== 'string' || !request) throw Error('Invalid deferred build generation');
31
+ const context = contexts.get(root);
32
+ // Compiler recycling loses its private context. The caller retains the ordinary
33
+ // build path for this recovery case; it must never guess another generation.
34
+ if (!context || context.cacheId !== base.cacheId || context.session !== devSession) return;
35
+ // Preserve the running generation's env-file mode for supported build-time
36
+ // hooks. The enclosing build() scope restores the caller's env on every exit.
37
+ loadEnvConfig(root,{dev:true,mode:context.envMode,inputs:devInputs});
38
+ const { config, environment, tsconfig } = context;
39
+ const stage = path.join(root, '.prnext-build-' + randomUUID());
40
+ try {
41
+ await mkdir(path.join(stage, '.entries'), { recursive: true });
42
+ const manifest = { version: 1, dev: true, buildId: context.buildId,
43
+ cacheId: context.cacheId, devSession, config: structuredClone(context.manifestConfig),
44
+ routes: [], prerendered: [] };
45
+ const project = { root, routes: [] };
46
+ configureCompiler({ config, root, stage, buildId: context.buildId, dev: true,
47
+ incremental, devInputs, tsconfig, assetBase: manifest.config.assetBase });
48
+ const fonts = createFonts({ projectRoot: root, stage, assetBase: manifest.config.assetBase });
49
+ const cssModules = createCssModules({ projectRoot: root, stage,
50
+ assetBase: manifest.config.assetBase, sassOptions: config.sassOptions,
51
+ cssEngine: config.cssEngine, dev: true, config });
52
+ const images = await createImages({ projectRoot: root, stage,
53
+ assetBase: manifest.config.assetBase, config: config.images });
54
+ await compileApp({ project, stage, manifest, cssModules, fonts, images, dev: true,
55
+ devActionSecret, reactCompiler: config.reactCompiler, defineEnvironment: environment,
56
+ moduleResolution: config.turbopack, serverExternalPackages: config.serverExternalPackages,
57
+ lazyDynamicImports: true, devLazyRequest: request, devLazyOnly: true });
58
+ const files = [];
59
+ let bytes = 0;
60
+ // This unpublished stage contains only this deferred graph, never other
61
+ // routes or a copied framework runtime. Publication uses this exact receipt.
62
+ for (const entry of await readdir(path.join(stage, 'assets'), { withFileTypes: true })) {
63
+ if (!entry.isFile() || files.length >= FILE_LIMIT) throw Error('Invalid or excessive deferred output assets');
64
+ const info = await lstat(path.join(stage, 'assets', entry.name));
65
+ bytes += info.size;
66
+ if (!info.isFile() || bytes > BYTE_LIMIT) throw Error('Deferred output exceeds the 128 MiB generation budget');
67
+ files.push({ name: entry.name, bytes: info.size });
68
+ }
69
+ files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
70
+ return { ...manifest, outputDirectory: stage, devLazyTemporary: true,
71
+ devLazyAssets: { version: 1, baseCacheId: context.cacheId, files } };
72
+ } catch (error) {
73
+ try { await discardLazyBuild(root, { outputDirectory: stage, devLazyTemporary: true }); }
74
+ catch (cleanupError) { error.cleanupError = cleanupError; error.message += '; deferred compiler staging cleanup also failed'; }
75
+ throw error;
76
+ }
77
+ }
package/build/dynamic.mjs CHANGED
@@ -59,17 +59,19 @@ function loaderBody(loader) {
59
59
  }
60
60
 
61
61
  /** Add shared hydration IDs without resolving, evaluating or eagerly importing loaders. */
62
- export function transformDynamicImports(source, filename, { projectRoot, mode = 'browser' } = {}) {
62
+ export function transformDynamicImports(source, filename, { projectRoot, mode = 'browser',dev=false } = {}) {
63
63
  if (![...sources].some(name => source.includes(name))) return source;
64
64
  projectRoot ||= process.cwd();
65
- return cachedTransform('dynamic',source,[filename,projectRoot,mode],()=>transformDynamicImportsUncached(source,filename,{projectRoot,mode}));
65
+ return cachedTransform('dynamic',source,[filename,projectRoot,mode,dev],()=>transformDynamicImportsUncached(source,filename,{projectRoot,mode,dev}));
66
66
  }
67
67
 
68
- function transformDynamicImportsUncached(source, filename, {projectRoot,mode}) {
68
+ function transformDynamicImportsUncached(source, filename, {projectRoot,mode,dev}) {
69
69
  const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: filename,
70
70
  plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] });
71
71
  const relative = path.relative(projectRoot || process.cwd(), filename).replaceAll(path.sep, '/');
72
+ const developmentRevision=dev&&mode==='browser'?createHash('sha256').update(generate(ast,{compact:true,comments:false}).code).digest('hex'):undefined;
72
73
  let changed = false;
74
+ let ordinal=0;
73
75
  traverse(ast, {
74
76
  CallExpression(call) {
75
77
  let callee = call.get('callee');
@@ -105,6 +107,10 @@ function transformDynamicImportsUncached(source, filename, {projectRoot,mode}) {
105
107
  const generated = t.objectProperty(t.identifier('loadableGenerated'), t.objectExpression([
106
108
  t.objectProperty(t.identifier('modules'), t.arrayExpression(ids.map(id => t.stringLiteral(id)))),
107
109
  ]));
110
+ const statement=call.findParent(value=>value.isStatement());
111
+ const declaration=statement?.parentPath?.isExportNamedDeclaration()?statement.parentPath:statement;
112
+ const stableModuleCall=call.scope===call.scope.getProgramParent()&&declaration?.parentPath?.isProgram()&&!t.isLoop(declaration.node);
113
+ const identity=developmentRevision&&stableModuleCall?t.objectProperty(t.identifier('__prnextDevIdentity'),t.stringLiteral(createHash('sha256').update(relative+'\0'+ordinal+++'\0'+developmentRevision).digest('hex').slice(0,24))):undefined;
108
114
 
109
115
  if (noSSR && mode !== 'browser') {
110
116
  const empty = t.arrowFunctionExpression([], t.nullLiteral(), true);
@@ -115,9 +121,10 @@ function transformDynamicImportsUncached(source, filename, {projectRoot,mode}) {
115
121
  loader.replaceWith(t.arrowFunctionExpression([], loader.node));
116
122
  }
117
123
  const target = secondOptions || firstOptions;
118
- if (target?.isObjectExpression()) target.node.properties.push(generated);
119
- else if (target?.node) target.replaceWith(t.objectExpression([t.spreadElement(target.node), generated]));
120
- else call.node.arguments.push(t.objectExpression([generated]));
124
+ const fields=[generated,...(identity?[identity]:[])];
125
+ if (target?.isObjectExpression()) target.node.properties.push(...fields);
126
+ else if (target?.node) target.replaceWith(t.objectExpression([t.spreadElement(target.node), ...fields]));
127
+ else call.node.arguments.push(t.objectExpression(fields));
121
128
  changed = true;
122
129
  },
123
130
  });
@@ -1,5 +1,7 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { mkdir, readFile, writeFile, rename, rm, readdir, lstat } from 'node:fs/promises';
2
+ import { open, writeFile, rename, rm, readdir, lstat } from 'node:fs/promises';
3
+ import {constants} from 'node:fs';
4
+ import {privateCacheDirectory} from './persistent-cache.mjs';
3
5
  import path from 'node:path';
4
6
 
5
7
  const hash = value => createHash('sha256').update(value).digest('hex');
@@ -11,12 +13,14 @@ let writes = Promise.resolve();
11
13
  */
12
14
  export async function cachedFontResource(root, url, maximum, accept, download) {
13
15
  if (!root) return download();
14
- const directory = path.join(root, '.prnext-cache', 'fonts-v1');
16
+ const directory = await privateCacheDirectory(root,'fonts-v1');
17
+ if(!directory)return download();
15
18
  const file = path.join(directory, hash(JSON.stringify([url, accept])) + '.bin');
16
19
  try {
17
20
  const metadata = await lstat(file);
18
- if (metadata.isFile() && metadata.size > 65 && metadata.size <= maximum + 65) {
19
- const stored = await readFile(file);
21
+ if (metadata.isFile() && !metadata.isSymbolicLink() && metadata.size > 65 && metadata.size <= maximum + 65) {
22
+ const handle=await open(file,constants.O_RDONLY|(constants.O_NOFOLLOW||0));
23
+ let stored;try{const info=await handle.stat();if(!info.isFile()||info.size>maximum+65)throw Object.assign(Error('Font cache changed'),{code:'ENOENT'});stored=await handle.readFile();}finally{await handle.close();}
20
24
  const bytes = stored.subarray(65);
21
25
  if (stored[64] === 10 && hash(bytes) === stored.toString('ascii', 0, 64)) return bytes;
22
26
  }
@@ -24,10 +28,9 @@ export async function cachedFontResource(root, url, maximum, accept, download) {
24
28
  const bytes = await download();
25
29
  if (!bytes.length || bytes.length > maximum) throw new Error('Font resource exceeds its cache input limit');
26
30
  const task = writes.catch(() => {}).then(async () => {
27
- await mkdir(directory, { recursive: true });
28
31
  const temporary = file + '.' + randomUUID() + '.tmp';
29
32
  try {
30
- await writeFile(temporary, Buffer.concat([Buffer.from(hash(bytes) + '\n'), bytes]), { flag: 'wx' });
33
+ await writeFile(temporary, Buffer.concat([Buffer.from(hash(bytes) + '\n'), bytes]), { flag: 'wx',mode:0o600 });
31
34
  await rename(temporary, file);
32
35
  } finally { await rm(temporary, { force: true }); }
33
36
  const entries = [];
@@ -183,7 +183,7 @@ async function google(name, options, context) {
183
183
  }
184
184
 
185
185
  async function fallbackMetrics(faces, fallbackName) {
186
- const { create } = await import('fontkit');
186
+ const { create } = await import('../vendor/fontkit/index.mjs');
187
187
  // Prefer an upright face near the normal text weight for a shared fallback.
188
188
  const candidates = [...faces].sort((a, b) => Number(a.style === 'italic') - Number(b.style === 'italic') || Math.abs((parseFloat(a.weight) || 400) - 400) - Math.abs((parseFloat(b.weight) || 400) - 400));
189
189
  for (const face of candidates) {