@thomas.f/prnext 0.1.0-alpha.1

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 (211) hide show
  1. package/README.md +54 -0
  2. package/build/action-exports.mjs +107 -0
  3. package/build/actions.mjs +214 -0
  4. package/build/app-config.mjs +138 -0
  5. package/build/app-routing.mjs +119 -0
  6. package/build/app-static.mjs +158 -0
  7. package/build/app.mjs +536 -0
  8. package/build/cache-components.mjs +91 -0
  9. package/build/cache-handlers.mjs +54 -0
  10. package/build/check.mjs +40 -0
  11. package/build/compiler.mjs +222 -0
  12. package/build/config.mjs +193 -0
  13. package/build/css-modules.mjs +114 -0
  14. package/build/custom-routes.mjs +363 -0
  15. package/build/dev-worker.mjs +18 -0
  16. package/build/dev.mjs +76 -0
  17. package/build/directory-cache.mjs +29 -0
  18. package/build/dynamic.mjs +125 -0
  19. package/build/edge.mjs +141 -0
  20. package/build/font-data.LICENSE.txt +25 -0
  21. package/build/font-data.json +1 -0
  22. package/build/font-files.mjs +221 -0
  23. package/build/fonts.mjs +131 -0
  24. package/build/framework-imports.mjs +17 -0
  25. package/build/i18n.mjs +39 -0
  26. package/build/images-config.mjs +62 -0
  27. package/build/images.mjs +86 -0
  28. package/build/incremental.mjs +76 -0
  29. package/build/index.mjs +356 -0
  30. package/build/loader-cache.mjs +69 -0
  31. package/build/metadata-images.mjs +72 -0
  32. package/build/metadata-routes.mjs +77 -0
  33. package/build/middleware.mjs +231 -0
  34. package/build/migrate.mjs +289 -0
  35. package/build/module-resolution.mjs +78 -0
  36. package/build/native-discovery.mjs +134 -0
  37. package/build/native-packages.mjs +105 -0
  38. package/build/npm-packages.mjs +110 -0
  39. package/build/og.mjs +14 -0
  40. package/build/page-extensions.mjs +12 -0
  41. package/build/passthrough-loader.cjs +4 -0
  42. package/build/precompress.mjs +122 -0
  43. package/build/scan.mjs +178 -0
  44. package/build/script-workers.mjs +25 -0
  45. package/build/server-actions-config.mjs +27 -0
  46. package/build/source-maps.mjs +34 -0
  47. package/build/standalone-config.mjs +19 -0
  48. package/build/standalone-runner.mjs +21 -0
  49. package/build/standalone-worker.mjs +5 -0
  50. package/build/standalone.mjs +240 -0
  51. package/build/static-export.mjs +93 -0
  52. package/build/styles.mjs +174 -0
  53. package/build/transform-cache.mjs +50 -0
  54. package/build/transform.mjs +155 -0
  55. package/build/turbopack-conditions.mjs +59 -0
  56. package/build/webpack-bridge.cjs +3 -0
  57. package/build/webpack-graph.mjs +256 -0
  58. package/build/webpack-loader.cjs +9 -0
  59. package/cli.mjs +210 -0
  60. package/compat/app-client-page.cjs +32 -0
  61. package/compat/app-context.cjs +40 -0
  62. package/compat/app-context.d.cts +20 -0
  63. package/compat/app-dynamic.cjs +34 -0
  64. package/compat/app-error-boundary.cjs +53 -0
  65. package/compat/app-global-error-boundary.cjs +34 -0
  66. package/compat/app-layout-context.cjs +27 -0
  67. package/compat/app-navigation-boundary.cjs +47 -0
  68. package/compat/app.cjs +33 -0
  69. package/compat/app.d.cts +9 -0
  70. package/compat/cache-handlers.cjs +223 -0
  71. package/compat/cache-life.cjs +34 -0
  72. package/compat/cache.cjs +99 -0
  73. package/compat/cache.d.cts +36 -0
  74. package/compat/compat/router.d.cts +2 -0
  75. package/compat/compat-router.cjs +7 -0
  76. package/compat/constants.cjs +8 -0
  77. package/compat/constants.d.cts +7 -0
  78. package/compat/cookies.cjs +129 -0
  79. package/compat/data-cache.cjs +389 -0
  80. package/compat/dist/shared/lib/app-router-context.shared-runtime.d.cts +3 -0
  81. package/compat/dist/shared/lib/router-context.d.cts +1 -0
  82. package/compat/dist/shared/lib/router-context.shared-runtime.d.cts +3 -0
  83. package/compat/document.cjs +73 -0
  84. package/compat/document.d.cts +41 -0
  85. package/compat/draft.cjs +28 -0
  86. package/compat/dynamic-bailout.cjs +13 -0
  87. package/compat/dynamic.cjs +119 -0
  88. package/compat/dynamic.d.cts +32 -0
  89. package/compat/error.cjs +36 -0
  90. package/compat/error.d.cts +15 -0
  91. package/compat/fetch.d.cts +10 -0
  92. package/compat/font/google.d.cts +1 -0
  93. package/compat/font/local.d.cts +2 -0
  94. package/compat/font-google.cjs +2 -0
  95. package/compat/font-google.d.cts +1944 -0
  96. package/compat/font-local.cjs +5 -0
  97. package/compat/font-local.d.cts +3 -0
  98. package/compat/font-types.d.cts +25 -0
  99. package/compat/head.cjs +180 -0
  100. package/compat/head.d.cts +3 -0
  101. package/compat/headers.cjs +89 -0
  102. package/compat/headers.d.cts +26 -0
  103. package/compat/image-config.cjs +3 -0
  104. package/compat/image-imports.d.ts +12 -0
  105. package/compat/image-shared.cjs +62 -0
  106. package/compat/image.cjs +64 -0
  107. package/compat/image.d.cts +35 -0
  108. package/compat/incremental-cache.cjs +186 -0
  109. package/compat/index.d.ts +159 -0
  110. package/compat/instant-samples.cjs +39 -0
  111. package/compat/instant.cjs +13 -0
  112. package/compat/link.cjs +68 -0
  113. package/compat/link.d.cts +17 -0
  114. package/compat/locale.cjs +30 -0
  115. package/compat/metadata.d.cts +77 -0
  116. package/compat/navigation-controls.cjs +27 -0
  117. package/compat/navigation-server.cjs +25 -0
  118. package/compat/navigation.cjs +52 -0
  119. package/compat/navigation.d.cts +28 -0
  120. package/compat/next-app-router-context.cjs +6 -0
  121. package/compat/next-router-context.cjs +3 -0
  122. package/compat/og.cjs +32 -0
  123. package/compat/og.d.cts +1 -0
  124. package/compat/paths.cjs +33 -0
  125. package/compat/preview.cjs +41 -0
  126. package/compat/rewrite.cjs +41 -0
  127. package/compat/route-request.cjs +64 -0
  128. package/compat/router.cjs +137 -0
  129. package/compat/router.d.cts +58 -0
  130. package/compat/script-context.cjs +5 -0
  131. package/compat/script-loader.cjs +135 -0
  132. package/compat/script.cjs +69 -0
  133. package/compat/script.d.cts +17 -0
  134. package/compat/server-primitives.cjs +109 -0
  135. package/compat/server.cjs +13 -0
  136. package/compat/server.d.cts +37 -0
  137. package/compat/static-generation.cjs +108 -0
  138. package/compat/use-cache.cjs +129 -0
  139. package/index.mjs +1 -0
  140. package/native/platforms.json +6 -0
  141. package/native/resolve.mjs +81 -0
  142. package/package.json +127 -0
  143. package/release/verify.mjs +59 -0
  144. package/runtime/action-client.mjs +19 -0
  145. package/runtime/action-crypto.mjs +138 -0
  146. package/runtime/action-reference.mjs +2 -0
  147. package/runtime/action-server.mjs +161 -0
  148. package/runtime/action-ssr.mjs +5 -0
  149. package/runtime/api.mjs +326 -0
  150. package/runtime/app-client.mjs +539 -0
  151. package/runtime/app-content.mjs +8 -0
  152. package/runtime/app-errors.mjs +12 -0
  153. package/runtime/app-instant.mjs +30 -0
  154. package/runtime/app-metadata.mjs +410 -0
  155. package/runtime/app-partial-model.mjs +228 -0
  156. package/runtime/app-partial.mjs +130 -0
  157. package/runtime/app-prefetch.mjs +34 -0
  158. package/runtime/app-recovery.mjs +87 -0
  159. package/runtime/app-render.mjs +544 -0
  160. package/runtime/app-routing-access.mjs +143 -0
  161. package/runtime/app-routing-context.mjs +35 -0
  162. package/runtime/app-routing-state.mjs +61 -0
  163. package/runtime/app-routing.mjs +323 -0
  164. package/runtime/app-segments.mjs +22 -0
  165. package/runtime/app-static-params.mjs +120 -0
  166. package/runtime/app-static.mjs +86 -0
  167. package/runtime/app-stream-channel.mjs +92 -0
  168. package/runtime/app-stream-html.mjs +279 -0
  169. package/runtime/build-directory.mjs +48 -0
  170. package/runtime/cache-arguments.mjs +33 -0
  171. package/runtime/cache-components-rsc.mjs +64 -0
  172. package/runtime/cache-request.mjs +23 -0
  173. package/runtime/client-navigation.mjs +40 -0
  174. package/runtime/dev-client.mjs +129 -0
  175. package/runtime/dev-file-changes.mjs +110 -0
  176. package/runtime/document.mjs +96 -0
  177. package/runtime/edge.mjs +105 -0
  178. package/runtime/env.mjs +68 -0
  179. package/runtime/fetch-cache.mjs +379 -0
  180. package/runtime/font-preload.mjs +4 -0
  181. package/runtime/http.mjs +190 -0
  182. package/runtime/incremental-cache.mjs +83 -0
  183. package/runtime/metadata-route.mjs +83 -0
  184. package/runtime/middleware.mjs +186 -0
  185. package/runtime/module-loader.mjs +21 -0
  186. package/runtime/navigation.mjs +16 -0
  187. package/runtime/og-loader.mjs +17 -0
  188. package/runtime/pages-client.mjs +197 -0
  189. package/runtime/pages-error-props.mjs +6 -0
  190. package/runtime/pages-errors.mjs +85 -0
  191. package/runtime/pages-initial-props.mjs +43 -0
  192. package/runtime/pages-navigation.mjs +565 -0
  193. package/runtime/pages-paths.mjs +80 -0
  194. package/runtime/pages-revalidate.mjs +33 -0
  195. package/runtime/partial-artifact-cache.mjs +37 -0
  196. package/runtime/partial-template.mjs +174 -0
  197. package/runtime/profile.mjs +9 -0
  198. package/runtime/profiles.json +47 -0
  199. package/runtime/render.mjs +304 -0
  200. package/runtime/request-transport.mjs +41 -0
  201. package/runtime/request-work.mjs +7 -0
  202. package/runtime/route-static.mjs +117 -0
  203. package/runtime/rsc-worker.mjs +426 -0
  204. package/runtime/script-html.mjs +61 -0
  205. package/runtime/stream-select.mjs +25 -0
  206. package/runtime/stream-utils.mjs +127 -0
  207. package/runtime/transport.mjs +108 -0
  208. package/runtime/watch-project.mjs +130 -0
  209. package/runtime/worker-artifacts.mjs +22 -0
  210. package/runtime/worker-metadata.mjs +31 -0
  211. package/runtime/worker.mjs +197 -0
@@ -0,0 +1,76 @@
1
+ import { context } from 'esbuild';
2
+ import { readFile, writeFile, mkdir, realpath } from 'node:fs/promises';
3
+ import { createHash } from 'node:crypto';
4
+ import path from 'node:path';
5
+ import {clearDirectoryCache} from './directory-cache.mjs';
6
+ const contexts = new Map();
7
+ const LIMIT = 8;
8
+ let created = 0, reused = 0;
9
+ export const incrementalCompilerStats = () => ({contexts:contexts.size, created, reused});
10
+ export async function closeIncrementalCompiler() {
11
+ clearDirectoryCache();
12
+ const active=[...contexts.values()];contexts.clear();await Promise.all(active.map(slot=>slot.context.dispose()));
13
+ }
14
+ /** Retain esbuild's parsed module graphs while rebinding per-build framework plugins. */
15
+ export async function incrementalBuild(options, {root,stage,group='project',run=callback=>callback()}) {
16
+ const canonicalRoot = await realpath(root);
17
+ if (canonicalRoot !== root) {
18
+ const canonical = value => typeof value === 'string' ? value.replaceAll(root, canonicalRoot) : Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' && !(value instanceof RegExp) && !ArrayBuffer.isView(value) ? Object.fromEntries(Object.entries(value).map(([key,item]) => [key,canonical(item)])) : value;
19
+ return incrementalBuild(canonical(options),{root:canonicalRoot,stage:stage.replace(root,canonicalRoot),group,run});
20
+ }
21
+ const virtual=path.join(root,'.prnext-incremental-stage');
22
+ const toVirtual=value=>typeof value==='string'?value.replaceAll(stage,virtual):value;
23
+ const fromVirtual=value=>typeof value==='string'?value.replaceAll(virtual,stage):value;
24
+ const deep=(value,map)=>typeof value==='string'?map(value):Array.isArray(value)?value.map(item=>deep(item,map)):value&&typeof value==='object'&&!(value instanceof RegExp)&&!ArrayBuffer.isView(value)?Object.fromEntries(Object.entries(value).map(([key,item])=>[map(key),deep(item,map)])):value;
25
+ const realResult=result=>result && {...result,...(result.path?{path:fromVirtual(result.path)}:{}),...(result.resolveDir?{resolveDir:fromVirtual(result.resolveDir)}:{})};
26
+ const virtualResult=result=>result && {...result,...(result.path?{path:toVirtual(result.path)}:{}),...(result.resolveDir?{resolveDir:toVirtual(result.resolveDir)}:{}),...(result.watchFiles?{watchFiles:result.watchFiles.map(toVirtual)}:{}),...(result.watchDirs?{watchDirs:result.watchDirs.map(toVirtual)}:{})};
27
+ const hooks=[];
28
+ const initialOptions={...options,plugins:undefined,write:false};
29
+ let slot;
30
+ const resolve=async(specifier,settings)=>realResult(await slot.builder.resolve(specifier,settings));
31
+ for(const plugin of options.plugins || []) await plugin.setup({initialOptions,resolve,
32
+ onResolve(settings,callback){hooks.push({kind:'onResolve',settings,callback});},onLoad(settings,callback){hooks.push({kind:'onLoad',settings,callback});},
33
+ onStart(callback){hooks.push({kind:'onStart',callback});},onEnd(callback){hooks.push({kind:'onEnd',callback});},onDispose(callback){hooks.push({kind:'onDispose',callback});},
34
+ esbuild:await import('esbuild'),
35
+ });
36
+ const normalized=deep(initialOptions,toVirtual);
37
+ const signature=JSON.stringify([normalized,hooks.map(({kind,settings})=>[kind,settings?.namespace,settings?.filter?.source,settings?.filter?.flags]),(options.plugins||[]).map(plugin=>plugin.name)]);
38
+ const key=group+':'+createHash('sha256').update(signature).digest('hex');
39
+ slot=contexts.get(key);
40
+ if(slot){contexts.delete(key);reused++;}
41
+ else {
42
+ while([...contexts.values()].filter(slot=>slot.group===group).length>=(group==='auxiliary'?2:LIMIT)){const [oldest,previous]=[...contexts].find(([,slot])=>slot.group===group);contexts.delete(oldest);await previous.context.dispose();}
43
+ created++;slot={group};
44
+ slot.current={hooks,run,toVirtual,fromVirtual,virtualResult,stage};
45
+ slot.context=await context({...normalized,plugins:[{name:'prnext-incremental-delegates',setup(builder){
46
+ slot.builder=builder;
47
+ builder.onResolve({filter:/.*/},async args=>{
48
+ const active=slot.current;
49
+ if(args.pluginData?.prnextVirtualResolution || !(args.path.startsWith(virtual+path.sep)||args.path.startsWith(active.stage+path.sep)||(args.resolveDir===virtual || args.resolveDir.startsWith(virtual+path.sep))))return;
50
+ const result=await builder.resolve(active.fromVirtual(args.path),{resolveDir:active.fromVirtual(args.resolveDir),importer:active.fromVirtual(args.importer),kind:args.kind,with:args.with,pluginData:{...args.pluginData,prnextVirtualResolution:true}});
51
+ const pluginData={...(result.pluginData || args.pluginData)}; delete pluginData.prnextVirtualResolution;
52
+ return {...active.virtualResult(result),pluginData};
53
+ });
54
+ hooks.forEach(({kind,settings},index)=>{
55
+ const callback=args=>{
56
+ const active=slot.current;
57
+ return active.run(async()=>active.virtualResult(await active.hooks[index].callback(args&&{...args,path:active.fromVirtual(args.path),importer:active.fromVirtual(args.importer),resolveDir:active.fromVirtual(args.resolveDir)})));
58
+ };
59
+ if(settings)builder[kind](settings,callback);else builder[kind](callback);
60
+ });
61
+ builder.onLoad({filter:/.*/,namespace:'file'},async args=>{
62
+ if(!args.path.startsWith(virtual+path.sep))return;
63
+ const actual=slot.current.fromVirtual(args.path),extension=path.extname(actual);
64
+ return {contents:await readFile(actual),loader:options.loader?.[extension] || ({'.json':'json','.ts':'ts','.tsx':'tsx','.jsx':'jsx','.css':'css'}[extension] || 'js'),resolveDir:path.dirname(args.path)};
65
+ });
66
+ }}]});
67
+ }
68
+ slot.current={hooks,run,toVirtual,fromVirtual,virtualResult,stage};
69
+ contexts.set(key,slot);
70
+ const result=await slot.context.rebuild();
71
+ const relativeVirtual=path.basename(virtual),relativeStage=path.basename(stage);
72
+ const metadata=value=>fromVirtual(value).replaceAll(relativeVirtual,relativeStage);
73
+ const files=result.outputFiles.map(file=>({path:fromVirtual(file.path),contents:file.contents,get text(){return Buffer.from(this.contents).toString();},hash:file.hash}));
74
+ if(options.write!==false)for(const file of files){await mkdir(path.dirname(file.path),{recursive:true});await writeFile(file.path,file.contents);}
75
+ return {...result,...(result.metafile?{metafile:deep(result.metafile,metadata)}:{}),...(options.write===false?{outputFiles:files}:{outputFiles:undefined})};
76
+ }
@@ -0,0 +1,356 @@
1
+ import {frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
2
+ import { expandLocales, localizedStaticPaths } from './i18n.mjs';
3
+ import { build as bundle, withCompilation, configureCompiler, compilerSource } from './compiler.mjs';
4
+ import { mkdir, readFile, writeFile, rename, rm, cp, stat, readdir } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+ import { createRequire } from 'node:module';
8
+ import { randomUUID, randomBytes, createHash } from 'node:crypto';
9
+ import { prepareBuildDirectory, OUTPUT_POINTER } from '../runtime/build-directory.mjs';
10
+ import { moduleResolutionPlugin } from './module-resolution.mjs';
11
+ import { scanProject } from './scan.mjs';
12
+ import { stripServerCode, assertSupportedSource, rewriteServerChunks } from './transform.mjs';
13
+ import { transformDynamicImports } from './dynamic.mjs';
14
+ import { createCssModules } from './css-modules.mjs';
15
+ import { createFonts } from './fonts.mjs';
16
+ import { createImages } from './images.mjs';
17
+ import { compileApp } from './app.mjs';
18
+ import { prerenderApp } from './app-static.mjs';
19
+ import { precompressBuild } from './precompress.mjs';
20
+ import { loadProjectConfig, defineEnvironment, generateBuildId, publicAssetBase } from './config.mjs';
21
+ import { compileCustomRoutes } from './custom-routes.mjs';
22
+ import { compileMiddleware } from './middleware.mjs';
23
+ import { prepareScriptWorkers } from './script-workers.mjs';
24
+ import { prepareMetadataRoutes } from './metadata-routes.mjs';
25
+ import { prepareImageResponse } from './og.mjs';
26
+ import { prepareCacheHandler, prepareCacheHandlers } from './cache-handlers.mjs';
27
+ import { npmPackagesPlugin } from './npm-packages.mjs';
28
+ import { createRefreshTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
29
+ import { snapshotEnvConfig } from '../runtime/env.mjs';
30
+ import { validateStaticPaths } from '../runtime/pages-paths.mjs';
31
+ export { staticPath } from '../runtime/pages-paths.mjs';
32
+
33
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
+ const compatibleModules = new Set(['link', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'image', 'cache', 'dynamic', 'error', 'document', 'app', 'script', 'og']);
35
+ const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx' };
36
+ const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
37
+
38
+ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, fonts, refresh }) {
39
+ return {
40
+ name: 'prnext-pages-compatibility',
41
+ setup(esbuild) {
42
+ esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, args => args.importer === documentFile
43
+ ? { errors: [{ text: 'CSS cannot be imported within pages/_document. Move global styles to pages/_app, or return collected styles from Document.getInitialProps.' }] } : undefined);
44
+ esbuild.onResolve({ filter: frameworkRuntimeImportPattern }, args => {
45
+ const name = frameworkImportName(args.path);
46
+ if (!compatibleModules.has(name)) return { errors: [{ text: `PRNext does not implement ${args.path} yet (imported by ${args.importer}). Supported runtime imports: next/link, next/head, next/router, next/image, next/cache, next/dynamic, next/error, next/document, next/app, next/script.` }] };
47
+ if (browser && name === 'document') return { errors: [{ text: `${args.path} is server-only. Import Document, Html, Head, Main and NextScript from pages/_document, not from a browser page or component.` }] };
48
+ if (browser && ['cache', 'og'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a browser component.` }] };
49
+ const file = name === 'compat/router' ? 'compat-router' : name;
50
+ return browser ? { path: path.join(packageRoot, 'compat', file + '.cjs') } : { path: `../compat/${file}.cjs`, external: true };
51
+ });
52
+ if (browser) {
53
+ const resolveFromProject = createRequire(path.join(projectRoot, 'package.json'));
54
+ esbuild.onResolve({ filter: /^(?:react|react-dom)(?:\/|$)/ }, args => ({ path: resolveFromProject.resolve(args.path) }));
55
+ esbuild.onResolve({ filter: /^server-only$/ }, () => ({ errors: [{ text: 'A server-only module is reachable from a browser component. Move its imports into getServerSideProps/getStaticProps dependencies.' }] }));
56
+ } else {
57
+ esbuild.onResolve({ filter: /^server-only$/ }, () => ({ path: 'server-only', namespace: 'prnext-empty' }));
58
+ esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, args => {
59
+ if (/\.module\.(?:css|scss|sass)$/i.test(args.path) || args.pluginData?.prnextResolvingCss) return;
60
+ return { path: args.path, namespace: 'prnext-empty' };
61
+ });
62
+ esbuild.onLoad({ filter: /.*/, namespace: 'prnext-empty' }, () => ({ contents: '', loader: 'js' }));
63
+ }
64
+ esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
65
+ if (browser && args.path === documentFile) return { errors: [{ text: 'pages/_document is server-only and cannot be imported by a browser page or component.' }] };
66
+ const source = await fonts.transform(args.prnextSource ?? await readFile(args.path, 'utf8'), args.path);
67
+ assertSupportedSource(source, args.path);
68
+ const dynamicSource = transformDynamicImports(source, args.path, { projectRoot, mode: browser ? 'browser' : 'server' });
69
+ let contents = browser && pageFiles.has(args.path) ? stripServerCode(dynamicSource, args.path) : dynamicSource;
70
+ if (browser && refresh) contents = await refresh(contents, args.path);
71
+ return { contents, loader: extensions[path.extname(args.path)] || 'jsx', resolveDir: path.dirname(args.path) };
72
+ });
73
+ },
74
+ };
75
+ }
76
+
77
+ function browserEntry(route, app, manifestUrl, paths, development) {
78
+ return `${development ? `import ${JSON.stringify(devClientFile)};\n` : ''}import Page from ${JSON.stringify(route.file)};
79
+ ${app ? `import App from ${JSON.stringify(app)};` : 'const App=undefined;'}
80
+ import {bootstrapPages} from ${JSON.stringify(path.join(packageRoot, 'runtime/pages-client.mjs'))};
81
+ export {Page,App};
82
+ bootstrapPages({Page,App,pattern:${JSON.stringify(route.pattern)},manifestUrl:${JSON.stringify(manifestUrl)},...${JSON.stringify(paths)}${development ? `,dev:${JSON.stringify(development)}` : ''}});
83
+ `;
84
+ }
85
+
86
+ function isDynamic(pattern) { return pattern.includes('['); }
87
+
88
+ async function addBuiltinPagesErrors(project, stage) {
89
+ if (!project.pageRoot || project.pagesErrors?.error) return;
90
+ project.pagesErrors ||= {};
91
+ for (const [kind, status] of [['notFound', 404], ['serverError', 500], ['error', undefined]]) {
92
+ if (project.pagesErrors[kind]) continue;
93
+ // Internal artifacts must not occupy real App routes named /404 or /500.
94
+ const pattern = status ? `/_prnext/errors/${status}` : '/_error';
95
+ const id = `page-builtin-error-${status || 'fallback'}`;
96
+ const file = path.join(stage, '.entries', `${id}.jsx`);
97
+ await writeFile(file, status
98
+ ? `import ErrorComponent from 'prnext/error';export default function BuiltinError(props){return <ErrorComponent {...props} statusCode={${status}}/>}`
99
+ : `export {default} from 'prnext/error';`);
100
+ project.routes.push({ id, pattern, kind: 'page', file, internal: true,
101
+ ...(status ? { errorStatus: status } : {}),
102
+ });
103
+ project.pagesErrors[kind] = id;
104
+ }
105
+ }
106
+
107
+ async function prerender(stage, manifest, dev) {
108
+ const { prerenderRoute, renderFallback } = await import(pathToFileURL(path.join(stage, 'runtime/render.mjs')).href);
109
+ const paths = new Set();
110
+ const seedsByRoute = new Map();
111
+ const validatedDocuments = new Set();
112
+ const staticListings = new Map();
113
+ // Error page data is available when another static page returns notFound.
114
+ const ordered = [...manifest.routes].sort((left, right) => Number(!!right.errorStatus) - Number(!!left.errorStatus));
115
+ for (const route of ordered) {
116
+ if (route.kind !== 'page' || route.router === 'app') continue;
117
+ const modulePath = path.join(stage, route.module);
118
+ const page = await import(pathToFileURL(modulePath).href);
119
+ if (page.__prnextDocument && !validatedDocuments.has(page.__prnextDocument)) {
120
+ for (const name of ['getStaticProps', 'getStaticPaths', 'getServerSideProps']) {
121
+ if (Object.hasOwn(page.__prnextDocument, name) || typeof page.Document?.[name] === 'function') throw new Error(`pages/_document does not support ${name}. Use Document.getInitialProps for server document rendering.`);
122
+ }
123
+ validatedDocuments.add(page.__prnextDocument);
124
+ }
125
+ if (route.errorStatus && (page.getServerSideProps || page.default?.getInitialProps)) throw new Error(`pages${route.pattern} must be static and cannot use getServerSideProps or getInitialProps. Use getStaticProps instead.`);
126
+ route.gip = typeof page.default?.getInitialProps === 'function';
127
+ route.appGip = !!page.App && page.App.getInitialProps !== page.App.origGetInitialProps;
128
+ if (route.internal && route.errorStatus && route.appGip) {
129
+ // A custom App hook makes the built-in Error request-dependent. Select
130
+ // its actual _error module so the default status props and visible URL
131
+ // reach App.getInitialProps instead of a synthetic static wrapper.
132
+ const kind = route.errorStatus === 404 ? 'notFound' : 'serverError';
133
+ if (manifest.pagesErrors?.[kind] === route.id) delete manifest.pagesErrors[kind];
134
+ }
135
+ if (route.gip && (page.getStaticProps || page.getServerSideProps)) throw new Error(`Page ${route.pattern} cannot combine getInitialProps with getStaticProps or getServerSideProps.`);
136
+ if (route.internal && !route.errorStatus) {
137
+ if (page.getStaticProps || page.getServerSideProps || page.getStaticPaths) throw new Error('pages/_error does not support getStaticProps, getServerSideProps or getStaticPaths. Use ErrorComponent.getInitialProps instead.');
138
+ continue;
139
+ }
140
+ if (page.getServerSideProps && page.getStaticProps) throw new Error(`Page ${route.pattern} exports both getServerSideProps and getStaticProps. Choose one.`);
141
+ if (page.getStaticPaths && !page.getStaticProps) throw new Error(`Page ${route.pattern} exports getStaticPaths without getStaticProps.`);
142
+ if (page.getServerSideProps) { route.ssp = true; continue; }
143
+ if (page.getStaticProps) route.ssg = true;
144
+ else if (route.gip || route.appGip) continue;
145
+ let entries = [{ path: route.pattern, params: {} }];
146
+ if (isDynamic(route.pattern)) {
147
+ if (!page.getStaticProps) continue;
148
+ if (!page.getStaticPaths) throw new Error(`Dynamic page ${route.pattern} with getStaticProps must export getStaticPaths.`);
149
+ if (!staticListings.has(modulePath)) staticListings.set(modulePath, Promise.resolve().then(() => page.getStaticPaths(manifest.config.i18n ? {locales: manifest.config.i18n.locales, defaultLocale: manifest.config.i18n.defaultLocale} : {})));
150
+ const result = localizedStaticPaths(route, await staticListings.get(modulePath), manifest.config.i18n);
151
+ route.fallback = result.fallback;
152
+ entries = result.paths;
153
+ } else if (page.getStaticPaths) throw new Error(`getStaticPaths is only valid on dynamic pages (${route.pattern}).`);
154
+ for (const entry of entries) {
155
+ if (paths.has(entry.path)) throw new Error(`Multiple pages prerender the same path: ${entry.path}.`);
156
+ paths.add(entry.path);
157
+ const rendered = await prerenderRoute({ ...manifest.config, manifest, distDir: stage, route, modulePath, path: entry.path, params: entry.params, client: route.client, css: route.css, pattern: route.pattern, errorStatus: route.errorStatus, buildId: manifest.buildId, production: !dev });
158
+ const file = `static/${createHash('sha256').update(entry.path).digest('hex').slice(0, 20)}.html`;
159
+ await writeFile(path.join(stage, file), rendered.body);
160
+ const seed = { routeId: route.id, path: entry.path, file, status: route.errorStatus || rendered.status || 200, headers: rendered.headers || {} };
161
+ if (route.ssg) {
162
+ seed.dataFile = file.replace(/\.html$/, '.json');
163
+ seed.revalidate = rendered.revalidate ?? false;
164
+ seed.generatedAt = rendered.generatedAt;
165
+ await writeFile(path.join(stage, seed.dataFile), rendered.dataJSON);
166
+ }
167
+ manifest.prerendered.push(seed);
168
+ if (!seedsByRoute.has(route.id)) seedsByRoute.set(route.id, []);
169
+ seedsByRoute.get(route.id).push(seed);
170
+ }
171
+ if (route.fallback === true) {
172
+ const rendered = await renderFallback({ ...manifest.config, modulePath, path: route.pattern, params: {}, client: route.client, css: route.css, pattern: route.pattern, buildId: manifest.buildId, production: !dev });
173
+ route.fallbackFile = `static/fallback-${route.id}.html`;
174
+ await writeFile(path.join(stage, route.fallbackFile), rendered.body);
175
+ }
176
+ }
177
+ // Rendering errors first must not change the published route/seed ordering.
178
+ manifest.prerendered = manifest.routes.flatMap(route => seedsByRoute.get(route.id) || []);
179
+ }
180
+
181
+ let buildQueue = Promise.resolve();
182
+ /** Isolate process.env between programmatic builds, including simultaneous calls. */
183
+ export function build(projectRoot, options = {}) {
184
+ const task = buildQueue.then(async () => {
185
+ const restore = snapshotEnvConfig();
186
+ const envMode = options.envMode || (process.env.NODE_ENV === 'test' ? 'test' : options.dev ? 'development' : 'production');
187
+ process.env.NODE_ENV = options.dev ? 'development' : 'production';
188
+ try { return await withCompilation(() => buildProject(projectRoot, { ...options, envMode })); }
189
+ finally { restore(); }
190
+ });
191
+ buildQueue = task.catch(() => {});
192
+ return task;
193
+ }
194
+
195
+ /** Compile Pages Router and App Router projects. A failed build preserves .prnext. */
196
+ async function buildProject(projectRoot, { dev = false, envMode, incremental = false, devInputs } = {}) {
197
+ const started = performance.now();
198
+ const config = await loadProjectConfig(projectRoot, { dev, envMode });
199
+ const environment = defineEnvironment(config);
200
+ const publicPaths = { trailingSlash: config.trailingSlash, skipTrailingSlashRedirect: config.skipTrailingSlashRedirect, basePath: config.basePath, assetPrefix: config.assetPrefix, assetBase: publicAssetBase(config) };
201
+ const customRoutes = await compileCustomRoutes(config);
202
+ const buildId = await generateBuildId(config, { dev });
203
+ const project = await scanProject(projectRoot, { basePath: config.basePath, pageExtensions: config.pageExtensions, i18n: config.i18n });
204
+ const stage = path.join(project.root, `.prnext-build-${randomUUID()}`);
205
+ const destination = await prepareBuildDirectory(project.root, config.distDir);
206
+ const backup = path.join(project.root, `.prnext-backup-${randomUUID()}`);
207
+ await mkdir(path.join(stage, '.entries'), { recursive: true });
208
+ await mkdir(path.join(stage, 'static'), { recursive: true });
209
+ await addBuiltinPagesErrors(project, stage);
210
+ const manifest = { version: 1, dev, buildId, cacheId: randomUUID(), previewModeId: randomUUID().replaceAll('-', ''), previewModeEncryptionKey: randomBytes(32).toString('hex'),
211
+ config: { i18n: config.i18n, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, compress: config.compress, poweredByHeader: config.poweredByHeader, reactStrictMode: config.reactStrictMode, cacheComponents: config.cacheComponents, cacheLife: config.cacheLife, images: config.images, ...publicPaths }, customRoutes, routes: [], prerendered: [],
212
+ ...(project.pagesErrors ? { pagesErrors: project.pagesErrors } : {}) };
213
+ // Its build-unique name is known before bundling and avoids a hash cycle
214
+ // between page entry URLs and the manifest that lists those entries.
215
+ const pagesManifestUrl = `${publicPaths.assetBase}/pages-manifest-${manifest.cacheId}.json`;
216
+ if (dev) manifest.devClient = `${publicPaths.assetBase}/dev-manifest-${manifest.cacheId}.json`;
217
+ const development = dev ? { buildId, clientManifest: manifest.devClient } : undefined;
218
+ const refresh = dev ? createRefreshTransform(project.root) : undefined;
219
+ const fonts = createFonts({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase });
220
+ const cssModules = createCssModules({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, sassOptions: config.sassOptions });
221
+ configureCompiler({config,root:project.root,stage,buildId,dev,incremental,devInputs,assetBase:publicPaths.assetBase});
222
+ try {
223
+ await mkdir(path.join(stage, 'runtime'), { recursive: true });
224
+ await mkdir(path.join(stage, 'compat'), { recursive: true });
225
+ for (const name of await readdir(path.join(packageRoot, 'runtime'))) {
226
+ if (name.endsWith('.mjs') && !name.endsWith('.test.mjs')) await cp(path.join(packageRoot, 'runtime', name), path.join(stage, 'runtime', name));
227
+ }
228
+ await cp(path.join(packageRoot, 'runtime/profiles.json'), path.join(stage, 'runtime/profiles.json'));
229
+ // Runtime copies must work even when dotenv is nested beneath the PRNext
230
+ // package instead of hoisted beside the application's dependencies.
231
+ await bundle({ entryPoints: [path.join(packageRoot, 'runtime/env.mjs')], outfile: path.join(stage, 'runtime/env.mjs'), bundle: true, platform: 'node', format: 'esm', target: 'node22', logLevel: 'silent', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" } });
232
+ for (const name of await readdir(path.join(packageRoot, 'compat'))) {
233
+ if (name.endsWith('.cjs')) await cp(path.join(packageRoot, 'compat', name), path.join(stage, 'compat', name));
234
+ }
235
+ await prepareImageResponse(stage);
236
+ manifest.config.serverActions = config.experimental.serverActions;
237
+ const cacheHandlers = await prepareCacheHandlers({ config, projectRoot: project.root, stage, environment });
238
+ if (Object.keys(cacheHandlers).length) manifest.config.cacheHandlers = cacheHandlers;
239
+ const cacheHandler = await prepareCacheHandler({ config, projectRoot: project.root, stage, environment });
240
+ if (cacheHandler) manifest.config.cacheHandler = cacheHandler;
241
+ if (config.cacheMaxMemorySize !== undefined) manifest.config.cacheMaxMemorySize = config.cacheMaxMemorySize;
242
+ const images = await createImages({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, config: config.images });
243
+ if (config.experimental.nextScriptWorkers) manifest.scriptWorkers = await prepareScriptWorkers({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase });
244
+ const serverEntries = {};
245
+ const clientEntries = {};
246
+ const pageFiles = new Set([...project.routes.filter(route => route.kind === 'page').map(route => route.file), ...(project.app ? [project.app] : [])]);
247
+ for (const route of project.routes.filter(route => route.router !== 'app')) {
248
+ const serverEntry = path.join(stage, '.entries', route.id + '.server.mjs');
249
+ await writeFile(serverEntry, `export {default} from ${JSON.stringify(route.file)};\nexport * from ${JSON.stringify(route.file)};\n${route.kind === 'page' && project.app ? `export {default as App} from ${JSON.stringify(project.app)};` : ''}\n${route.kind === 'page' && project.document ? `export {default as Document} from ${JSON.stringify(project.document)};export * as __prnextDocument from ${JSON.stringify(project.document)};` : ''}\n`);
250
+ serverEntries[route.id] = serverEntry;
251
+ if (route.kind === 'page') {
252
+ const clientEntry = path.join(stage, '.entries', route.id + '.client.jsx');
253
+ await writeFile(clientEntry, browserEntry(route, project.app, pagesManifestUrl, { ...publicPaths, strictMode: config.reactStrictMode === true }, development));
254
+ clientEntries[route.id] = clientEntry;
255
+ }
256
+ manifest.routes.push({ id: route.id, pattern: route.pattern, kind: route.kind, module: `server/${route.id}.mjs`, css: [],
257
+ ...(route.internal ? { internal: true } : {}), ...(route.errorStatus ? { errorStatus: route.errorStatus } : {}),
258
+ });
259
+ }
260
+ const common = { absWorkingDir: project.root, bundle: true, logLevel: 'silent', jsx: 'automatic', loader: assetLoaders, assetNames: '[name]-[hash]', publicPath: publicPaths.assetBase, metafile: true, define: { 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), ...environment } };
261
+ if (Object.keys(serverEntries).length) {
262
+ const server = await bundle({ ...common, entryPoints: serverEntries, outdir: path.join(stage, 'server'), outExtension: { '.js': '.mjs' }, platform: 'node', format: 'esm', splitting: true, chunkNames: 'chunk-[hash]', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" }, target: 'node22', sourcemap: dev, plugins: [moduleResolutionPlugin(config.turbopack, project.root), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: false, pageFiles, projectRoot: project.root, documentFile: project.document, fonts }), npmPackagesPlugin({ pages: true, projectRoot: project.root, transpilePackages: config.transpilePackages, serverExternalPackages: config.serverExternalPackages }), cssModules.plugin(false)] });
263
+ fonts.record(server.metafile);
264
+ const serverChunks = new Set(Object.keys(server.metafile.outputs).filter(file => file.endsWith('.mjs')).map(file => path.basename(file)));
265
+ for (const [output, metadata] of Object.entries(server.metafile.outputs)) {
266
+ if (!output.endsWith('.mjs') || !metadata.imports.some(item => !item.external)) continue;
267
+ const file = path.resolve(project.root, output);
268
+ await rewriteServerChunks(file, serverChunks, publicPaths.assetBase);
269
+ }
270
+ // Image/font imports must resolve to the same public URLs on both render paths.
271
+ await mkdir(path.join(stage, 'assets'), { recursive: true });
272
+ for (const [output, metadata] of Object.entries(server.metafile.outputs)) {
273
+ if (!metadata.entryPoint && !output.endsWith('.map') && !output.endsWith('.mjs')) await cp(path.resolve(project.root, output), path.join(stage, 'assets', path.basename(output)));
274
+ }
275
+ }
276
+ if (Object.keys(clientEntries).length) {
277
+ const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, fonts, refresh }), cssModules.plugin(true)] });
278
+ fonts.record(browser.metafile);
279
+ const byEntry = new Map(Object.entries(clientEntries).map(([id, file]) => [path.resolve(file), id]));
280
+ for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
281
+ if (!metadata.entryPoint || !output.endsWith('.js')) continue;
282
+ const id = byEntry.get(path.resolve(project.root, metadata.entryPoint));
283
+ const route = manifest.routes.find(route => route.id === id);
284
+ // Dynamic import() chunks are also entry points in esbuild's metadata.
285
+ // Their loaders retain those chunks; only route entries bootstrap pages.
286
+ if (!route) continue;
287
+ route.client = publicPaths.assetBase + '/' + path.basename(output);
288
+ if (metadata.cssBundle) route.css.push(publicPaths.assetBase + '/' + path.basename(metadata.cssBundle));
289
+ }
290
+ }
291
+ await compileMiddleware({ project, stage, manifest, dev, moduleResolution: config.turbopack, defineEnvironment: environment });
292
+ await prepareMetadataRoutes(project, stage);
293
+ await compileApp({ project, stage, manifest, moduleResolution: config.turbopack, cssModules, fonts, images, dev, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages });
294
+ fonts.attach(manifest);
295
+ expandLocales(manifest);
296
+ await prerender(stage, manifest, dev);
297
+ await prerenderApp(stage, manifest, dev);
298
+ const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal);
299
+ const publicPage = route => ({ ...(route.locale ? {locale:route.locale,originalPattern:route.originalPattern} : {}), id: route.id, pattern: route.pattern, client: route.client, css: route.css, ...(route.fonts ? { fonts: route.fonts } : {}), ssg: !!route.ssg, ssp: !!route.ssp, gip: !!route.gip, appGip: !!route.appGip });
300
+ if (pages.length || manifest.pagesErrors) {
301
+ const publicPages = {
302
+ buildId: manifest.buildId,
303
+ ...publicPaths,
304
+ // Header rules cannot select a different page. Other server routing
305
+ // stays authoritative, including middleware with conditional behavior.
306
+ needsServerRouting: !!manifest.middleware || customRoutes.redirects.length > 0 ||
307
+ Object.values(customRoutes.rewrites).some(rules => rules.length > 0),
308
+ routes: pages.map(publicPage),
309
+ ...(manifest.pagesErrors ? { errors: Object.fromEntries(Object.entries(manifest.pagesErrors).map(([kind, id]) => [kind, publicPage(manifest.routes.find(route => route.id === id))])) } : {}),
310
+ ...(config.i18n && manifest.pagesErrors ? {localizedErrors:Object.fromEntries(config.i18n.locales.map(locale=>[locale,Object.fromEntries(Object.entries(manifest.pagesErrors).map(([kind,id])=>[kind,publicPage(manifest.routes.find(route=>route.id===(locale===config.i18n.defaultLocale?id:id+'-locale-'+locale)))]))]))} : {}),
311
+ nonPagesRoutes: manifest.routes.filter(route => !route.internal && (route.kind !== 'page' || route.router === 'app'))
312
+ .map(route => ({ pattern: route.pattern, kind: route.kind, router: route.router || 'pages' })),
313
+ };
314
+ await writeFile(path.join(stage, 'assets', path.basename(pagesManifestUrl)), JSON.stringify(publicPages));
315
+ manifest.pagesManifest = pagesManifestUrl;
316
+ }
317
+ await rm(path.join(stage, '.entries'), { recursive: true });
318
+ if (dev) {
319
+ const appRoute = manifest.routes.find(route => route.router === 'app' && route.kind === 'page' && route.client);
320
+ await writeFile(path.join(stage, 'assets', path.basename(manifest.devClient)), JSON.stringify({ buildId,
321
+ pages: manifest.routes.filter(route => route.router !== 'app' && route.client).map(publicPage),
322
+ ...(appRoute ? { app: { client: appRoute.client, css: appRoute.css } } : {}),
323
+ }));
324
+ }
325
+ await writeFile(path.join(stage, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
326
+ if (config.compress) await precompressBuild(stage, manifest, {cacheDirectory:path.join(project.root,'.prnext-cache/precompress-v1')});
327
+ if (config.output === 'standalone' && !dev) await (await import('./standalone-runner.mjs')).prepareStandalone({ projectRoot: project.root, stage, manifest, config });
328
+ const exporting = config.output === 'export' && !dev ? await import('./static-export.mjs') : null;
329
+ const exportSource = exporting && await exporting.prepareStaticExport({ project, stage, manifest, config });
330
+ let hadDestination = false;
331
+ try { await stat(destination); hadDestination = true; } catch (error) { if (error.code !== 'ENOENT') throw error; }
332
+ if (hadDestination) await rename(destination, backup);
333
+ const pointerTemporary = path.join(project.root, `.prnext-output-${randomUUID()}.tmp`);
334
+ let published = false;
335
+ let exported;
336
+ try {
337
+ await writeFile(pointerTemporary, JSON.stringify({ distDir: config.distDir }) + '\n', { flag: 'wx' });
338
+ if (exporting) exported = await exporting.publishStaticExport(exportSource, project.root);
339
+ await rename(stage, destination);
340
+ published = true;
341
+ await rename(pointerTemporary, path.join(project.root, OUTPUT_POINTER));
342
+ } catch (error) {
343
+ if (published) await rename(destination, stage);
344
+ await exported?.rollback();
345
+ if (hadDestination) await rename(backup, destination);
346
+ throw error;
347
+ } finally { await rm(pointerTemporary, {force:true}); }
348
+ await exported?.commit();
349
+ if (hadDestination) await rm(backup, { recursive: true, force: true });
350
+ return { ...manifest, outputDirectory: destination, durationMs: Math.round(performance.now() - started) };
351
+ } catch (error) {
352
+ await rm(stage, { recursive: true, force: true });
353
+ if (error.errors) error.message = error.errors.map(item => `${item.location ? `${item.location.file}:${item.location.line}: ` : ''}${item.text}`).join('\n');
354
+ throw error;
355
+ }
356
+ }
@@ -0,0 +1,69 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { createRequire } from 'node:module';
3
+ import { readFile, writeFile, mkdir, rename, rm, readdir, stat } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ const require = createRequire(import.meta.url);
6
+ const hash = value => createHash('sha256').update(value).digest('hex');
7
+ const MAX_ENTRY = 16 * 1024 * 1024, MAX_TOTAL = 64 * 1024 * 1024;
8
+ async function fileHash(file) {
9
+ try { if ((await stat(file)).size > MAX_ENTRY) return null; return hash(await readFile(file)); }
10
+ catch(error) { if(error.code === 'ENOENT') return 'missing'; throw error; }
11
+ }
12
+ async function directoryHash(root) {
13
+ const records = []; let count = 0;
14
+ async function walk(directory) {
15
+ for (const name of (await readdir(directory)).sort()) {
16
+ if (++count > 10000) throw new Error('Loader context dependency exceeds 10000 files');
17
+ const file=path.join(directory,name), metadata=await stat(file);
18
+ records.push([path.relative(root,file),metadata.isDirectory()?'directory':await fileHash(file)]);
19
+ if(metadata.isDirectory())await walk(file);
20
+ }
21
+ }
22
+ try { await walk(root); return hash(JSON.stringify(records)); }
23
+ catch(error) { if(error.code==='ENOENT')return 'missing'; throw error; }
24
+ }
25
+ function loaderModules(loaders) {
26
+ const files=new Set();
27
+ function visit(file){if(files.has(file))return;files.add(file);for(const child of require.cache[file]?.children || [])visit(child.filename);}
28
+ for(const loader of loaders)visit(loader.loader);
29
+ return [...files];
30
+ }
31
+ async function snapshot(files,contexts,missing) {
32
+ return {files:await Promise.all([...new Set([...files,...missing])].sort().map(async file=>[file,await fileHash(file)])),
33
+ contexts:await Promise.all([...new Set(contexts)].sort().map(async file=>[file,await directoryHash(file)]))};
34
+ }
35
+ async function valid(snapshot) {
36
+ if(snapshot.files.some(([,digest])=>digest===null))return false;
37
+ for(const [file,digest] of snapshot.files)if(await fileHash(file)!==digest)return false;
38
+ for(const [file,digest] of snapshot.contexts)if(await directoryHash(file)!==digest)return false;
39
+ return true;
40
+ }
41
+ async function prune(directory) {
42
+ const entries = await Promise.all((await readdir(directory)).filter(name=>name.endsWith('.json')).map(async name=>({name,...await stat(path.join(directory,name))})));
43
+ entries.sort((a,b)=>b.mtimeMs-a.mtimeMs);let bytes=0;
44
+ for(let index=0;index<entries.length;index++){bytes+=entries[index].size;if(index>=256||bytes>MAX_TOTAL)await rm(path.join(directory,entries[index].name),{force:true});}
45
+ }
46
+ /** Cache only loaders which opt into dependency tracking; no render output enters this cache. */
47
+ export async function cachedLoader(options,run) {
48
+ let serializable = true;
49
+ try { JSON.stringify(options,(_key,value)=>{if(typeof value==='function'||typeof value==='symbol'||typeof value==='bigint'||value instanceof RegExp)serializable=false;return value;}); } catch { serializable=false; }
50
+ if (!serializable) return run();
51
+ const directory=path.join(options.root,'.prnext-cache','loaders-v1');
52
+ const key=hash(JSON.stringify({...options,env:hash(JSON.stringify(Object.entries(process.env).sort()))}));
53
+ const file=path.join(directory,key+'.json');
54
+ try {
55
+ if((await stat(file)).size<=MAX_ENTRY){const cached=JSON.parse(await readFile(file,'utf8'));if(cached.version===1&&await valid(cached.snapshot))return cached.result;}
56
+ } catch(error) { if(!['ENOENT','ENOTDIR'].includes(error.code) && !(error instanceof SyntaxError)) throw error; }
57
+ for (const file of loaderModules(options.loaders)) delete require.cache[file];
58
+ const result=await run();
59
+ if(result.cacheable===false)return result;
60
+ const dependencies=[...new Set([options.file,...result.dependencies,...loaderModules(options.loaders),...['package.json','package-lock.json','pnpm-lock.yaml','yarn.lock'].map(name=>path.join(options.root,name))])];
61
+ const current=await snapshot(dependencies,result.contexts,result.missing);
62
+ const body=JSON.stringify({version:1,snapshot:current,result:{...result,dependencies}});
63
+ if(Buffer.byteLength(body)>MAX_ENTRY || current.files.some(([,value])=>value===null))return result;
64
+ await mkdir(directory,{recursive:true});
65
+ const temporary=file+'.'+randomUUID()+'.tmp';
66
+ try {await writeFile(temporary,body);await rename(temporary,file);}finally{await rm(temporary,{force:true});}
67
+ await prune(directory);
68
+ return result;
69
+ }
@@ -0,0 +1,72 @@
1
+ import { readFile, access } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { resolveNativeBinary } from '../native/resolve.mjs';
7
+ import { validateAppConfig, mergeAppConfig } from './app-config.mjs';
8
+ import { metadataExportNames } from './metadata-routes.mjs';
9
+ import { pageExtensionPattern, validatePageSource } from './page-extensions.mjs';
10
+ const execute = promisify(execFile);
11
+ const contentTypes = { ico: 'image/x-icon', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', svg: 'image/svg+xml', gif: 'image/gif' };
12
+ const imageTypes = { favicon: ['ico'], icon: ['ico', 'jpg', 'jpeg', 'png', 'svg'], 'apple-icon': ['jpg', 'jpeg', 'png'], 'opengraph-image': ['jpg', 'jpeg', 'png', 'gif'], 'twitter-image': ['jpg', 'jpeg', 'png', 'gif'] };
13
+ async function imageInfo(file, kind, extension) {
14
+ const bytes = await readFile(file);
15
+ const max = kind === 'opengraph-image' ? 8 : 5;
16
+ if (bytes.length > max * 1024 * 1024) throw new Error(`${file} exceeds the ${max} MiB metadata image limit`);
17
+ let dimensions = {};
18
+ if (extension !== 'svg') {
19
+ const binary = await resolveNativeBinary();
20
+ const { stdout } = await execute(binary, ['image-info', file], { timeout: 30000, maxBuffer: 65536 });
21
+ const info = JSON.parse(stdout);
22
+ dimensions = { width: info.width, height: info.height };
23
+ }
24
+ let alt;
25
+ try { alt = (await readFile(file.slice(0, -extension.length - 1) + '.alt.txt', 'utf8')).trim(); }
26
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
27
+ return { ...dimensions, type: contentTypes[extension], ...(alt ? { alt } : {}), hash: createHash('sha256').update(bytes).digest('hex').slice(0, 16), ...(extension === 'svg' ? { sizes: 'any' } : {}) };
28
+ }
29
+ export async function scanMetadataImages({ root, appRoot, files, routes, patterns, validatePattern, basePath, pageExtensions, appNotFound }) {
30
+ const metadataImages = [];
31
+ const sourceExtension = pageExtensionPattern(pageExtensions);
32
+ for (const file of files) {
33
+ const relative = path.relative(appRoot, file).replaceAll(path.sep, '/');
34
+ const parts = relative.split('/');
35
+ const name = parts.pop();
36
+ if (parts.some(part => part.startsWith('_'))) continue;
37
+ const dynamic = sourceExtension.test(name);
38
+ const match = dynamic ? /^(favicon|icon\d*|apple-icon\d*|opengraph-image\d*|twitter-image\d*)$/.exec(name.replace(sourceExtension, '')) : /^(favicon|icon\d*|apple-icon\d*|opengraph-image\d*|twitter-image\d*)\.(ico|jpg|jpeg|png|svg|gif)$/.exec(name);
39
+ if (!match) continue;
40
+ const [, stem, extension] = match;
41
+ const kind = stem.replace(/\d+$/, '');
42
+ if (dynamic) validatePageSource(file);
43
+ if (!dynamic && !imageTypes[kind].includes(extension)) continue;
44
+ if (kind === 'favicon' && (parts.length || dynamic)) continue;
45
+ const names = dynamic ? metadataExportNames(await readFile(file, 'utf8'), file) : [];
46
+ if (names.some(name => ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].includes(name))) throw new Error(`${relative} must export a default metadata function, not HTTP methods.`);
47
+ const multiple = names.includes('generateImageMetadata');
48
+ const urlParts = parts.filter(part => !/^\([^()]+\)$/.test(part) && !part.startsWith('@')).map(part => part.replace(/^%5f/i, '_'));
49
+ if (urlParts.some(part => /^\(\.{1,3}\)/.test(part))) continue;
50
+ let suffix = '';
51
+ if (parts.some(part => /^\([^()]+\)$/.test(part) || part.startsWith('@'))) {
52
+ let hash = 5381;
53
+ for (const character of '/' + parts.join('/')) hash = (hash * 33 + character.charCodeAt(0)) | 0;
54
+ suffix = '-' + (hash >>> 0).toString(36).slice(0, 6);
55
+ }
56
+ const filename = stem + suffix + (dynamic ? '' : '.' + extension);
57
+ const pattern = validatePattern([...urlParts, filename, ...(multiple ? ['[__metadata_id__]'] : [])], relative);
58
+ const signature = pattern.replace(/\[[^\]]+\]/g, '[]');
59
+ if (patterns.has(signature)) throw new Error(`Conflicting metadata route ${relative} and ${patterns.get(signature)} at ${pattern}.`);
60
+ try { await access(path.join(root, 'public', pattern.slice(1))); throw new Error(`Metadata route ${pattern} conflicts with a file in public/.`); }
61
+ catch (error) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error; }
62
+ patterns.set(signature, relative);
63
+ const handlerConfig = dynamic ? await validateAppConfig(file) : {};
64
+ handlerConfig.revalidate ??= false;
65
+ if (multiple) handlerConfig.generateStaticParams = true;
66
+ routes.push({ id: `app-metadata-${createHash('sha256').update(pattern).digest('hex').slice(0, 12)}`, pattern, router: 'app', kind: 'api', file, handlerConfig,
67
+ cacheConfig: mergeAppConfig([handlerConfig]), metadataFile: { kind, dynamic, image: true, multiple, contentType: contentTypes[extension] } });
68
+ metadataImages.push({ path: parts.join('/'), kind, pattern: basePath + pattern, multiple, ...(dynamic ? { file, exports: names.filter(name => ['alt', 'size', 'contentType', 'generateImageMetadata'].includes(name)) } : { info: await imageInfo(file, kind, extension) }) });
69
+ }
70
+ for (const route of routes) if (route.kind === 'page' && route.router === 'app') route.metadataFiles = metadataImages.filter(image => route.segments.some(segment => segment.path === image.path));
71
+ if (appNotFound) appNotFound.metadataFiles = metadataImages.filter(image => image.path === '');
72
+ }