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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +2 -0
  2. package/build/actions.mjs +26 -14
  3. package/build/app.mjs +90 -101
  4. package/build/build-lock.mjs +2 -71
  5. package/build/build-retention.mjs +119 -0
  6. package/build/bundle-runtime.mjs +9 -8
  7. package/build/cache-command.mjs +31 -0
  8. package/build/check.mjs +41 -3
  9. package/build/compilation-cache.mjs +32 -12
  10. package/build/compilation-storage.mjs +190 -0
  11. package/build/compiler.mjs +8 -7
  12. package/build/config.mjs +25 -2
  13. package/build/create.mjs +1 -1
  14. package/build/css-modules.mjs +4 -0
  15. package/build/dev-action-graph.mjs +21 -0
  16. package/build/dev-css-assets.mjs +173 -0
  17. package/build/dev-css-update.mjs +14 -12
  18. package/build/dev-lazy-build.mjs +77 -0
  19. package/build/dynamic.mjs +13 -6
  20. package/build/font-cache.mjs +9 -6
  21. package/build/font-files.mjs +1 -1
  22. package/build/incremental.mjs +112 -18
  23. package/build/index.mjs +61 -15
  24. package/build/lazy-dynamic-imports.mjs +137 -0
  25. package/build/migrate.mjs +4 -4
  26. package/build/migration-audit.mjs +19 -31
  27. package/build/migration-imports.mjs +245 -0
  28. package/build/migration-scenarios-runner.mjs +73 -0
  29. package/build/migration-scenarios.mjs +72 -0
  30. package/build/native-discovery.mjs +65 -5
  31. package/build/native-packages.mjs +2 -0
  32. package/build/native-ppr.mjs +32 -0
  33. package/build/npm-packages.mjs +1 -0
  34. package/build/og.mjs +121 -1
  35. package/build/parallel-plan.mjs +55 -0
  36. package/build/persistent-cache.mjs +29 -24
  37. package/build/production-builder.mjs +1 -1
  38. package/build/production-check.mjs +34 -0
  39. package/build/react-compiler-report.mjs +31 -8
  40. package/build/react-compiler.mjs +101 -10
  41. package/build/server-config.mjs +35 -0
  42. package/build/source-analysis.mjs +54 -4
  43. package/build/standalone.mjs +21 -3
  44. package/build/static-adapter.mjs +7 -3
  45. package/build/transform.mjs +38 -6
  46. package/build/typescript-dev.mjs +3 -1
  47. package/build/webpack-graph.mjs +13 -3
  48. package/cli.mjs +24 -6
  49. package/compat/app-dynamic.cjs +12 -1
  50. package/compat/app-layout-context.cjs +7 -3
  51. package/compat/cache-handlers.cjs +3 -1
  52. package/compat/flight-delta.cjs +50 -13
  53. package/compat/index.d.ts +24 -1
  54. package/compat/link.cjs +2 -2
  55. package/compat/navigation-authorization.cjs +8 -1
  56. package/docs/ai/compatibility.md +187 -7
  57. package/docs/ai/deployment.md +141 -0
  58. package/docs/ai/quickstart.md +5 -0
  59. package/docs/ai/routing.md +11 -0
  60. package/docs/ai/troubleshooting.md +79 -0
  61. package/package.json +26 -11
  62. package/release/fontkit.mjs +31 -0
  63. package/release/verify.mjs +3 -0
  64. package/runtime/api.mjs +5 -2
  65. package/runtime/app-client.mjs +254 -53
  66. package/runtime/app-partial.mjs +2 -10
  67. package/runtime/app-prefetch.mjs +2 -2
  68. package/runtime/app-render.mjs +11 -7
  69. package/runtime/app-stream-channel.mjs +3 -2
  70. package/runtime/app-styles.mjs +22 -4
  71. package/runtime/atomic-file.mjs +27 -0
  72. package/runtime/cache-components-rsc.mjs +3 -1
  73. package/runtime/compile-cache-retention.mjs +53 -0
  74. package/runtime/compile-cache.mjs +28 -1
  75. package/runtime/dev-client.mjs +13 -5
  76. package/runtime/dev-file-changes.mjs +123 -8
  77. package/runtime/dev-gateway.mjs +3 -2
  78. package/runtime/dev-lazy-imports.mjs +121 -0
  79. package/runtime/dev-lazy-staging.mjs +14 -0
  80. package/runtime/dev-route-selection.mjs +64 -0
  81. package/runtime/dev-runtime-pool.mjs +3 -2
  82. package/runtime/dev-runtime-spare.mjs +1 -1
  83. package/runtime/dev-session-retention.mjs +154 -0
  84. package/runtime/dev-style-retention.mjs +15 -0
  85. package/runtime/development.mjs +86 -26
  86. package/runtime/env.mjs +9 -0
  87. package/runtime/flight-delta.mjs +59 -9
  88. package/runtime/fs.mjs +15 -8
  89. package/runtime/lazy-import-client.mjs +38 -0
  90. package/runtime/native-ppr.mjs +35 -0
  91. package/runtime/og/NOTICE.txt +1 -1
  92. package/runtime/og/index.node.mjs +76 -31216
  93. package/runtime/og/upstream.json +115 -0
  94. package/runtime/persistent/client.mjs +4 -2
  95. package/runtime/persistent/daemon.mjs +3 -11
  96. package/runtime/persistent/health.mjs +58 -0
  97. package/runtime/project-lock.mjs +71 -0
  98. package/runtime/request-transport.mjs +112 -32
  99. package/runtime/storage-path.mjs +36 -0
  100. package/runtime/trace.mjs +44 -4
  101. package/runtime/worker.mjs +9 -4
  102. package/vendor/fontkit/NOTICE.txt +5 -0
  103. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  104. package/vendor/fontkit/index.mjs +3 -0
  105. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  106. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  107. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  108. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  109. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  110. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  111. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  112. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  113. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  114. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  115. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  116. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  117. package/vendor/fontkit/upstream.json +215 -0
  118. package/vendor/nft/LICENSE +7 -0
  119. package/vendor/nft/README.md +16 -0
  120. package/vendor/nft/out/analyze.d.ts +8 -0
  121. package/vendor/nft/out/analyze.js +1216 -0
  122. package/vendor/nft/out/analyze.js.map +1 -0
  123. package/vendor/nft/out/cli.d.ts +2 -0
  124. package/vendor/nft/out/cli.js +94 -0
  125. package/vendor/nft/out/cli.js.map +1 -0
  126. package/vendor/nft/out/fs.d.ts +22 -0
  127. package/vendor/nft/out/fs.js +106 -0
  128. package/vendor/nft/out/fs.js.map +1 -0
  129. package/vendor/nft/out/index.d.ts +4 -0
  130. package/vendor/nft/out/index.js +25 -0
  131. package/vendor/nft/out/index.js.map +1 -0
  132. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  133. package/vendor/nft/out/node-file-trace.js +380 -0
  134. package/vendor/nft/out/node-file-trace.js.map +1 -0
  135. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  136. package/vendor/nft/out/resolve-dependency.js +427 -0
  137. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  138. package/vendor/nft/out/types.d.ts +66 -0
  139. package/vendor/nft/out/types.js +2 -0
  140. package/vendor/nft/out/types.js.map +1 -0
  141. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  142. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  143. package/vendor/nft/out/utils/binary-locators.js +88 -0
  144. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  145. package/vendor/nft/out/utils/get-package-base.js +30 -0
  146. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  147. package/vendor/nft/out/utils/interop-require.js +24 -0
  148. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  149. package/vendor/nft/out/utils/node-version.js +10 -0
  150. package/vendor/nft/out/utils/node-version.js.map +1 -0
  151. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  152. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  153. package/vendor/nft/out/utils/special-cases.js +347 -0
  154. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  155. package/vendor/nft/out/utils/static-eval.js +579 -0
  156. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  157. package/vendor/nft/out/utils/types.js +2 -0
  158. package/vendor/nft/out/utils/types.js.map +1 -0
  159. package/vendor/nft/out/utils/wrappers.js +659 -0
  160. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  161. package/vendor/nft/package.json +4 -0
  162. package/vendor/nft/upstream.json +51 -0
package/README.md CHANGED
@@ -93,6 +93,8 @@ npm run dev -- examples/app
93
93
 
94
94
  Source builds require Rust 1.98 and native build tools: Visual Studio C++ tools and a Windows SDK on Windows, Xcode command line tools on macOS, or a C/C++ toolchain and pkg-config on Linux. Install NASM on macOS, Linux and Windows x64.
95
95
 
96
+ For a larger integration example, see the [SaaS reference](https://github.com/Lorkydey/PRNext/tree/main/examples/saas-reference): Better Auth, PostgreSQL/Drizzle, French/English routing and a portable standalone build. It has its own dependency lockfile and database setup.
97
+
96
98
  </details>
97
99
 
98
100
  ## Documentation
package/build/actions.mjs CHANGED
@@ -9,28 +9,39 @@ import path from 'node:path';
9
9
 
10
10
  const traverse = traverseModule.default || traverseModule;
11
11
  const generate = generateModule.default || generateModule;
12
+ // Kept inside the compiler process. The development session is public metadata
13
+ // and must never be usable by itself to derive an action encryption key.
14
+ const processDevelopmentSecret = randomBytes(32);
12
15
  function hasDirective(node,value) { return node?.directives?.some(item=>item.value.value===value); }
13
16
  function removeDirective(node,value) { if(node?.directives) node.directives=node.directives.filter(item=>item.value.value!==value); }
14
17
 
15
- export function createServerActions({ projectRoot }) {
16
- const salt = randomBytes(32);
18
+ export function createServerActions({ projectRoot, developmentSession, developmentSecret }) {
19
+ if (developmentSession !== undefined && (typeof developmentSession !== 'string' || !developmentSession || developmentSession.length > 1024)) throw new TypeError('A development action session must be a non-empty string of at most 1024 characters');
20
+ if (developmentSecret !== undefined && (typeof developmentSecret !== 'string' || !/^[A-Za-z0-9+/]{43}=$/.test(developmentSecret))) throw new TypeError('A development action secret must encode 32 random bytes');
21
+ const privateSecret = developmentSecret ? Buffer.from(developmentSecret,'base64') : processDevelopmentSecret;
22
+ const derive = purpose => createHmac('sha256', privateSecret).update(JSON.stringify([purpose, path.resolve(projectRoot), developmentSession])).digest();
23
+ const salt = developmentSession ? derive('action-identities') : randomBytes(32);
17
24
  const modules = new Map();
18
25
  const actions = new Map();
19
26
  const configured = process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY;
20
27
  if (configured && (!/^[A-Za-z0-9+/]+={0,2}$/.test(configured) || ![16,24,32].includes(Buffer.from(configured,'base64').length))) throw new Error('NEXT_SERVER_ACTIONS_ENCRYPTION_KEY must be a base64-encoded 16, 24, or 32 byte AES key.');
21
- const actionKey = configured ? Buffer.from(configured,'base64').toString('base64') : randomBytes(32).toString('base64');
22
- function identity(file,name) { return createHmac('sha256',salt).update(path.relative(projectRoot,file).replaceAll(path.sep,'/')+'\0'+name).digest('hex'); }
23
- function register(file,name,binding) {
24
- const id=identity(file,name);
28
+ const actionKey = configured ? Buffer.from(configured,'base64').toString('base64') : (developmentSession ? derive('action-encryption') : randomBytes(32)).toString('base64');
29
+ function identity(file,name,revision) { return createHmac('sha256',salt).update(path.relative(projectRoot,file).replaceAll(path.sep,'/')+'\0'+name+(revision?'\0'+revision:'')).digest('hex'); }
30
+ function register(file,name,binding,revision) {
31
+ const id=identity(file,name,revision);
25
32
  if(!actions.has(id)) actions.set(id,{id,file,binding});
26
33
  return id;
27
34
  }
28
35
 
29
- function analyze(source,file) {
30
- const key=file+'\0'+createHash('sha256').update(source).digest('hex');
36
+ function analyze(source,file,{revisionSource=source}={}) {
37
+ const sourceHash=createHash('sha256').update(source).digest('hex');
38
+ // Cache transforms embed a generation ID. IDs for an unchanged action must
39
+ // follow its original source revision, not those ephemeral emitted bytes.
40
+ const revision=developmentSession?createHash('sha256').update(revisionSource).digest('hex'):sourceHash;
41
+ const key=file+'\0'+sourceHash+(developmentSession?'\0'+revision:'');
31
42
  if(modules.has(key))return modules.get(key);
32
43
  const template=JSON.parse(cachedTransform('server-action-analysis',source,[projectRoot,file],()=>JSON.stringify(analyzeTemplate(source,file))));
33
- const ids=new Map(template.registrations.map(item=>[item.id,register(file,item.name,item.binding)]));
44
+ const ids=new Map(template.registrations.map(item=>[item.id,register(file,item.name,item.binding,developmentSession?revision:undefined)]));
34
45
  let server=template.server;
35
46
  for(const [previous,current]of ids)server=server.replaceAll(previous,current);
36
47
  const result={...template,server,declared:template.declared.map(item=>({...item,id:ids.get(item.id)}))};
@@ -203,13 +214,14 @@ export function createServerActions({ projectRoot }) {
203
214
  return {
204
215
  actions, actionKey,
205
216
  // Only compiler-generated module-level action proxies bypass React Refresh.
206
- // Their random reference IDs change each build; they are not component exports.
207
- isServerModule(source,file) {
208
- return source.includes('use server') && analyze(source,file).moduleServer === true;
217
+ // Their opaque IDs are not component exports. Development keeps unchanged
218
+ // modules valid within one session; production IDs still change each build.
219
+ isServerModule(source,file,options) {
220
+ return source.includes('use server') && analyze(source,file,options).moduleServer === true;
209
221
  },
210
- transform(source,file,mode) {
222
+ transform(source,file,mode,options) {
211
223
  if(!source.includes('use server')) return source;
212
- const result=analyze(source,file);
224
+ const result=analyze(source,file,options);
213
225
  // A Route Handler invokes the implementation as ordinary server code.
214
226
  // It does not serialize functions through Flight or manufacture proxies.
215
227
  // Analyze first to preserve validation and the shared action registry.
package/build/app.mjs CHANGED
@@ -6,9 +6,9 @@ import path from 'node:path';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { createRequire, builtinModules } from 'node:module';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { parse } from '@babel/parser';
10
9
  import * as t from '@babel/types';
11
- import { rewriteServerChunks } from './transform.mjs';
10
+ import {analyzeSource} from './source-analysis.mjs';
11
+ import { rewriteServerChunkOutputFiles } from './transform.mjs';
12
12
  import { transformDynamicImports } from './dynamic.mjs';
13
13
  import { createServerActions } from './actions.mjs';
14
14
  import { expandActionExports } from './action-exports.mjs';
@@ -21,6 +21,9 @@ import {createReactCompilerTransform} from './react-compiler.mjs';
21
21
  import {createAppStyleGraph, appStyleScopes, planAppStyleEntries} from './app-styles.mjs';
22
22
  import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
23
23
  import {createHtmlOnlyGraph} from './html-only-graph.mjs';
24
+ import {createLazyDynamicImports} from './lazy-dynamic-imports.mjs';
25
+ import {developmentActionMetadata} from './dev-action-graph.mjs';
26
+ import {immutableStylePlan, executeStylePlan, createStyleOutputWriter} from './parallel-plan.mjs';
24
27
 
25
28
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
29
  const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx', '.mts': 'ts', '.cts': 'ts' };
@@ -42,77 +45,28 @@ export async function validateReactVersions(resolveFromProject) {
42
45
  }
43
46
  }
44
47
 
45
- function sourceAst(source, filename) {
46
- return parse(source, { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] });
47
- }
48
48
  function clientDirective(source, filename) {
49
49
  if (!source.includes('use client')) return false;
50
- return sourceAst(source, filename).program.directives.some(directive => directive.value.value === 'use client');
50
+ return analyzeSource(source,filename).directives.includes('use client');
51
51
  }
52
52
 
53
53
  function frameworkDirective(source, filename) {
54
54
  if (clientDirective(source, filename)) return true;
55
55
  if (!source.includes('use server')) return false;
56
- function contains(node) {
57
- if (!node || typeof node !== 'object') return false;
58
- if (t.isDirective(node) && node.value.value === 'use server') return true;
59
- return (t.VISITOR_KEYS[node.type] || []).some(key => Array.isArray(node[key]) ? node[key].some(contains) : contains(node[key]));
60
- }
61
- return contains(sourceAst(source, filename));
56
+ return analyzeSource(source,filename).hasServerDirective;
62
57
  }
63
58
 
64
59
  /** Enumerate runtime ESM exports without evaluating client code in the server build. */
65
- async function exportedNames(file, esbuild, seen = new Set()) {
60
+ export async function exportedNames(file, esbuild, seen = new Set()) {
66
61
  if (seen.has(file)) return new Set();
67
62
  seen.add(file);
68
63
  const source = await compilerSource(file);
69
- const ast = sourceAst(source, file);
70
- const names = new Set();
71
- for (const node of ast.program.body) {
72
- if (t.isExportDefaultDeclaration(node)) { names.add('default'); continue; }
73
- if (node.exportKind === 'type') continue;
74
- if (t.isExportNamedDeclaration(node)) {
75
- if (node.declaration && !t.isTSInterfaceDeclaration(node.declaration) && !t.isTSTypeAliasDeclaration(node.declaration) && !node.declaration.declare) {
76
- for (const name of Object.keys(t.getOuterBindingIdentifiers(node.declaration))) names.add(name);
77
- }
78
- for (const specifier of node.specifiers) if (specifier.exportKind !== 'type') names.add(specifier.exported.name ?? specifier.exported.value);
79
- }
80
- if (t.isExportAllDeclaration(node)) {
81
- const resolved = await esbuild.resolve(node.source.value, { resolveDir: path.dirname(file), kind: 'import-statement', pluginData: { prnextExportScan: true } });
82
- if (resolved.errors.length || !resolved.path || resolved.external) throw new Error(`Cannot enumerate client boundary exports from ${node.source.value} in ${file}. Use explicit named re-exports.`);
64
+ const analysis=analyzeSource(source,file),names=new Set(analysis.runtimeExports);
65
+ for (const specifier of analysis.exportStars) {
66
+ const resolved = await esbuild.resolve(specifier, { resolveDir: path.dirname(file), kind: 'import-statement', pluginData: { prnextExportScan: true } });
67
+ if (resolved.errors.length || !resolved.path || resolved.external) throw new Error(`Cannot enumerate client boundary exports from ${specifier} in ${file}. Use explicit named re-exports.`);
83
68
  for (const name of await exportedNames(resolved.path, esbuild, seen)) if (name !== 'default') names.add(name);
84
- }
85
- }
86
- // CommonJS is supported for npm and PRNext compatibility components. The default
87
- // is module.exports; statically assigned properties are named exports as in Node.
88
- function visit(node) {
89
- if (!node || typeof node !== 'object') return;
90
- if (t.isCallExpression(node) && t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object, { name: 'Object' }) && t.isIdentifier(node.callee.property, { name: 'defineProperty' }) && t.isIdentifier(node.arguments[0], { name: 'exports' }) && t.isStringLiteral(node.arguments[1]) && node.arguments[1].value !== '__esModule') {
91
- names.add('default');
92
- names.add(node.arguments[1].value);
93
- }
94
- if (t.isAssignmentExpression(node) && t.isMemberExpression(node.left)) {
95
- const left = node.left;
96
- const property = left.computed ? left.property.value : left.property.name;
97
- if (t.isIdentifier(left.object, { name: 'module' }) && property === 'exports') {
98
- names.add('default');
99
- if (t.isObjectExpression(node.right)) for (const item of node.right.properties) {
100
- const key = item.computed ? item.key?.value : item.key?.name ?? item.key?.value;
101
- if (typeof key === 'string') names.add(key);
102
- }
103
- }
104
- if (t.isIdentifier(left.object, { name: 'exports' }) || (t.isMemberExpression(left.object) && t.isIdentifier(left.object.object, { name: 'module' }) && left.object.property.name === 'exports')) {
105
- names.add('default');
106
- if (typeof property === 'string') names.add(property);
107
- }
108
- }
109
- for (const key of t.VISITOR_KEYS[node.type] || []) {
110
- const child = node[key];
111
- if (Array.isArray(child)) child.forEach(visit); else visit(child);
112
- }
113
69
  }
114
- visit(ast.program);
115
- names.delete('__esModule');
116
70
  return names;
117
71
  }
118
72
 
@@ -177,16 +131,18 @@ function routeEntry(route, globalError, dev) {
177
131
  }
178
132
 
179
133
  /** Compile independent React Server and client graphs. The RSC graph contains only references to client boundaries. */
180
- export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, reactCompiler = false, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {} }) {
134
+ export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, devActionSecret, reactCompiler = false, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {},lazyDynamicImports=false,devLazyRequest,devLazyOnly=false,devLazyActivated=[], parallelBuilds=false }) {
135
+ if(devLazyOnly&&(!dev||!lazyDynamicImports||!devLazyRequest))throw Error('Deferred-only compilation requires a registered development import');
181
136
  const refresh = dev ? createRefreshTransform(project.root, {generation:manifest.cacheId,forceGeneration:manifest.devForceRefresh,forceFiles:manifest.devAffectedModules}) : undefined;
182
137
  const locations = dev ? createSourceLocationTransform(project.root) : undefined;
183
138
  const compileReact = reactCompiler ? createReactCompilerTransform(project.root, reactCompiler, {dev}) : undefined;
184
139
  const { basePath = '', assetPrefix = '', assetBase = '/_prnext/assets', trailingSlash = false, skipTrailingSlashRedirect = false } = manifest.config || {};
185
140
  const publicPath = assetBase + '/';
186
- const routes = project.routes.filter(route => route.router === 'app');
141
+ const lazy=dev&&lazyDynamicImports?createLazyDynamicImports({root:project.root,stage,manifest,request:devLazyRequest,activated:devLazyActivated}):undefined;
142
+ const routes = devLazyOnly?[]:project.routes.filter(route => route.router === 'app');
187
143
  const htmlOnly = dev ? new Set() : selectHtmlOnlyRoutes(project.routes, manifest.config.htmlOnly, manifest.config);
188
144
  const htmlGraph = !dev && htmlOnly.size ? createHtmlOnlyGraph(project.root) : undefined;
189
- if (project.appNotFound) {
145
+ if (project.appNotFound&&!devLazyOnly) {
190
146
  const file = path.join(stage, '.entries', 'app-not-found.jsx');
191
147
  await writeFile(file, `import {notFound} from 'next/navigation';export default function MissingRoute(){notFound()}`);
192
148
  const segments = project.appNotFound.segments.map(segment => ({ ...segment }));
@@ -199,12 +155,12 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
199
155
  routes.push({ ...project.appNotFound, segments, id, file, pattern: '/_not-found', kind: 'page', router: 'app', internal: true });
200
156
  manifest.appNotFound = id;
201
157
  }
202
- if (!routes.length) return;
158
+ if (!routes.length&&!devLazyOnly) return;
203
159
  if (!manifest.config.cacheComponents && routes.some(route => route.cacheConfig?.instant !== undefined || route.pageConfig?.instant !== undefined || route.segments?.some(segment => segment.staticConfig?.instant !== undefined))) throw new Error('instant requires cacheComponents: true.');
204
160
  if (!manifest.config.cacheComponents && routes.some(route => route.cacheConfig?.ensureStatic !== undefined || route.pageConfig?.ensureStatic !== undefined || route.segments?.some(segment => segment.staticConfig?.ensureStatic !== undefined))) throw new Error('ensureStatic requires cacheComponents: true.');
205
- const actions = createServerActions({ projectRoot: project.root });
206
- function actionTransform(source, file, mode, runtime = 'nodejs') {
207
- const result = actions.transform(source, file, mode);
161
+ const actions = createServerActions({ projectRoot: project.root,developmentSession:dev?manifest.devSession:undefined,developmentSecret:dev?devActionSecret:undefined });
162
+ function actionTransform(source, file, mode, runtime = 'nodejs', revisionSource = source) {
163
+ const result = actions.transform(source, file, mode, {revisionSource});
208
164
  if (mode !== 'browser') for (const action of actions.actions.values()) if (action.file === file) {
209
165
  (action.runtimes ||= new Set()).add(runtime);
210
166
  }
@@ -226,6 +182,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
226
182
  for (const segment of route.segments || []) if (segment.layout) cacheSegments.set(await canonicalFile(segment.layout), 'layout');
227
183
  }
228
184
  let browserStyleRevision;
185
+ const writeParallelStyles = parallelBuilds ? createStyleOutputWriter(path.join(stage, 'assets')) : undefined;
229
186
  const resolveFromProject = createRequire(path.join(project.root, 'package.json'));
230
187
  await validateReactVersions(resolveFromProject);
231
188
  const nativePackages = createNativePackages(project.root, { isClientSource: frameworkDirective });
@@ -314,14 +271,17 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
314
271
  htmlGraph?.source(args.path, original);
315
272
  // Only Client Component graphs use React's memo-cache runtime. Server
316
273
  // Components, route handlers and Server Action graphs remain untouched.
317
- const compiled=compileReact && (browser || mode==='ssr') ? await compileReact(original,args.path,{loaderOutput:args.prnextSource!==undefined}) : original;
318
- const source = await fonts.transform(await expandActionExports(locations?await locations(compiled,args.path):compiled, args.path, async (specifier, importer) => {
274
+ const compile = compileReact && (browser || mode==='ssr');
275
+ const locateFirst = !!(compile && reactCompiler.backend==='oxc' && locations);
276
+ const input = locateFirst ? await locations(original,args.path) : original;
277
+ const compiled = compile ? await compileReact(input,args.path,{loaderOutput:args.prnextSource!==undefined}) : input;
278
+ const source = await fonts.transform(await expandActionExports(locations && !locateFirst ? await locations(compiled,args.path) : compiled, args.path, async (specifier, importer) => {
319
279
  const resolved = await esbuild.resolve(specifier, { resolveDir: path.dirname(importer), kind: 'import-statement', pluginData: { prnextExportScan: true } });
320
280
  return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
321
281
  }), args.path);
322
282
  const cacheSegment = cacheSegments.get(await canonicalFile(args.path)) || '';
323
283
  const cachedSource = transformCacheComponents(source, args.path, { enabled: manifest.config.cacheComponents, handlers: manifest.config.cacheHandlers, projectRoot: project.root, mode, buildId: manifest.cacheId, segment: cacheSegment });
324
- const actionSource = actionTransform(cachedSource, args.path, mode);
284
+ const actionSource = actionTransform(cachedSource, args.path, mode, 'nodejs', original);
325
285
  const builtin = path.dirname(args.path) === path.join(packageRoot, 'compat') && forcedClient.has(path.basename(args.path));
326
286
  if (rsc && (builtin || clientDirective(source, args.path))) {
327
287
  htmlGraph?.issue(args.path, 'a Client Component or interactive framework component');
@@ -334,8 +294,9 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
334
294
  const references = boundary.exports.map((name, index) => builtin && path.basename(args.path) === 'image.cjs' && name === 'getImageProps' ? `export {getImageProps} from ${JSON.stringify(path.join(packageRoot, 'compat/image-shared.cjs'))};` : `const ref${index}=registerClientReference(function(){throw new Error(${JSON.stringify(`Cannot call client export ${name} from a Server Component (${path.relative(project.root, args.path)}).`)});},${JSON.stringify(boundary.id)},${JSON.stringify(name)});export {ref${index} as ${JSON.stringify(name)}};`);
335
295
  return { contents: `import {registerClientReference} from 'react-server-dom-webpack/server';\n${references.join('\n')}`, loader: 'js', resolveDir: path.dirname(args.path) };
336
296
  }
337
- let contents = transformDynamicImports(actionSource, args.path, { projectRoot: project.root, mode: rsc ? 'rsc' : browser ? 'browser' : 'server' });
338
- if (browser && refresh && !actions.isServerModule(cachedSource,args.path)) contents = await refresh(contents, args.path);
297
+ let contents = transformDynamicImports(actionSource, args.path, { projectRoot: project.root, mode: rsc ? 'rsc' : browser ? 'browser' : 'server',dev });
298
+ if(browser&&lazy)contents=lazy.transform(contents,args.path);
299
+ if (browser && refresh && !actions.isServerModule(cachedSource,args.path,{revisionSource:original})) contents = await refresh(contents, args.path);
339
300
  return { contents, loader: extensions[path.extname(args.path)] || 'jsx', resolveDir: path.dirname(args.path) };
340
301
  });
341
302
  },
@@ -397,11 +358,14 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
397
358
  const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport, transformTypedHelpers:!!compileReact && mode==='ssr',
398
359
  plugins: [moduleResolutionPlugin(moduleResolution, project.root), edgeGraphPlugin(mode), images.plugin(), fonts.plugin(), cssModules.plugin(false)],
399
360
  transformSource: async (source, file, builder, {loaderOutput=false}={}) => {
400
- const compiled=compileReact && mode==='ssr' ? await compileReact(source,file,{loaderOutput}) : source;
401
- return actionTransform(await fonts.transform(await expandActionExports(locations?await locations(compiled,file):compiled, file, async (specifier, importer) => {
361
+ const compile = compileReact && mode==='ssr';
362
+ const locateFirst = !!(compile && reactCompiler.backend==='oxc' && locations);
363
+ const input = locateFirst ? await locations(source,file) : source;
364
+ const compiled = compile ? await compileReact(input,file,{loaderOutput}) : input;
365
+ return actionTransform(await fonts.transform(await expandActionExports(locations && !locateFirst ? await locations(compiled,file) : compiled, file, async (specifier, importer) => {
402
366
  const resolved = await builder.resolve(specifier, {resolveDir:path.dirname(importer),kind:'import-statement',pluginData:{prnextExportScan:true}});
403
367
  return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
404
- }), file), file, mode, 'edge');
368
+ }), file), file, mode, 'edge', source);
405
369
  } });
406
370
  fonts.record(result.metafile);
407
371
  styleGraph.record(result.metafile);
@@ -435,35 +399,39 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
435
399
  fonts.record(output.metafile);
436
400
  styleGraph.record(output.metafile);
437
401
  htmlGraph?.record(output.metafile);
402
+ const chunks = new Set(Object.keys(output.metafile.outputs).filter(file => file.endsWith('.mjs')).map(file => path.basename(file)));
403
+ const importers = new Set(Object.entries(output.metafile.outputs).filter(([file,metadata])=>file.endsWith('.mjs')&&metadata.imports.some(item=>!item.external)).map(([file])=>path.resolve(project.root,file)));
404
+ output={...output,outputFiles:rewriteServerChunkOutputFiles(output.outputFiles,chunks,assetBase,importers)};
438
405
  for (const file of output.outputFiles) {
439
406
  await mkdir(path.dirname(file.path), { recursive: true });
440
407
  await writeFile(file.path, file.contents);
441
408
  }
442
- const chunks = new Set(Object.keys(output.metafile.outputs).filter(file => file.endsWith('.mjs')).map(file => path.basename(file)));
443
409
  for (const [file, metadata] of Object.entries(output.metafile.outputs)) {
444
410
  const absolute = path.resolve(project.root, file);
445
- if (file.endsWith('.mjs') && metadata.imports.some(item => !item.external)) await rewriteServerChunks(absolute, chunks, assetBase);
446
411
  if (!metadata.entryPoint && !file.endsWith('.map') && !file.endsWith('.mjs')) await cp(absolute, path.join(stage, 'assets', path.basename(file)));
447
412
  }
448
413
  return output;
449
414
  }
450
415
 
451
- async function collectBrowserStyles(browserEntries) {
416
+ async function collectBrowserStyles(browserEntries,force=false) {
452
417
  const revision = `${boundaries.size}:${actions.actions.size}:${nativePackages.version}`;
453
- if (browserStyleRevision === revision) return;
418
+ if (!force&&browserStyleRevision === revision) return;
454
419
  // SSR intentionally erases ssr:false loaders. Discover their dependencies
455
420
  // in one browser graph, then classify styles by reachability from each root.
456
421
  const discovery = await bundle({ ...common, write: false, entryPoints: browserEntries, outdir: path.join(stage, '.browser-styles'),
457
422
  platform: 'browser', format: 'esm', target: ['es2022'], splitting: true,
458
423
  define: { 'process.env': '{}', ...common.define },
459
- plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), nativePackages.plugin('browser'), cssModules.plugin(true)] });
424
+ plugins: [...(lazy?[lazy.plugin()]:[]),moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), nativePackages.plugin('browser'), cssModules.plugin(true)] });
425
+ await lazy?.discover(discovery.metafile);
460
426
  fonts.record(discovery.metafile);
461
427
  styleGraph.record(discovery.metafile);
462
- const entryIds = new Map(await Promise.all(Object.entries(browserEntries).map(async ([id, file]) => [await canonicalFile(file), id])));
428
+ const entryIds = new Map((await Promise.all(Object.entries(browserEntries).map(async ([id, file]) => [[path.resolve(file), id], [await canonicalFile(file), id]]))).flat());
463
429
  const globalRoots = [], normalRoots = [];
464
430
  for (const output of Object.values(discovery.metafile.outputs)) {
465
431
  if (!output.entryPoint) continue;
466
- const id = entryIds.get(await canonicalFile(path.resolve(project.root, output.entryPoint)));
432
+ // Dynamic entry points can contain a query/fragment identity rather
433
+ // than a physical filename. Only configured roots need classification.
434
+ const id = entryIds.get(path.resolve(project.root, output.entryPoint));
467
435
  if (id) (id === globalErrorId ? globalRoots : normalRoots).push(output.entryPoint);
468
436
  }
469
437
  async function collect(roots, target) {
@@ -514,12 +482,13 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
514
482
  const browserRevision = () => `${boundaryRevision()}:${cssFiles.size}:${globalErrorCssFiles.size}:${nativePackages.version}`;
515
483
  // Route Handlers have a Node React environment and must not share RSC chunks
516
484
  // that import registerServerReference or client-reference registration.
517
- await serverBuild(handlerEntries, 'server', 'route');
485
+ if (!devLazyOnly) await serverBuild(handlerEntries, 'server', 'route');
518
486
  for (;;) {
519
487
  // Each graph consumes discoveries from the preceding one. Discovering a
520
488
  // client boundary in RSC is normal: SSR and browser below already include
521
489
  // it. Only discoveries made too late for a graph require another pass.
522
490
  const emittedActions = actionRevision();
491
+ if (!devLazyOnly) {
523
492
  for (const action of actions.actions.values()) {
524
493
  const entry = path.join(stage, '.entries', 'action-' + action.id + '.mjs');
525
494
  await writeFile(entry, `export {${JSON.stringify(action.binding)} as invoke} from ${JSON.stringify(action.file)};`);
@@ -527,8 +496,10 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
527
496
  if (!action.runtimes?.size || action.runtimes.has('nodejs')) entries['action-' + action.id] = entry;
528
497
  }
529
498
  await serverBuild(entries, 'server', 'rsc');
499
+ }
530
500
  const serverNativeVersion = nativePackages.version;
531
501
  const emittedBoundaries = boundaryRevision();
502
+ if (!devLazyOnly) {
532
503
  const ssrEntries = {};
533
504
  const globalSsrEntries = {};
534
505
  for (const boundary of boundaries.values()) {
@@ -539,63 +510,78 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
539
510
  await serverBuild(ssrEntries, 'app-ssr', 'ssr');
540
511
  for (const boundary of boundaries.values()) if (boundary.edge) await edgeAppBuild(path.join(stage, '.entries', boundary.id + '.ssr.mjs'), path.join(stage, 'app-ssr-edge', boundary.id + '.mjs'), 'ssr', boundary.exports.includes('default'));
541
512
  await serverBuild(globalSsrEntries, 'app-ssr', 'ssr', globalErrorCssFiles);
513
+ }
542
514
  const browserEntries = {};
543
515
  for (const boundary of boundaries.values()) {
544
516
  const entry = path.join(stage, '.entries', boundary.id + '.browser.mjs');
545
517
  await writeFile(entry, boundaryEntry(boundary));
546
518
  browserEntries[boundary.id] = entry;
547
519
  }
548
- await collectBrowserStyles(browserEntries);
520
+ if(!devLazyRequest)await collectBrowserStyles(browserEntries);
521
+ let lazyEntries={};
522
+ if(lazy)for(;;){
523
+ const next=await lazy.targetEntries();
524
+ if(Object.keys(next).join(',')===Object.keys(lazyEntries).join(','))break;
525
+ lazyEntries=next;
526
+ await collectBrowserStyles(devLazyRequest?lazyEntries:{...browserEntries,...lazyEntries},true);
527
+ }
549
528
  globalErrorOnlyCss.clear();
550
529
  for (const file of globalErrorCssFiles) if (!cssFiles.has(file)) globalErrorOnlyCss.add(file);
551
530
  const emittedBrowserInputs = browserRevision();
552
- if (routes.some(route => route.kind === 'page')) {
531
+ if (devLazyOnly||routes.some(route => route.kind === 'page')) {
553
532
  let globalErrorCss = [];
554
533
  if (globalErrorCssFiles.size) {
555
534
  const styleEntry = path.join(stage, '.entries', 'app-global-error-style.mjs');
556
535
  await writeFile(styleEntry, [...globalErrorCssFiles].map(file => `import ${JSON.stringify(file)};`).join('\n'));
557
536
  const styles = await bundle({ ...common, entryPoints: { 'app-global-error-style': styleEntry }, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]',
558
537
  platform: 'browser', format: 'esm', target: ['es2022'], minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
559
- plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] });
538
+ plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] }, { role: 'styles' });
560
539
  globalErrorCss = Object.values(styles.metafile.outputs).filter(output => output.cssBundle).map(output => publicPath + path.basename(output.cssBundle));
561
540
  }
562
541
  if (globalErrorId) manifest.app.globalError = { id: globalErrorId, name: 'default', css: globalErrorCss };
563
542
  const runtimeEntry = path.join(stage, '.entries', 'app-runtime.mjs');
564
543
  const htmlOnlyRoutes = !dev && htmlOnly.size ? routes.filter(route => route.kind === 'page' && !route.internal).map(route => ({pattern:route.pattern,...(htmlOnly.has(route.id)?{htmlOnly:true}:{})})) : undefined;
565
544
  const importers = Object.entries(browserEntries).map(([id, file]) => `${JSON.stringify(id)}:()=>import(${JSON.stringify(file)})`);
566
- await writeFile(runtimeEntry, `${dev ? `import ${JSON.stringify(devClientFile)};\n` : ''}\nimport {bootstrapApp} from ${JSON.stringify(path.join(packageRoot, 'runtime/app-client.mjs'))};\nbootstrapApp({clientModules:{${importers.join(',')}},...${JSON.stringify({ basePath, assetPrefix, assetBase, htmlOnlyRoutes, trailingSlash, skipTrailingSlashRedirect, deploymentId: dev ? undefined : manifest.cacheId, strictMode: manifest.config.reactStrictMode !== false, cacheComponents: manifest.config.cacheComponents === true, partialPrefetching: manifest.config.partialPrefetching === true, navigationCache: manifest.config.navigationCache, ...(globalErrorId ? { globalErrorId, globalErrorCss } : {}), ...(dev ? { dev: { buildId: manifest.buildId, clientManifest: manifest.devClient, onDemand:manifest.devOnDemand===true, sequence:manifest.devSequence, session:manifest.devSession, indicator:manifest.devIndicators, ...(manifest.devTools ? {tools:manifest.devTools} : {}) } } : {}) })}});\n`);
567
- const clientEntries = { 'app-runtime': runtimeEntry, ...browserEntries };
545
+ if(!devLazyRequest)await writeFile(runtimeEntry, `${dev ? `import ${JSON.stringify(devClientFile)};\n` : ''}\nimport {bootstrapApp} from ${JSON.stringify(path.join(packageRoot, 'runtime/app-client.mjs'))};\nbootstrapApp({clientModules:{${importers.join(',')}},...${JSON.stringify({ basePath, assetPrefix, assetBase, htmlOnlyRoutes, trailingSlash, skipTrailingSlashRedirect, deploymentId: dev ? undefined : manifest.cacheId, strictMode: manifest.config.reactStrictMode !== false, cacheComponents: manifest.config.cacheComponents === true, partialPrefetching: manifest.config.partialPrefetching === true, navigationCache: manifest.config.navigationCache, ...(globalErrorId ? { globalErrorId, globalErrorCss } : {}), ...(dev ? { dev: { buildId: manifest.buildId, clientManifest: manifest.devClient, onDemand:manifest.devOnDemand===true, sequence:manifest.devSequence, session:manifest.devSession, indicator:manifest.devIndicators, ...(manifest.devTools ? {tools:manifest.devTools} : {}) } } : {}) })}});\n`);
546
+ const clientEntries = devLazyRequest?{...lazyEntries,...lazy.bridgeEntries()}:{ 'app-runtime': runtimeEntry, ...browserEntries,...lazyEntries,...(lazy?lazy.bridgeEntries():{}) };
568
547
  const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'app-chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
569
- define: { 'process.env': '{}', ...common.define, __PRNEXT_CACHE_COMPONENTS__: JSON.stringify(manifest.config.cacheComponents === true) }, plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitAllStyles: true }), nativePackages.plugin('browser'), cssModules.plugin(true, { emitStyles: () => false })] });
548
+ define: { 'process.env': '{}', ...common.define, __PRNEXT_CACHE_COMPONENTS__: JSON.stringify(manifest.config.cacheComponents === true) }, plugins: [...(lazy?[lazy.plugin()]:[]),moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitAllStyles: !lazy }), nativePackages.plugin('browser'), cssModules.plugin(true, { emitStyles: () => !!lazy })] });
570
549
  fonts.record(browser.metafile);
571
550
  const entryIds = new Map(Object.entries(clientEntries).map(([id, file]) => [file, id]));
572
551
  const edgeBoundaries = new Set([...boundaries.values()].filter(boundary => boundary.edge).map(boundary => boundary.id));
573
552
  let client;
574
553
  for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
575
554
  if (!metadata.entryPoint || !output.endsWith('.js')) continue;
555
+ if(lazy?.record(metadata,output))continue;
576
556
  const id = entryIds.get(path.resolve(project.root, metadata.entryPoint));
577
557
  if (id === 'app-runtime') { client = publicPath + path.basename(output); continue; }
578
558
  if (!id) continue;
579
559
  manifest.app.clientModules[id] = { id, chunks: [id, publicPath + path.basename(output)], name: '*', ssrModule: `app-ssr/${id}.mjs`, ...(edgeBoundaries.has(id) ? { edgeSsrModule: `app-ssr-edge/${id}.mjs` } : {}), browserModule: publicPath + path.basename(output) };
580
560
  }
581
- const styleBundles = new Map();
582
- for (const route of routes.filter(route => route.kind === 'page')) {
561
+ const styledRoutes = routes.filter(route => route.kind === 'page');
562
+ for (const route of styledRoutes) {
583
563
  // All esbuild graphs canonicalize their inputs, including symlinked
584
564
  // convention files. Match those identities before querying reachability.
585
565
  for (const files of appStyleScopes(route)) for (const file of files) styleGraph.alias(file, await canonicalFile(file));
586
- const plan = planAppStyleEntries(route, styleGraph);
566
+ }
567
+ // Freeze discovery before any independent emission starts. Deduplication
568
+ // owns each output path once, and linking follows source order even when
569
+ // builds complete out of order. Failure drains all started jobs first.
570
+ const plan = immutableStylePlan(styledRoutes, route => planAppStyleEntries(route, styleGraph));
571
+ const {outputs: styleBundles, concurrency} = await executeStylePlan(plan, async ({styles: files}) => {
572
+ const id = 'app-style-' + createHash('sha256').update(files.map(file => path.relative(project.root, file).replaceAll(path.sep, '/')).join('\0')).digest('hex').slice(0, 16);
573
+ const styleEntry = path.join(stage, '.entries', id + '.mjs');
574
+ await writeFile(styleEntry, files.map(file => `import ${JSON.stringify(file)};`).join('\n'));
575
+ const output = await bundle({ ...common, ...(writeParallelStyles ? {write: false} : {}), entryPoints: { [id]: styleEntry }, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
576
+ plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] }, { role: 'styles' });
577
+ if (writeParallelStyles) await writeParallelStyles(output.outputFiles);
578
+ return Object.values(output.metafile.outputs).filter(output => output.cssBundle).map(output => publicPath + path.basename(output.cssBundle));
579
+ }, dev ? false : parallelBuilds);
580
+ if (!dev && parallelBuilds) manifest.buildParallelism = {version: 1, phase: 'app-styles', jobs: plan.jobs.length, concurrency};
581
+ for (const route of plan.routes) {
587
582
  const styles = [];
588
583
  const cssScopes = [];
589
- for (const { styles:files, path:scopePath, kind } of plan) {
590
- const signature = JSON.stringify(files);
591
- if (!styleBundles.has(signature)) {
592
- const id = 'app-style-' + createHash('sha256').update(files.map(file => path.relative(project.root, file).replaceAll(path.sep, '/')).join('\0')).digest('hex').slice(0, 16);
593
- const styleEntry = path.join(stage, '.entries', id + '.mjs');
594
- await writeFile(styleEntry, files.map(file => `import ${JSON.stringify(file)};`).join('\n'));
595
- const output = await bundle({ ...common, entryPoints: { [id]: styleEntry }, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
596
- plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] });
597
- styleBundles.set(signature, Object.values(output.metafile.outputs).filter(output => output.cssBundle).map(output => publicPath + path.basename(output.cssBundle)));
598
- }
584
+ for (const { signature, path:scopePath, kind } of route.scopes) {
599
585
  const css = styleBundles.get(signature);
600
586
  styles.push(...css);
601
587
  if (route.parallel) cssScopes.push({ path:scopePath, kind, css });
@@ -610,10 +596,13 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
610
596
  module: `server/action-${action.runtimes?.has('nodejs')?'':'edge-'}${action.id}.mjs`, export: 'invoke',
611
597
  ...(action.runtimes?.has('edge') ? {edgeModule:`server/action-edge-${action.id}.mjs`} : {}),
612
598
  }]));
613
- if (emittedActions === actionRevision() && emittedBoundaries === boundaryRevision() &&
599
+ if ((devLazyOnly || emittedActions === actionRevision() && emittedBoundaries === boundaryRevision()) &&
614
600
  serverNativeVersion === nativePackages.version && emittedBrowserInputs === browserRevision()) break;
615
601
  }
616
602
 
603
+ lazy?.finish();
604
+ if (dev) manifest.devActions = developmentActionMetadata(project.root, actions.actions.values());
605
+
617
606
  if (htmlGraph) for (const route of routes) if (htmlOnly.has(route.id)) {
618
607
  for (const file of [...appStyleScopes(route).flat(), ...(route.metadataFiles || []).map(item=>item.file)].filter(Boolean)) htmlGraph.alias(file, await canonicalFile(file));
619
608
  htmlGraph.validate(route);
@@ -1,71 +1,2 @@
1
- import { mkdir, writeFile, rename, rm, realpath, lstat, open } from 'node:fs/promises';
2
- import { randomUUID } from 'node:crypto';
3
- import { hostname } from 'node:os';
4
- import path from 'node:path';
5
-
6
- const LOCK = '.prnext-build.lock';
7
- const busy = message => Object.assign(new Error(message), { code: 'PRNEXT_BUILD_LOCKED' });
8
- function alive(pid) {
9
- try { process.kill(pid, 0); return true; }
10
- catch (error) { return error.code !== 'ESRCH'; }
11
- }
12
- async function owner(directory) {
13
- const info = await lstat(directory);
14
- if (!info.isDirectory() || info.isSymbolicLink()) throw busy(`The project lock is not a real directory. Check ${path.basename(directory)} before retrying.`);
15
- const file = await open(path.join(directory, 'owner.json'), 'r');
16
- try {
17
- if ((await file.stat()).size > 4096) throw new Error('oversized owner');
18
- const value = JSON.parse(await file.readFile('utf8'));
19
- if (!Number.isSafeInteger(value.pid) || value.pid < 1 || typeof value.host !== 'string' || !/^[a-f0-9-]{36}$/.test(value.token)) throw new Error('invalid owner');
20
- return value;
21
- } finally { await file.close(); }
22
- }
23
-
24
- /** One publisher per canonical project, covering React and direct static imports. */
25
- export async function withBuildLock(directory, action, {development = false} = {}) {
26
- const root = await realpath(directory), lock = path.join(root, development ? '.prnext-dev.lock' : LOCK);
27
- const identity = { pid: process.pid, host: hostname(), token: randomUUID(), createdAt: new Date().toISOString() };
28
- const prepared = path.join(root, `.prnext-lock-${identity.token}`);
29
- await mkdir(prepared, { mode: 0o700 });
30
- try {
31
- await writeFile(path.join(prepared, 'owner.json'), JSON.stringify(identity), { flag: 'wx', mode: 0o600 });
32
- for (let attempt = 0; ; attempt++) {
33
- try { await rename(prepared, lock); break; }
34
- catch (error) {
35
- if (!['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES'].includes(error.code)) throw error;
36
- let previous;
37
- try { previous = await owner(lock); }
38
- catch (cause) {
39
- if (cause.code === 'ENOENT' && attempt < 8) continue;
40
- throw busy(`Cannot verify the project lock. Check ${path.basename(lock)} before retrying.`);
41
- }
42
- if (previous.host !== identity.host || alive(previous.pid)) throw busy(development ? `A development server is already running for this project (PID ${previous.pid}). Stop it before starting another.` : `A build is already running for this project (PID ${previous.pid}). Wait for it to finish.`);
43
- // Exactly one contender can recover this generation. Never delete a lock
44
- // merely because it is old: long builds and suspended processes are valid.
45
- let claim;
46
- try { claim = await open(path.join(lock, 'recovery'), 'wx', 0o600); }
47
- catch (cause) {
48
- if (cause.code === 'ENOENT' && attempt < 8) continue;
49
- throw busy(`Project lock recovery is in progress. Retry shortly; if recovery was interrupted, check ${path.basename(lock)} manually.`);
50
- }
51
- const retired = path.join(root, `.prnext-lock-retired-${identity.token}`);
52
- try {
53
- await claim.writeFile(JSON.stringify(identity)); await claim.close(); claim = null;
54
- const current = await owner(lock);
55
- if (current.token !== previous.token) {
56
- // Do not unlink through this path: the new owner can release and
57
- // another generation can appear between our read and the unlink.
58
- throw busy('The build lock changed during recovery. Retry the build.');
59
- }
60
- await rename(lock, retired);
61
- await rm(retired, { recursive: true });
62
- } finally { await claim?.close(); }
63
- }
64
- }
65
- try { return await action(root); }
66
- finally {
67
- // Ownership must still match before releasing a lock.
68
- if ((await owner(lock)).token === identity.token) await rm(lock, { recursive: true });
69
- }
70
- } finally { await rm(prepared, { recursive: true, force: true }); }
71
- }
1
+ // Compiler entry point for the shared publisher and maintenance lock protocol.
2
+ export {withBuildLock} from '../runtime/project-lock.mjs';