@thomas.f/prnext 0.1.17-alpha → 0.1.19-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 (202) hide show
  1. package/README.md +27 -868
  2. package/build/actions.mjs +27 -15
  3. package/build/analyze.mjs +180 -24
  4. package/build/app-config.mjs +13 -0
  5. package/build/app-static.mjs +34 -12
  6. package/build/app-styles.mjs +79 -0
  7. package/build/app.mjs +122 -46
  8. package/build/bundle-runtime.mjs +22 -9
  9. package/build/cache-components.mjs +1 -1
  10. package/build/cache-handlers.mjs +1 -1
  11. package/build/compact-css.mjs +53 -0
  12. package/build/compilation-cache.mjs +269 -0
  13. package/build/compilation-store.mjs +142 -0
  14. package/build/compiler.mjs +34 -10
  15. package/build/config.mjs +34 -3
  16. package/build/create.mjs +239 -0
  17. package/build/css-modules.mjs +56 -12
  18. package/build/deployment-assets.mjs +89 -0
  19. package/build/deployment-config.mjs +18 -0
  20. package/build/dev-css-assets.mjs +173 -0
  21. package/build/dev-css-update.mjs +18 -14
  22. package/build/dev-dependencies.mjs +1 -1
  23. package/build/dev-discovery.mjs +3 -3
  24. package/build/dev-lazy-build.mjs +77 -0
  25. package/build/dev.mjs +15 -8
  26. package/build/dynamic.mjs +14 -7
  27. package/build/edge.mjs +3 -3
  28. package/build/fonts.mjs +1 -1
  29. package/build/framework-imports.mjs +15 -5
  30. package/build/html-only-config.mjs +53 -0
  31. package/build/html-only-graph.mjs +67 -0
  32. package/build/incremental.mjs +112 -18
  33. package/build/index.mjs +90 -28
  34. package/build/instrumentation.mjs +30 -0
  35. package/build/lazy-dynamic-imports.mjs +137 -0
  36. package/build/metadata-routes.mjs +1 -1
  37. package/build/migrate.mjs +4 -4
  38. package/build/migration-audit.mjs +19 -31
  39. package/build/migration-imports.mjs +245 -0
  40. package/build/migration-scenarios-runner.mjs +73 -0
  41. package/build/migration-scenarios.mjs +72 -0
  42. package/build/native-discovery.mjs +65 -5
  43. package/build/native-packages.mjs +2 -0
  44. package/build/native-type-config.mjs +37 -0
  45. package/build/npm-packages.mjs +1 -0
  46. package/build/persistent-cache.mjs +18 -3
  47. package/build/precompress.mjs +1 -1
  48. package/build/prerender-cache.mjs +70 -0
  49. package/build/prerender-config.mjs +29 -0
  50. package/build/prerender-pages.mjs +19 -9
  51. package/build/prerender-pool.mjs +165 -0
  52. package/build/prerender-process-worker.mjs +58 -0
  53. package/build/prerender-worker.mjs +5 -5
  54. package/build/production-builder.mjs +59 -10
  55. package/build/react-compiler-report.mjs +225 -0
  56. package/build/react-compiler.mjs +139 -0
  57. package/build/runtime-contract.mjs +2 -2
  58. package/build/standalone.mjs +21 -3
  59. package/build/static-export.mjs +4 -4
  60. package/build/styles.mjs +6 -4
  61. package/build/transform.mjs +1 -1
  62. package/build/typed-routes.mjs +4 -0
  63. package/build/typescript-dev.mjs +35 -8
  64. package/build/typescript-native-worker.mjs +55 -0
  65. package/build/typescript-native.mjs +18 -0
  66. package/build/typescript-toolchain.mjs +19 -0
  67. package/build/typescript.mjs +6 -4
  68. package/build/webpack-graph.mjs +19 -6
  69. package/build/worker-memory.mjs +9 -0
  70. package/cli.mjs +70 -5
  71. package/compat/activity-cache.cjs +26 -0
  72. package/compat/after.cjs +6 -4
  73. package/compat/app-dynamic.cjs +12 -1
  74. package/compat/app-layout-context.cjs +52 -2
  75. package/compat/base64.cjs +10 -0
  76. package/compat/cache-handlers.cjs +15 -3
  77. package/compat/cache-wire.cjs +28 -0
  78. package/compat/cache.cjs +2 -1
  79. package/compat/cache.d.cts +4 -0
  80. package/compat/cookies.cjs +21 -5
  81. package/compat/data-cache.cjs +12 -8
  82. package/compat/flight-delta.cjs +152 -0
  83. package/compat/headers.cjs +4 -2
  84. package/compat/incremental-cache.cjs +20 -7
  85. package/compat/index.d.ts +69 -4
  86. package/compat/link.cjs +13 -10
  87. package/compat/navigation-authorization.cjs +9 -0
  88. package/compat/navigation-stages.cjs +19 -0
  89. package/compat/offline-store.cjs +11 -0
  90. package/compat/offline.cjs +5 -0
  91. package/compat/offline.d.cts +2 -0
  92. package/compat/redis-cache.cjs +201 -0
  93. package/compat/redis-cache.d.cts +37 -0
  94. package/compat/server-primitives.cjs +31 -11
  95. package/compat/server.d.cts +15 -2
  96. package/compat/static-generation.cjs +7 -0
  97. package/compat/use-cache.cjs +3 -0
  98. package/docs/ai/compatibility.md +248 -2
  99. package/docs/ai/data.md +165 -5
  100. package/docs/ai/deployment.md +158 -0
  101. package/docs/ai/quickstart.md +29 -4
  102. package/docs/ai/routing.md +93 -0
  103. package/docs/ai/troubleshooting.md +116 -2
  104. package/package.json +40 -9
  105. package/runtime/action-client.mjs +10 -4
  106. package/runtime/action-server.mjs +2 -0
  107. package/runtime/app-client.mjs +316 -92
  108. package/runtime/app-content.mjs +4 -3
  109. package/runtime/app-partial-client.mjs +3 -0
  110. package/runtime/app-partial.mjs +1 -1
  111. package/runtime/app-prefetch-constants.mjs +4 -0
  112. package/runtime/app-prefetch.mjs +2 -3
  113. package/runtime/app-render-key.mjs +5 -0
  114. package/runtime/app-render.mjs +88 -28
  115. package/runtime/app-routing.mjs +22 -9
  116. package/runtime/app-static.mjs +18 -8
  117. package/runtime/app-stream-html.mjs +10 -53
  118. package/runtime/app-style-selection.mjs +34 -0
  119. package/runtime/app-styles.mjs +154 -0
  120. package/runtime/atomic-file.mjs +27 -0
  121. package/runtime/cache-components-rsc.mjs +7 -2
  122. package/runtime/cache-request.mjs +2 -1
  123. package/runtime/deployment-client.mjs +19 -0
  124. package/runtime/dev-client.mjs +13 -3
  125. package/runtime/dev-compiler-broker.mjs +42 -0
  126. package/runtime/dev-file-changes.mjs +123 -8
  127. package/runtime/dev-indicator.mjs +8 -3
  128. package/runtime/dev-lazy-imports.mjs +121 -0
  129. package/runtime/dev-lazy-staging.mjs +14 -0
  130. package/runtime/dev-lazy-tool.mjs +16 -0
  131. package/runtime/dev-style-retention.mjs +15 -0
  132. package/runtime/development.mjs +83 -25
  133. package/runtime/document.mjs +1 -1
  134. package/runtime/edge.mjs +6 -5
  135. package/runtime/fetch-cache.mjs +10 -8
  136. package/runtime/flight-delta.mjs +74 -0
  137. package/runtime/flight-tap.mjs +28 -0
  138. package/runtime/fs.mjs +15 -8
  139. package/runtime/html-only.mjs +27 -0
  140. package/runtime/http.mjs +2 -0
  141. package/runtime/incremental-cache.mjs +25 -4
  142. package/runtime/instrumentation.mjs +49 -0
  143. package/runtime/lazy-import-client.mjs +38 -0
  144. package/runtime/navigation-recovery.mjs +36 -0
  145. package/runtime/navigation.mjs +7 -0
  146. package/runtime/node-html-stream.mjs +47 -0
  147. package/runtime/pages-client.mjs +14 -7
  148. package/runtime/pages-navigation.mjs +48 -63
  149. package/runtime/prefetch-scheduler.mjs +110 -0
  150. package/runtime/profile.mjs +14 -0
  151. package/runtime/profiles.json +2 -2
  152. package/runtime/render.mjs +3 -0
  153. package/runtime/route-pattern.mjs +44 -0
  154. package/runtime/rsc-worker.mjs +34 -12
  155. package/runtime/trace.mjs +78 -0
  156. package/runtime/watch-project.mjs +19 -15
  157. package/runtime/worker.mjs +70 -26
  158. package/vendor/nft/LICENSE +7 -0
  159. package/vendor/nft/README.md +16 -0
  160. package/vendor/nft/out/analyze.d.ts +8 -0
  161. package/vendor/nft/out/analyze.js +1216 -0
  162. package/vendor/nft/out/analyze.js.map +1 -0
  163. package/vendor/nft/out/cli.d.ts +2 -0
  164. package/vendor/nft/out/cli.js +94 -0
  165. package/vendor/nft/out/cli.js.map +1 -0
  166. package/vendor/nft/out/fs.d.ts +22 -0
  167. package/vendor/nft/out/fs.js +106 -0
  168. package/vendor/nft/out/fs.js.map +1 -0
  169. package/vendor/nft/out/index.d.ts +4 -0
  170. package/vendor/nft/out/index.js +25 -0
  171. package/vendor/nft/out/index.js.map +1 -0
  172. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  173. package/vendor/nft/out/node-file-trace.js +380 -0
  174. package/vendor/nft/out/node-file-trace.js.map +1 -0
  175. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  176. package/vendor/nft/out/resolve-dependency.js +427 -0
  177. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  178. package/vendor/nft/out/types.d.ts +66 -0
  179. package/vendor/nft/out/types.js +2 -0
  180. package/vendor/nft/out/types.js.map +1 -0
  181. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  182. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  183. package/vendor/nft/out/utils/binary-locators.js +88 -0
  184. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  185. package/vendor/nft/out/utils/get-package-base.js +30 -0
  186. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  187. package/vendor/nft/out/utils/interop-require.js +24 -0
  188. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  189. package/vendor/nft/out/utils/node-version.js +10 -0
  190. package/vendor/nft/out/utils/node-version.js.map +1 -0
  191. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  192. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  193. package/vendor/nft/out/utils/special-cases.js +347 -0
  194. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  195. package/vendor/nft/out/utils/static-eval.js +579 -0
  196. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  197. package/vendor/nft/out/utils/types.js +2 -0
  198. package/vendor/nft/out/utils/types.js.map +1 -0
  199. package/vendor/nft/out/utils/wrappers.js +659 -0
  200. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  201. package/vendor/nft/package.json +4 -0
  202. package/vendor/nft/upstream.json +51 -0
@@ -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');
@@ -31,9 +32,10 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
31
32
  for(const dependency of manifest.devDependencies||[])for(const file of dependency.files)if(file.endsWith('.css'))known.add(path.join(root,file));
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
- const stage=destination,modules=createCssModules({projectRoot:root,stage,assetBase:manifest.config.assetBase,cssEngine});
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,18 +69,19 @@ 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);
73
- for(const page of old.pages)page.css=revise(page.css);if(old.app)old.app.css=revise(old.app.css);
79
+ const reviseScopes=scopes=>scopes?.map(scope=>({...scope,css:revise(scope.css)}));
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);}}
81
+ next.routes=next.routes.map(route=>({...route,css:revise(route.css||[]),...(route.cssScopes?{cssScopes:reviseScopes(route.cssScopes)}:{})}));
74
82
  await writeFile(path.join(stage,'assets',path.basename(next.devClient)),JSON.stringify(old));
75
- const protectedNames=new Set([path.basename(baseClient),path.basename(next.devClient)]),history=[];
76
- 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});}
77
- history.sort((a,b)=>b.time-a.time);let bytes=0;
78
- 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));}
79
83
  const temporary=path.join(stage,'manifest.'+buildId+'.tmp');await writeFile(temporary,JSON.stringify(next));await rename(temporary,path.join(stage,'manifest.json'));
80
- await rm(path.join(stage,'build-output.json'));await writeBuildOutput(stage,next);
84
+ await writeBuildOutput(stage,next);
81
85
  return next;
82
- }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;}
83
87
  }
@@ -8,7 +8,7 @@ export function recordDevDependencies(state,options,result){
8
8
  for(const item of input.imports||[])if(!item.external)imports.add(absolute(item.path));
9
9
  state.devDependencies.set(file,imports);
10
10
  }
11
- for(const [name,output]of Object.entries(result.metafile.outputs))if(name.endsWith('.css')){
11
+ for(const [name,output]of Object.entries(result.metafile.outputs))if(name.endsWith('.css')&&path.resolve(options.absWorkingDir||state.root,name).startsWith(path.join(state.stage,'assets')+path.sep)){
12
12
  const inputs=Object.keys(output.inputs||{}).filter(file=>file!=='<stdin>'),imported=new Set();
13
13
  for(const input of inputs)for(const item of result.metafile.inputs[input]?.imports||[])if(item.kind==='import-rule')imported.add(item.path);
14
14
  const roots=inputs.filter(input=>!imported.has(input));
@@ -19,9 +19,9 @@ export async function discoverDevelopment(root) {
19
19
  try {
20
20
  const config=await loadProjectConfig(root,{dev:true,envMode});
21
21
  const project=await scanProject(root,{basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n,devAppFiles:new Set()});
22
- // Dynamic middleware/rewrite destinations and intercepted/parallel trees
23
- // need the complete graph until the gateway can resolve their routing RPC.
24
- const eager=!!project.middleware || !!config.rewrites || project.routes.some(route=>route.parallel||route.interception);
22
+ // Destinations unknown before routing are represented by deferred entries.
23
+ // Their worker asks the compiler only after middleware has run once.
24
+ const eager=false;
25
25
  return {project,eager,cssFast:!config.webpack&&!Object.keys(config.turbopack?.rules||{}).length,config:{basePath:config.basePath,assetPrefix:config.assetPrefix,i18n:config.i18n,distDir:config.distDir,
26
26
  onDemandEntries:config.onDemandEntries,typescript:config.typescript,typedRoutes:config.typedRoutes,cssEngine:config.cssEngine}};
27
27
  } finally {restore();}
@@ -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/dev.mjs CHANGED
@@ -5,7 +5,8 @@ import { createHash } from 'node:crypto';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { cachedAsyncTransform } from './transform-cache.mjs';
8
- import {inlineSourceMap} from './source-maps.mjs';
8
+ import {inlineSourceMap,extractSourceMap} from './source-maps.mjs';
9
+ import {TraceMap,originalPositionFor} from '@jridgewell/trace-mapping';
9
10
 
10
11
  const packageRoot = fileURLToPath(new URL('../', import.meta.url));
11
12
  export const devClientFile = path.join(packageRoot, 'runtime/dev-client.mjs');
@@ -25,10 +26,14 @@ function namedDefaultComponents({types:t}){
25
26
  }
26
27
  }}}};
27
28
  }
28
- function sourceElements(id){return ({types:t})=>({visitor:{JSXOpeningElement(node){
29
+ function sourceElements(id,trace){return ({types:t})=>({visitor:{JSXOpeningElement(node){
29
30
  const name=node.node.name;
30
31
  if(name.type!=='JSXIdentifier'||!/^[a-z]/.test(name.name)||node.node.attributes.some(attribute=>attribute.type==='JSXAttribute'&&attribute.name.name==='data-prnext-source'))return;
31
- const start=node.node.loc?.start;if(!start)return;
32
+ let start=node.node.loc?.start;if(!start)return;
33
+ // React Compiler and custom loaders may move JSX before instrumentation.
34
+ // Follow their map back to the editable source; generated, unmapped elements
35
+ // must not send the inspector to an invented line in the user's component.
36
+ if(trace){const original=originalPositionFor(trace,start);if(original.line==null||original.column==null)return;start=original;}
32
37
  node.node.attributes.push(t.jsxAttribute(t.jsxIdentifier('data-prnext-source'),t.stringLiteral(`${id}:${start.line}:${start.column+1}`)));
33
38
  }}});}
34
39
 
@@ -41,8 +46,9 @@ export function createSourceLocationTransform(projectRoot){
41
46
  const root=filename.startsWith(projectRoot+path.sep)?projectRoot:await canonicalRoot;
42
47
  if(!filename.startsWith(root+path.sep))return source;
43
48
  const id=path.relative(root,filename).split(path.sep).join('/');
44
- return cachedAsyncTransform('dev-source-elements',source,[root,filename,'v1'],async()=>{
45
- const result=await transformAsync(source,{filename,babelrc:false,configFile:false,sourceMaps:true,sourceFileName:filename,sourceType:'unambiguous',parserOpts:{plugins:['jsx',...(/\.tsx?$/.test(filename)?['typescript']:[])]},plugins:[sourceElements(id)]});
49
+ return cachedAsyncTransform('dev-source-elements',source,[root,filename,'v2-original-locations'],async()=>{
50
+ const input=extractSourceMap(source),trace=input.map?new TraceMap(input.map):undefined;
51
+ const result=await transformAsync(source,{filename,babelrc:false,configFile:false,sourceMaps:true,sourceFileName:filename,sourceType:'unambiguous',parserOpts:{plugins:['jsx',...(/\.[cm]?tsx?$/.test(filename)?['typescript']:[])]},plugins:[sourceElements(id,trace)]});
46
52
  return inlineSourceMap(result.code,result.map);
47
53
  });
48
54
  };
@@ -96,12 +102,13 @@ export function createRefreshTransform(projectRoot, { generation,forceGeneration
96
102
  const root = filename.startsWith(projectRoot + path.sep) ? projectRoot : await canonicalRoot;
97
103
  if (!filename.startsWith(root + path.sep)) return source;
98
104
  const hash = createHash('sha256').update(source).digest('hex');
99
- const transformed = await cachedAsyncTransform('fast-refresh',source,[projectRoot,filename,'source-elements-v2'],async () => {
105
+ const transformed = await cachedAsyncTransform('fast-refresh',source,[projectRoot,filename,'source-elements-v3-original-locations'],async () => {
100
106
  const id = path.relative(root, filename).replaceAll(path.sep, '/');
107
+ const input=extractSourceMap(source),trace=input.map?new TraceMap(input.map):undefined;
101
108
  const result = await transformAsync(source, { filename, babelrc: false, configFile: false,
102
109
  ast: true, sourceMaps: true, sourceFileName:filename, sourceType: 'unambiguous',
103
- parserOpts: { plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] },
104
- plugins: [namedDefaultComponents,[refresh, { skipEnvCheck: true, refreshReg: '__prnextRefreshReg', refreshSig: '__prnextRefreshSig' }],sourceElements(id)],
110
+ parserOpts: { plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] },
111
+ plugins: [namedDefaultComponents,[refresh, { skipEnvCheck: true, refreshReg: '__prnextRefreshReg', refreshSig: '__prnextRefreshSig' }],sourceElements(id,trace)],
105
112
  });
106
113
  // An ESM import changes webpack's interpretation of module.exports.
107
114
  // Keep script/CommonJS inputs in their original module format.
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
- plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] });
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
  });
package/build/edge.mjs CHANGED
@@ -60,7 +60,7 @@ function validateUncached(source,file,reactMode) {
60
60
  }
61
61
 
62
62
  /** Bundle the entire application graph as Web ESM before entering the V8 realm. */
63
- export async function compileEdge({ file, outfile, projectRoot, dev = false, defineEnvironment = {}, defaultExport = false, reactMode, plugins = [], transformSource }) {
63
+ export async function compileEdge({ file, outfile, projectRoot, dev = false, defineEnvironment = {}, defaultExport = false, reactMode, plugins = [], transformSource, transformTypedHelpers = false }) {
64
64
  const runtime = path.resolve(path.dirname(outfile), '../runtime');
65
65
  await mkdir(runtime, { recursive: true });
66
66
  try { await access(path.join(runtime, 'edge-vm.cjs')); }
@@ -85,7 +85,7 @@ export async function compileEdge({ file, outfile, projectRoot, dev = false, def
85
85
  mainFields: ['browser', 'module', 'main'], logLevel: 'silent', metafile: true,
86
86
  define: { 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), ...defineEnvironment, 'process.env.NEXT_RUNTIME': '"edge"' },
87
87
  plugins: [...plugins, { name: 'prnext-edge', setup(builder) {
88
- applicationEntry(builder,readCompilerFile);
88
+ applicationEntry(builder,readCompilerFile,{transformTypedHelpers});
89
89
  builder.onResolve({ filter: /.*/ }, args => {
90
90
  if (builtin.has(args.path) || args.path.startsWith('node:')) return { errors: [{ text: `Node.js module ${args.path} cannot run in the Edge Runtime (${args.importer}).` }] };
91
91
  if (reactMode && ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom', 'react-server-dom-webpack/server', 'react-server-dom-webpack/server.node'].includes(args.path)) return { path: args.path, namespace: 'prnext-edge-react' };
@@ -112,7 +112,7 @@ export async function compileEdge({ file, outfile, projectRoot, dev = false, def
112
112
  : names[args.path].map(name => `export const ${name}=globalThis.__PRNEXT_EDGE_HOST.${args.path}.${name};`).join('\n') }));
113
113
  builder.onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: 'file' }, async args => {
114
114
  let source = args.prnextSource ?? await readFile(args.path, 'utf8');
115
- if (transformSource) source = await transformSource(source, args.path, builder);
115
+ if (transformSource) source = await transformSource(source, args.path, builder, {loaderOutput:args.prnextSource!==undefined});
116
116
  if (!trusted.has(args.path)) source = validate(source, args.path, reactMode);
117
117
  return { contents: source, loader: /\.[cm]?tsx?$/.test(args.path) ? 'tsx' : 'jsx', resolveDir: path.dirname(args.path) };
118
118
  });
package/build/fonts.mjs CHANGED
@@ -51,7 +51,7 @@ export function createFonts({ projectRoot, stage, assetBase }) {
51
51
  }
52
52
  async function transformSource(source, file) {
53
53
  if (/(?:^|[/\\])pages[/\\]_document\.[cm]?[jt]sx?$/.test(file)) throw new Error('next/font cannot be used in pages/_document. Define fonts in pages/_app or a page component.');
54
- const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.tsx?$/.test(file) ? ['typescript'] : [])] });
54
+ const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(file) ? ['typescript'] : [])] });
55
55
  let program;
56
56
  traverse(ast, { Program(value) { program = value; value.stop(); } });
57
57
  const imports = [], calls = [];
@@ -4,6 +4,7 @@ import {readFile} from 'node:fs/promises';
4
4
  import {analyzeSource} from './source-analysis.mjs';
5
5
  import {cachedTransform} from './transform-cache.mjs';
6
6
  const adapters = new Map([
7
+ ['cache/redis','redis-cache'],
7
8
  ['dist/shared/lib/router-context.shared-runtime','next-router-context'],
8
9
  ['dist/shared/lib/router-context','next-router-context'],
9
10
  ['dist/shared/lib/app-router-context.shared-runtime','next-app-router-context'],
@@ -23,33 +24,42 @@ export function frameworkImportName(specifier) {
23
24
 
24
25
  // Application graphs must not traverse the programmatic build API's native tools.
25
26
  // Outside a compiled application, the package root retains its full Node API.
26
- export function applicationEntry(builder,readSource=(file,override)=>override??readFile(file,'utf8')){
27
- // esbuild understands .mts/.cts, but our framework transforms do not. Never
27
+ export function applicationEntry(builder,readSource=(file,override)=>override??readFile(file,'utf8'),{transformTypedHelpers=false}={}){
28
+ // Server/client module boundaries in .mts/.cts are not supported. Never
28
29
  // silently bundle a server implementation or misclassify a client boundary.
29
30
  builder.onLoad({filter:/\.[cm]ts$/i,namespace:'file'},async args=>{
30
31
  // Loader bridges dispatch framework callbacks independently of their filters.
31
32
  if(!/\.[cm]ts$/i.test(args.path))return;
32
33
  const source=await readSource(args.path,args.prnextSource);
33
34
  const original=args.prnextSource===undefined?source:await readSource(args.path);
35
+ let nativeDecorators=false;
34
36
  // A loader may remove a directive as well as introduce one. Both versions
35
37
  // must be safe before its output can become an ordinary browser module.
36
38
  for(const candidate of new Set([original,source])){
37
- const unsupported=cachedTransform('unsupported-framework-directives',candidate,[args.path,'v3'],()=>{
39
+ const unsupported=cachedTransform('unsupported-framework-directives',candidate,[args.path,'v4-native-decorators'],()=>{
38
40
  const analysis=analyzeSource(candidate,args.path);
39
41
  if(analysis.frameworkBoundary)return 'yes';
40
- const pending=[analysis.program];
42
+ const pending=[analysis.program];let decorators=false;
41
43
  while(pending.length){
42
44
  const node=pending.pop();
43
45
  if(node.type==='ExpressionStatement'&&node.directive==='use server')return 'yes';
46
+ if(node.type==='Decorator'||node.decorators?.length)decorators=true;
44
47
  for(const value of Object.values(node)){
45
48
  if(Array.isArray(value)){for(const item of value)if(item?.type)pending.push(item);}
46
49
  else if(value&&typeof value==='object'&&value.type)pending.push(value);
47
50
  }
48
51
  }
49
- return 'no';
52
+ return decorators?'decorated':'no';
50
53
  });
51
54
  if(unsupported==='yes')throw new Error(`React server/client directives are not supported in ${args.path}. Rename this file to .ts or .tsx.`);
55
+ nativeDecorators ||= unsupported==='decorated';
52
56
  }
57
+ // Compiler-enabled render graphs can transform ordinary typed helpers after
58
+ // this boundary check. Other consumers keep the historical esbuild path.
59
+ // Decorated typed helpers already have a native esbuild implementation.
60
+ // Keep that path instead of widening the syntax accepted by every Babel
61
+ // transform, while their importing React components can still be compiled.
62
+ if(transformTypedHelpers && !nativeDecorators)return;
53
63
  return {contents:source,loader:args.prnextSource===undefined?'ts':'tsx',resolveDir:dirname(args.path)};
54
64
  });
55
65
  builder.onResolve({filter:/^(?:prnext|@thomas\.f\/prnext)(?:\/route(?:\.js)?)?$/},()=>
@@ -0,0 +1,53 @@
1
+ import {assertHtmlOnlyDocument} from '../runtime/html-only.mjs';
2
+ const MAX_PATTERNS = 128;
3
+
4
+ export function validateHtmlOnly(value) {
5
+ if (value === undefined) return [];
6
+ if (!Array.isArray(value) || value.length > MAX_PATTERNS) throw new TypeError('htmlOnly must be an array of at most 128 App route patterns.');
7
+ for (const pattern of value) {
8
+ if (typeof pattern !== 'string' || pattern.length > 512 || !pattern.startsWith('/') || pattern.startsWith('//') ||
9
+ /[\s\\?#\u0000-\u001f\u007f]/.test(pattern) || /%(?![\da-f]{2})/i.test(pattern)) throw new TypeError('htmlOnly entries must be clean absolute route patterns without queries, fragments, whitespace or backslashes.');
10
+ const pathname = pattern.endsWith('/**') ? pattern.slice(0, -3) || '/' : pattern;
11
+ if (pathname.includes('*') || (pathname !== '/' && pathname.endsWith('/')) || pathname.slice(1).includes('//')) throw new TypeError('htmlOnly supports exact registered route patterns and a final /** subtree suffix only.');
12
+ for (const segment of pathname.split('/').slice(1)) {
13
+ let decoded;
14
+ try { decoded = decodeURIComponent(segment); } catch { throw new TypeError('htmlOnly patterns must use valid URL encoding.'); }
15
+ if (decoded === '.' || decoded === '..' || /[\\/\u0000-\u001f\u007f]/.test(decoded)) throw new TypeError('htmlOnly patterns cannot contain dot segments or encoded path separators.');
16
+ }
17
+ }
18
+ return [...new Set(value)];
19
+ }
20
+
21
+ export function matchesHtmlOnly(pattern, routePattern) {
22
+ if (!pattern.endsWith('/**')) return pattern === routePattern;
23
+ const prefix = pattern.slice(0, -3);
24
+ return !prefix || routePattern === prefix || routePattern.startsWith(prefix + '/');
25
+ }
26
+
27
+ export function selectHtmlOnlyRoutes(routes, patterns, { cacheComponents = false } = {}) {
28
+ const selected = new Set();
29
+ for (const pattern of validateHtmlOnly(patterns)) {
30
+ const matches = routes.filter(route => !route.internal && matchesHtmlOnly(pattern, route.pattern));
31
+ if (!matches.length) throw new Error(`htmlOnly pattern ${JSON.stringify(pattern)} does not match a registered route.`);
32
+ for (const route of matches) {
33
+ if (route.router !== 'app' || route.kind !== 'page') throw new Error(`htmlOnly route ${route.pattern} must be an App Router page; Pages Router and Route Handlers are not supported.`);
34
+ const config = route.cacheConfig || {};
35
+ if (cacheComponents || config.runtime === 'edge' || config.dynamic === 'force-dynamic' || config.forceNoStore || (config.revalidate !== undefined && config.revalidate !== false)) {
36
+ throw new Error(`htmlOnly route ${route.pattern} must be fully prerendered without Edge, Cache Components/PPR, dynamic rendering or ISR.`);
37
+ }
38
+ if (route.parallel || route.interception) throw new Error(`htmlOnly route ${route.pattern} does not support parallel or intercepted routing.`);
39
+ if (route.pattern.includes('[') && config.dynamicParams !== false) throw new Error(`htmlOnly route ${route.pattern} requires dynamicParams: false and complete generateStaticParams results.`);
40
+ selected.add(route.id);
41
+ }
42
+ }
43
+ return selected;
44
+ }
45
+
46
+ export function validateHtmlOnlyPrerender(route, rendered) {
47
+ if (!route.htmlOnly) return;
48
+ if (rendered.dynamic || rendered.partial || rendered.revalidate !== false || (rendered.status ?? 200) !== 200) {
49
+ throw new Error(`htmlOnly route ${route.pattern} must produce a complete successful static HTML document without dynamic data, partial rendering, redirects or ISR.`);
50
+ }
51
+ const html = Buffer.isBuffer(rendered.body) ? rendered.body.toString('utf8') : rendered.body;
52
+ assertHtmlOnlyDocument(html, route.pattern);
53
+ }
@@ -0,0 +1,67 @@
1
+ import path from 'node:path';
2
+ import { builtinModules } from 'node:module';
3
+ import { analyzeSource } from './source-analysis.mjs';
4
+ import { frameworkImportName, frameworkImportPattern } from './framework-imports.mjs';
5
+
6
+ const builtins = new Set([...builtinModules, ...builtinModules.map(name => 'node:' + name)]);
7
+ const interactive = new Set(['link','image','head','script','dynamic','router','compat/router','next-router-context','next-app-router-context']);
8
+ const namespace = /^prnext-(?:css-mapping|compiled-css|app-empty|edge-empty):/;
9
+
10
+ export function createHtmlOnlyGraph(root) {
11
+ const graph = new Map(), issues = new Map();
12
+ const file = value => path.resolve(root, value.replace(namespace, ''));
13
+ function issue(filename, reason) {
14
+ const key = file(filename);
15
+ if (!issues.has(key)) issues.set(key, reason);
16
+ }
17
+ return {
18
+ issue,
19
+ alias(source, target) {
20
+ if (file(source) !== file(target)) graph.set(file(source), [{ path:file(target) }]);
21
+ },
22
+ source(filename, source) {
23
+ const { program, directives, computedRequire, imports } = analyzeSource(source, filename);
24
+ if (directives.includes('use client')) issue(filename, 'a Client Component (use client)');
25
+ if (computedRequire) issue(filename, 'a computed require() whose dependencies cannot be validated');
26
+ for (const dependency of imports) if (frameworkImportPattern.test(dependency.path) && interactive.has(frameworkImportName(dependency.path))) issue(filename, `interactive framework import ${dependency.path}; use ordinary HTML elements instead`);
27
+ const pending = [program];
28
+ while (pending.length) {
29
+ const node = pending.pop();
30
+ if (node.directive === 'use server') issue(filename, 'a Server Action (use server)');
31
+ if (node.type === 'ImportExpression' && typeof node.source?.value !== 'string') issue(filename, 'a computed import() whose dependencies cannot be validated');
32
+ if (node.type === 'JSXOpeningElement' && node.name?.type === 'JSXIdentifier' && ['script','iframe','object','embed'].includes(node.name.name.toLowerCase())) issue(filename, `a <${node.name.name}> element`);
33
+ if (node.type === 'JSXAttribute' && node.name?.type === 'JSXIdentifier' && /^on[A-Z]/.test(node.name.name)) issue(filename, `interactive event property ${node.name.name}`);
34
+ for (const value of Object.values(node)) {
35
+ if (Array.isArray(value)) { for (const child of value) if (child?.type) pending.push(child); }
36
+ else if (value && typeof value === 'object' && value.type) pending.push(value);
37
+ }
38
+ }
39
+ },
40
+ record(metafile) {
41
+ for (const [name, input] of Object.entries(metafile?.inputs || {})) {
42
+ const key=file(name), dependencies=graph.get(key) || [];
43
+ for (const dependency of input.imports || []) {
44
+ if (!dependency.external) dependencies.push({path:file(dependency.path)});
45
+ else if (!builtins.has(dependency.path) && !/^(?:react|react-dom|react-server-dom-webpack)(?:\/|$)/.test(dependency.path) && !/^\.\.\/compat\/(?:navigation|headers|cache|server|font-local|font-google|og)\.cjs$/.test(dependency.path)) {
46
+ dependencies.push({external:dependency.path});
47
+ }
48
+ }
49
+ graph.set(key,dependencies);
50
+ }
51
+ },
52
+ validate(route) {
53
+ const roots = new Set([route.file, ...Object.values(route.segments || {}).flatMap(segment => Object.values(segment).filter(value => typeof value === 'string' && /\.[cm]?[jt]sx?$/.test(value))), ...(route.metadataFiles || []).map(item => item.file)].filter(Boolean));
54
+ const pending=[...roots].map(file), seen=new Set();
55
+ while (pending.length) {
56
+ const current=pending.pop();
57
+ if (seen.has(current)) continue;
58
+ seen.add(current);
59
+ if (issues.has(current)) throw new Error(`htmlOnly route ${route.pattern} reaches ${issues.get(current)} in ${path.relative(root,current)}.`);
60
+ for (const dependency of graph.get(current) || []) {
61
+ if (dependency.external) throw new Error(`htmlOnly route ${route.pattern} reaches unverified external dependency ${dependency.external} from ${path.relative(root,current)}. Bundle a static server dependency or remove it from this HTML-only route.`);
62
+ pending.push(dependency.path);
63
+ }
64
+ }
65
+ },
66
+ };
67
+ }