@thomas.f/prnext 0.1.16-alpha → 0.1.18-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 (215) hide show
  1. package/AI.md +63 -0
  2. package/README.md +35 -503
  3. package/build/ADAPTERS.md +27 -2
  4. package/build/actions.mjs +6 -1
  5. package/build/analyze.mjs +340 -0
  6. package/build/app-config.mjs +20 -8
  7. package/build/app-static.mjs +41 -14
  8. package/build/app-styles.mjs +79 -0
  9. package/build/app.mjs +99 -31
  10. package/build/build-lock.mjs +6 -6
  11. package/build/bundle-runtime.mjs +39 -11
  12. package/build/cache-components.mjs +1 -1
  13. package/build/cache-handlers.mjs +1 -1
  14. package/build/check.mjs +56 -11
  15. package/build/compact-css.mjs +53 -0
  16. package/build/compilation-cache.mjs +269 -0
  17. package/build/compilation-store.mjs +142 -0
  18. package/build/compiler.mjs +51 -4
  19. package/build/concurrent.mjs +12 -0
  20. package/build/config.mjs +50 -13
  21. package/build/create.mjs +239 -0
  22. package/build/css-modules.mjs +65 -10
  23. package/build/deployment-assets.mjs +89 -0
  24. package/build/deployment-config.mjs +18 -0
  25. package/build/dev-compiler-health.mjs +11 -0
  26. package/build/dev-css-update.mjs +85 -0
  27. package/build/dev-dependencies.mjs +44 -0
  28. package/build/dev-discovery.mjs +28 -0
  29. package/build/dev-worker.mjs +14 -5
  30. package/build/dev.mjs +64 -6
  31. package/build/dynamic.mjs +1 -1
  32. package/build/edge.mjs +5 -4
  33. package/build/fonts.mjs +1 -1
  34. package/build/framework-imports.mjs +50 -0
  35. package/build/html-only-config.mjs +53 -0
  36. package/build/html-only-graph.mjs +67 -0
  37. package/build/incremental.mjs +8 -4
  38. package/build/index.mjs +140 -112
  39. package/build/inspect.mjs +3 -0
  40. package/build/inspection-summary.mjs +37 -4
  41. package/build/instrumentation.mjs +30 -0
  42. package/build/loader-cache.mjs +53 -26
  43. package/build/metadata-routes.mjs +3 -4
  44. package/build/migrate.mjs +60 -57
  45. package/build/migration-audit.mjs +1 -1
  46. package/build/native-type-config.mjs +37 -0
  47. package/build/npm-packages.mjs +3 -18
  48. package/build/package-manager.mjs +308 -0
  49. package/build/persistent-cache.mjs +112 -0
  50. package/build/precompress.mjs +62 -30
  51. package/build/prerender-cache.mjs +70 -0
  52. package/build/prerender-config.mjs +29 -0
  53. package/build/prerender-pages.mjs +100 -0
  54. package/build/prerender-pool.mjs +165 -0
  55. package/build/prerender-process-worker.mjs +58 -0
  56. package/build/prerender-worker.mjs +10 -0
  57. package/build/production-builder.mjs +129 -0
  58. package/build/react-compiler-report.mjs +225 -0
  59. package/build/react-compiler.mjs +139 -0
  60. package/build/runtime-contract.mjs +2 -2
  61. package/build/scan.mjs +4 -4
  62. package/build/source-analysis.mjs +42 -0
  63. package/build/static-adapter.mjs +13 -5
  64. package/build/static-export.mjs +4 -4
  65. package/build/static-input.mjs +48 -0
  66. package/build/styles.mjs +17 -3
  67. package/build/transform-cache.mjs +37 -4
  68. package/build/transform.mjs +1 -1
  69. package/build/type-command.mjs +41 -0
  70. package/build/typed-routes.mjs +95 -0
  71. package/build/typescript-dev-worker.mjs +18 -0
  72. package/build/typescript-dev.mjs +49 -0
  73. package/build/typescript-native-worker.mjs +55 -0
  74. package/build/typescript-native.mjs +18 -0
  75. package/build/typescript-toolchain.mjs +19 -0
  76. package/build/typescript-watch.mjs +22 -0
  77. package/build/typescript.mjs +11 -8
  78. package/build/webpack-graph.mjs +170 -30
  79. package/build/worker-memory.mjs +9 -0
  80. package/build/worker-preload.mjs +9 -0
  81. package/cli.mjs +162 -122
  82. package/compat/activity-cache.cjs +26 -0
  83. package/compat/after.cjs +6 -4
  84. package/compat/app-layout-context.cjs +52 -2
  85. package/compat/base64.cjs +10 -0
  86. package/compat/cache-handlers.cjs +12 -2
  87. package/compat/cache-wire.cjs +28 -0
  88. package/compat/cache.cjs +2 -1
  89. package/compat/cache.d.cts +4 -0
  90. package/compat/cookies.cjs +21 -5
  91. package/compat/data-cache.cjs +12 -8
  92. package/compat/flight-delta.cjs +150 -0
  93. package/compat/headers.cjs +4 -2
  94. package/compat/incremental-cache.cjs +20 -7
  95. package/compat/index.d.ts +89 -10
  96. package/compat/link.cjs +12 -9
  97. package/compat/link.d.cts +4 -3
  98. package/compat/navigation-authorization.cjs +9 -0
  99. package/compat/navigation-stages.cjs +19 -0
  100. package/compat/navigation.d.cts +4 -3
  101. package/compat/offline-store.cjs +11 -0
  102. package/compat/offline.cjs +5 -0
  103. package/compat/offline.d.cts +2 -0
  104. package/compat/redis-cache.cjs +201 -0
  105. package/compat/redis-cache.d.cts +37 -0
  106. package/compat/router.d.cts +4 -3
  107. package/compat/server-primitives.cjs +31 -11
  108. package/compat/server.d.cts +15 -2
  109. package/compat/static-generation.cjs +7 -0
  110. package/compat/typed-route.d.cts +17 -0
  111. package/compat/use-cache.cjs +3 -0
  112. package/docs/ai/compatibility.md +275 -0
  113. package/docs/ai/data.md +288 -0
  114. package/docs/ai/deployment.md +269 -0
  115. package/docs/ai/manifest.json +11 -0
  116. package/docs/ai/quickstart.md +171 -0
  117. package/docs/ai/routing.md +214 -0
  118. package/docs/ai/troubleshooting.md +155 -0
  119. package/index.mjs +2 -0
  120. package/native/diagnostics.mjs +34 -0
  121. package/native/resolve.mjs +7 -2
  122. package/package.json +44 -7
  123. package/release/ai-docs.mjs +48 -0
  124. package/release/verify.mjs +2 -0
  125. package/runtime/action-client.mjs +10 -4
  126. package/runtime/action-server.mjs +2 -0
  127. package/runtime/ai-docs.mjs +202 -0
  128. package/runtime/app-client.mjs +226 -68
  129. package/runtime/app-content.mjs +4 -3
  130. package/runtime/app-partial-client.mjs +3 -0
  131. package/runtime/app-partial.mjs +1 -1
  132. package/runtime/app-prefetch-constants.mjs +4 -0
  133. package/runtime/app-prefetch.mjs +2 -3
  134. package/runtime/app-render-key.mjs +5 -0
  135. package/runtime/app-render.mjs +100 -28
  136. package/runtime/app-routing.mjs +22 -9
  137. package/runtime/app-static.mjs +18 -8
  138. package/runtime/app-stream-html.mjs +10 -53
  139. package/runtime/app-style-selection.mjs +34 -0
  140. package/runtime/app-styles.mjs +154 -0
  141. package/runtime/cache-components-rsc.mjs +4 -1
  142. package/runtime/cache-request.mjs +2 -1
  143. package/runtime/cloud/archive.mjs +5 -6
  144. package/runtime/compile-cache.mjs +8 -0
  145. package/runtime/deployment-client.mjs +19 -0
  146. package/runtime/dev-ai-brands.mjs +26 -0
  147. package/runtime/dev-ai-style.mjs +99 -0
  148. package/runtime/dev-build-reasons.mjs +25 -0
  149. package/runtime/dev-cache-control.mjs +53 -0
  150. package/runtime/dev-client.mjs +245 -46
  151. package/runtime/dev-compiler-broker.mjs +42 -0
  152. package/runtime/dev-console-copy.mjs +19 -0
  153. package/runtime/dev-console.mjs +106 -0
  154. package/runtime/dev-dependency-graph.mjs +40 -0
  155. package/runtime/dev-element-picker-style.mjs +27 -0
  156. package/runtime/dev-element-picker.mjs +213 -0
  157. package/runtime/dev-error-overlay-style.mjs +41 -0
  158. package/runtime/dev-error-overlay.mjs +167 -0
  159. package/runtime/dev-error-report.mjs +121 -0
  160. package/runtime/dev-file-changes.mjs +1 -1
  161. package/runtime/dev-gateway.mjs +44 -0
  162. package/runtime/dev-host-relay.mjs +66 -0
  163. package/runtime/dev-indicator.mjs +191 -0
  164. package/runtime/dev-issue-motion.mjs +214 -0
  165. package/runtime/dev-lazy-tool.mjs +34 -0
  166. package/runtime/dev-logo.mjs +2 -0
  167. package/runtime/dev-notification.mjs +81 -0
  168. package/runtime/dev-popout.mjs +34 -0
  169. package/runtime/dev-position.mjs +107 -0
  170. package/runtime/dev-preview-touch.mjs +122 -0
  171. package/runtime/dev-runtime-pool.mjs +45 -0
  172. package/runtime/dev-runtime-spare.mjs +57 -0
  173. package/runtime/dev-tools-client.mjs +155 -0
  174. package/runtime/dev-tools-mcp.mjs +126 -0
  175. package/runtime/dev-tools-metrics.mjs +31 -0
  176. package/runtime/dev-tools-model.mjs +42 -0
  177. package/runtime/dev-tools-panel.mjs +341 -0
  178. package/runtime/dev-tools-result.mjs +37 -0
  179. package/runtime/dev-tools-server.mjs +256 -0
  180. package/runtime/dev-tools-source.mjs +104 -0
  181. package/runtime/dev-tooltip.mjs +94 -0
  182. package/runtime/dev-ui-icons.mjs +8 -0
  183. package/runtime/dev-workbench-style.mjs +106 -0
  184. package/runtime/dev-workbench.mjs +428 -0
  185. package/runtime/dev-worker-logs.mjs +31 -0
  186. package/runtime/development.mjs +324 -0
  187. package/runtime/document.mjs +1 -1
  188. package/runtime/edge.mjs +6 -5
  189. package/runtime/env.mjs +2 -1
  190. package/runtime/fetch-cache.mjs +10 -8
  191. package/runtime/flight-delta.mjs +50 -0
  192. package/runtime/flight-tap.mjs +28 -0
  193. package/runtime/html-only.mjs +27 -0
  194. package/runtime/http.mjs +2 -0
  195. package/runtime/incremental-cache.mjs +25 -4
  196. package/runtime/instrumentation.mjs +49 -0
  197. package/runtime/navigation-recovery.mjs +36 -0
  198. package/runtime/navigation.mjs +7 -0
  199. package/runtime/node-html-stream.mjs +47 -0
  200. package/runtime/pages-client.mjs +24 -8
  201. package/runtime/pages-navigation.mjs +54 -64
  202. package/runtime/persistent/client.mjs +6 -5
  203. package/runtime/persistent/daemon.mjs +66 -56
  204. package/runtime/persistent/native-proxy.mjs +63 -0
  205. package/runtime/prefetch-scheduler.mjs +110 -0
  206. package/runtime/preload.mjs +12 -0
  207. package/runtime/profile.mjs +14 -0
  208. package/runtime/profiles.json +2 -2
  209. package/runtime/render.mjs +3 -0
  210. package/runtime/route-pattern.mjs +44 -0
  211. package/runtime/rsc-worker.mjs +41 -12
  212. package/runtime/trace.mjs +78 -0
  213. package/runtime/typed-route.mjs +19 -0
  214. package/runtime/watch-project.mjs +29 -18
  215. package/runtime/worker.mjs +102 -24
@@ -0,0 +1,142 @@
1
+ // Optional content-addressed compilation outputs. Records contain metadata and
2
+ // references only; binary output bytes are shared across compilation graphs.
3
+ import { constants } from 'node:fs';
4
+ import { open, lstat, rename, unlink, readdir } from 'node:fs/promises';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { serialize, deserialize } from 'node:v8';
7
+ import { deflateRaw, inflateRaw } from 'node:zlib';
8
+ import { promisify } from 'node:util';
9
+ import path from 'node:path';
10
+ import { cacheHash } from './persistent-cache.mjs';
11
+
12
+ const compress = promisify(deflateRaw), decompress = promisify(inflateRaw);
13
+ const MAGIC = Buffer.from('PRNG2\0');
14
+ const HEADER = MAGIC.length + 1 + 4 + 32;
15
+ const MAX_BLOB = 16 * 1024 * 1024, MAX_RECORD = 16 * 1024 * 1024;
16
+ const MAX_DISK = 64 * 1024 * 1024, MAX_SCAN = 8192, MAX_RECORDS = 1024;
17
+ const digest = /^[a-f0-9]{64}$/;
18
+ const namePattern = /^([a-f0-9]{64})\.(graph|blob)$/;
19
+
20
+ async function encode(bytes) {
21
+ let payload = bytes, codec = 0;
22
+ if (bytes.length >= 16384) {
23
+ const packed = await compress(bytes, { level: 1 });
24
+ if (packed.length < bytes.length * 0.9) { payload = packed; codec = 1; }
25
+ }
26
+ const header = Buffer.alloc(HEADER);
27
+ MAGIC.copy(header); header[MAGIC.length] = codec;
28
+ header.writeUInt32BE(bytes.length, MAGIC.length + 1);
29
+ Buffer.from(cacheHash(bytes), 'hex').copy(header, MAGIC.length + 5);
30
+ return [header, payload];
31
+ }
32
+
33
+ async function read(directory, name, limit) {
34
+ let file;
35
+ try {
36
+ const filename = path.join(directory, name), info = await lstat(filename);
37
+ if (!info.isFile() || info.isSymbolicLink() || info.size > limit + HEADER || info.size < HEADER) return null;
38
+ file = await open(filename, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
39
+ const stat = await file.stat();
40
+ if (!stat.isFile() || stat.size > limit + HEADER) return null;
41
+ const packed = await file.readFile();
42
+ if (!packed.subarray(0, MAGIC.length).equals(MAGIC)) return null;
43
+ const size = packed.readUInt32BE(MAGIC.length + 1), codec = packed[MAGIC.length];
44
+ if (size > limit || codec > 1) return null;
45
+ const payload = packed.subarray(HEADER);
46
+ const bytes = codec ? await decompress(payload, { maxOutputLength: Math.max(1, size) }) : payload;
47
+ if (bytes.length !== size || cacheHash(bytes) !== packed.subarray(MAGIC.length + 5, HEADER).toString('hex')) return null;
48
+ return bytes;
49
+ } catch { return null; } finally { await file?.close().catch(() => {}); }
50
+ }
51
+
52
+ async function write(directory, name, bytes) {
53
+ const filename = path.join(directory, name), temporary = filename + '.' + randomUUID() + '.tmp';
54
+ try {
55
+ const parts = await encode(bytes), file = await open(temporary, 'wx', 0o600);
56
+ try { for (const part of parts) await file.writeFile(part); } finally { await file.close(); }
57
+ await rename(temporary, filename);
58
+ } finally { await unlink(temporary).catch(() => {}); }
59
+ }
60
+
61
+ function references(record) {
62
+ const files = record?.value?.files;
63
+ if (record?.format !== 2 || !Array.isArray(files) || files.length > 4096) return null;
64
+ if (!files.every(file => Array.isArray(file) && file.length === 4 && typeof file[0] === 'string' && digest.test(file[1]) && typeof file[2] === 'string' && Number.isInteger(file[3]) && file[3] >= 0 && file[3] <= MAX_BLOB)) return null;
65
+ if (files.reduce((sum, file) => sum + file[3], 0) > 48 * 1024 * 1024) return null;
66
+ return files;
67
+ }
68
+
69
+ export async function readCompilationRecord(directory, key) {
70
+ if (!directory || !digest.test(key)) return null;
71
+ try {
72
+ const bytes = await read(directory, key + '.graph', MAX_RECORD);
73
+ if (!bytes) return null;
74
+ const record = deserialize(bytes), files = references(record);
75
+ if (!files || record.cacheKey !== key) return null;
76
+ const blobs = new Map(), restored = [];
77
+ for (const [name, id, hash, size] of files) {
78
+ let value = blobs.get(id);
79
+ if (!value) {
80
+ value = await read(directory, id + '.blob', MAX_BLOB);
81
+ if (!value || value.length !== size || cacheHash(value) !== id) return null;
82
+ blobs.set(id, value);
83
+ }
84
+ if (value.length !== size) return null;
85
+ restored.push([name, value, hash]);
86
+ }
87
+ return { ...record, format: 1, value: { ...record.value, files: restored } };
88
+ } catch { return null; }
89
+ }
90
+
91
+ // Concurrent eviction may turn a read into a miss, never into a partial hit.
92
+ // Retain the newest complete records and their shared blobs within one budget.
93
+ export async function pruneCompilationRecords(directory) {
94
+ try {
95
+ const entries = await readdir(directory, { withFileTypes: true });
96
+ const files = [];
97
+ for (const entry of entries.slice(0, MAX_SCAN)) {
98
+ if (!entry.isFile() || !namePattern.test(entry.name)) continue;
99
+ const info = await lstat(path.join(directory, entry.name));
100
+ if (info.isFile() && !info.isSymbolicLink()) files.push({ name: entry.name, size: info.size, time: info.mtimeMs });
101
+ }
102
+ const blobs = new Map(files.filter(file => file.name.endsWith('.blob')).map(file => [file.name.slice(0, -5), file]));
103
+ const retained = new Set(); let total = 0, count = 0;
104
+ for (const file of files.filter(file => file.name.endsWith('.graph')).sort((a, b) => b.time - a.time)) {
105
+ let refs;
106
+ try { const bytes = await read(directory, file.name, MAX_RECORD); refs = bytes && references(deserialize(bytes)); } catch {}
107
+ const ids = refs && [...new Set(refs.map(item => item[1]))];
108
+ const added = file.size + (ids || []).reduce((sum, id) => sum + (retained.has(id + '.blob') ? 0 : blobs.get(id)?.size ?? MAX_DISK), 0);
109
+ if (ids && ids.every(id => blobs.has(id)) && count < MAX_RECORDS && total + added <= MAX_DISK) {
110
+ retained.add(file.name); for (const id of ids) retained.add(id + '.blob');
111
+ total += added; count++;
112
+ }
113
+ }
114
+ for (const file of files) if (!retained.has(file.name)) await unlink(path.join(directory, file.name)).catch(() => {});
115
+ } catch { /* Disk cache maintenance cannot fail a build. */ }
116
+ }
117
+
118
+ export async function writeCompilationRecord(directory, key, record) {
119
+ if (!directory || !digest.test(key)) return false;
120
+ try {
121
+ const files = [], written = new Set();
122
+ for (const [name, data, hash] of record.value.files) {
123
+ if (!(data instanceof Uint8Array) || data.byteLength > MAX_BLOB) return false;
124
+ const bytes = Buffer.from(data.buffer, data.byteOffset, data.byteLength), id = cacheHash(bytes);
125
+ files.push([name, id, hash, bytes.length]);
126
+ if (!written.has(id)) {
127
+ // Verify existing bytes before reuse, including a corrupted blob from an
128
+ // interrupted previous build. Atomic replacement repairs that entry.
129
+ const existing = await read(directory, id + '.blob', MAX_BLOB);
130
+ if (!existing || cacheHash(existing) !== id) await write(directory, id + '.blob', bytes);
131
+ written.add(id);
132
+ }
133
+ }
134
+ const saved = { ...record, format: 2, cacheKey: key, value: { ...record.value, files } };
135
+ if (!references(saved)) return false;
136
+ const bytes = serialize(saved);
137
+ if (bytes.length > MAX_RECORD) return false;
138
+ await write(directory, key + '.graph', bytes);
139
+ await pruneCompilationRecords(directory);
140
+ return true;
141
+ } catch { return false; }
142
+ }
@@ -6,6 +6,7 @@ import { mkdir, writeFile, readFile } from 'node:fs/promises';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { createRequire } from 'node:module';
9
+ import {createHash} from 'node:crypto';
9
10
  import { moduleResolutionPlugin, validateWebpackResolution } from './module-resolution.mjs';
10
11
  import { cachedLoader } from './loader-cache.mjs';
11
12
  import { incrementalBuild } from './incremental.mjs';
@@ -13,11 +14,36 @@ import { webpackGraph } from './webpack-graph.mjs';
13
14
  import { turbopackRules, sourceReader } from './turbopack-conditions.mjs';
14
15
  import {withSourceMaps,configureSourceMaps,inlineSourceMap} from './source-maps.mjs';
15
16
  import { optimizeImportsPlugins } from './optimize-imports.mjs';
17
+ import {recordBuildAnalysis} from './analyze.mjs';
18
+ import {recordDevDependencies,developmentDependencies,developmentStylePlans,developmentStyleDependencies,developmentModules} from './dev-dependencies.mjs';
19
+ import {normalizeReactCompilerConfig,REACT_COMPILER_VERSION} from './react-compiler.mjs';
20
+ import {withReactCompilerReporting,configureReactCompilerReporting,withReactCompilerGraph,recordReactCompilerGraph} from './react-compiler-report.mjs';
21
+ import {withCompilationGraphCacheReporting,cachedGraphBuild} from './compilation-cache.mjs';
22
+ export {reactCompilerReport} from './react-compiler-report.mjs';
23
+ export {compilationGraphCacheReport} from './compilation-cache.mjs';
16
24
 
17
25
  let validateOptions;
18
26
  const compilation = new AsyncLocalStorage();
19
- export const withCompilation = callback => withSourceMaps(()=>compilation.run({ targets: new Map(), loaded: new Map() }, callback));
20
- export function configureCompiler(options) { Object.assign(compilation.getStore() || {}, options);configureSourceMaps(options.dev || options.config?.productionBrowserSourceMaps); }
27
+ export const withCompilation = callback => withCompilationGraphCacheReporting(()=>withReactCompilerReporting(()=>withSourceMaps(()=>compilation.run({ targets: new Map(), loaded: new Map() }, callback))));
28
+ export function configureCompiler(options) {
29
+ Object.assign(compilation.getStore() || {}, options);configureSourceMaps(options.dev || options.config?.productionBrowserSourceMaps);
30
+ configureReactCompilerReporting({...options,config:{reactCompiler:options.config?.reactCompiler ? normalizeReactCompilerConfig(options.config.reactCompiler) : false},compilerVersion:REACT_COMPILER_VERSION});
31
+ }
32
+ export const buildAnalysisRecords=()=>compilation.getStore()?.analysis;
33
+ export const buildAnalysisRoutes=()=>compilation.getStore()?.analysisRoutes;
34
+ export const devDependencies=(project,manifest)=>developmentDependencies(compilation.getStore(),project,manifest);
35
+ export const devStylePlans=()=>developmentStylePlans(compilation.getStore());
36
+ export const devStyleDependencies=()=>developmentStyleDependencies(compilation.getStore());
37
+ export const devModules=root=>developmentModules(compilation.getStore(),root);
38
+ export async function readCompilerFile(file,override){
39
+ if(override!==undefined)return override;
40
+ const state=compilation.getStore();if(!state)return readFile(file,'utf8');
41
+ state.sourceFiles||=new Map();
42
+ if(state.sourceFiles.has(file))return state.sourceFiles.get(file);
43
+ const pending=readFile(file,'utf8').then(source=>{state.devInputs?.record(file,source);state.sourceBytes=(state.sourceBytes||0)+source.length*2;if(state.sourceBytes>8*1024*1024)state.sourceFiles.delete(file);return source;});
44
+ if(state.sourceFiles.size<512)state.sourceFiles.set(file,pending);
45
+ return pending;
46
+ }
21
47
  export async function compilerSource(file, fallback) {
22
48
  return compilation.getStore()?.loaded.get(file)?.source ?? fallback ?? await readFile(file, 'utf8');
23
49
  }
@@ -101,7 +127,7 @@ async function targetConfig(state, target) {
101
127
  }
102
128
  const validateDefinitions = (values,prefix='') => {for(const [name,value] of Object.entries(values)) {
103
129
  const key=prefix+name;
104
- if (/^(?:process\.env\.(?:NODE_ENV|NEXT_RUNTIME|PRNEXT_)|__prnext)/.test(key.replace(/^typeof /,''))) throw new Error(`DefinePlugin cannot override reserved ${key}`);
130
+ if (/^(?:process\.env\.(?:NODE_ENV|NEXT_RUNTIME|PRNEXT_)|__prnext|__PRNEXT_CACHE_COMPONENTS__(?:$|\.))/.test(key.replace(/^typeof /,''))) throw new Error(`DefinePlugin cannot override reserved ${key}`);
105
131
  if(value && Object.getPrototypeOf(value)===Object.prototype)validateDefinitions(value,key+'.');
106
132
  }};
107
133
  validateDefinitions(plugin.definitions);
@@ -120,7 +146,27 @@ export async function validateCompilerConfig(config, root, {dev=false,edge=false
120
146
  for (const target of ['node','browser',...(edge?['edge']:[])]) await targetConfig(state,target);
121
147
  }
122
148
  export async function build(options) {
149
+ const state=compilation.getStore();
150
+ const target=options.define?.['process.env.NEXT_RUNTIME']==='"edge"'?'edge':options.platform==='browser'?'browser':'node';
151
+ return withReactCompilerGraph(target,async()=>{
152
+ const result=await compileGraph(options);
153
+ recordReactCompilerGraph(result);
154
+ recordBuildAnalysis(state,options,result);
155
+ recordDevDependencies(state,options,result);
156
+ return result;
157
+ });
158
+ }
159
+ async function compileGraph(options) {
123
160
  const state = compilation.getStore();
161
+ // The transform is captured by the framework's per-build callbacks. Give both
162
+ // esbuild and the webpack bridge a new graph identity when its options change,
163
+ // even if none of the application's source files have changed. The marker has
164
+ // no hooks or runtime output and leaves the opt-out graph exactly as before.
165
+ if ((options.plugins || []).some(plugin => framework.test(plugin.name)) && state?.config?.reactCompiler) {
166
+ const settings=normalizeReactCompilerConfig(state.config.reactCompiler);
167
+ const fingerprint=createHash('sha256').update(JSON.stringify([REACT_COMPILER_VERSION,settings])).digest('hex').slice(0,24);
168
+ options={...options,plugins:[...options.plugins,{name:'prnext-react-compiler-'+fingerprint,setup(){}}]};
169
+ }
124
170
  if ((options.plugins || []).some(plugin => framework.test(plugin.name))) options = { ...options, plugins: optimizeImportsPlugins(options.plugins, state?.config?.experimental?.optimizePackageImports) };
125
171
  // esbuild's shared service dispatches callbacks from its own async context.
126
172
  // Re-enter this build so transforms never inherit another project's map mode,
@@ -137,7 +183,8 @@ export async function build(options) {
137
183
  // Runtime vendor bundles are framework-owned, not project webpack targets.
138
184
  const projectGraph = (options.plugins || []).some(plugin => framework.test(plugin.name));
139
185
  if (projectGraph && state?.tsconfig) options = { ...options, tsconfig: state.tsconfig };
140
- const compile = input => state?.incremental && state.dev && (projectGraph || !input.plugins?.length) ? incrementalBuild(input,{root:state.root,stage:state.stage,group:projectGraph?'project':'auxiliary',run:callback=>compilation.run(state,callback)}) : esbuild(input);
186
+ const nativeCompile = input => state?.incremental && (projectGraph || !input.plugins?.length) ? incrementalBuild(input,{root:state.root,stage:state.stage,group:projectGraph?'project':'auxiliary',run:callback=>compilation.run(state,callback)}) : esbuild(input);
187
+ const compile = input => state?.root && state?.stage ? cachedGraphBuild(input,state,nativeCompile) : nativeCompile(input);
141
188
  if (!(state?.config?.webpack || Object.keys(state?.config?.turbopack?.rules || {}).length) || !projectGraph) return compile(options);
142
189
  const target = options.define?.['process.env.NEXT_RUNTIME'] === '"edge"' ? 'edge' : options.platform === 'browser' ? 'browser' : 'node';
143
190
  const config = await targetConfig(state,target);
@@ -0,0 +1,12 @@
1
+ import {availableParallelism} from 'node:os';
2
+
3
+ // A shared finite worker set, with deterministic results and full settlement
4
+ // before a caller discards its stage. No new work starts after a failure.
5
+ export async function mapConcurrent(items,run,limit=Math.min(4,availableParallelism())) {
6
+ if(!Number.isInteger(limit)||limit<1||limit>32)throw new RangeError('Invalid build concurrency');
7
+ const values=Array.from(items),results=new Array(values.length);let next=0,failure,failed=false;
8
+ await Promise.all(Array.from({length:Math.min(limit,values.length)},async()=>{
9
+ while(!failed&&next<values.length){const index=next++;try{results[index]=await run(values[index],index);}catch(error){if(!failed){failed=true;failure=error;}}}
10
+ }));
11
+ if(failed)throw failure;return results;
12
+ }
package/build/config.mjs CHANGED
@@ -16,16 +16,30 @@ import { validateTurbopack } from './module-resolution.mjs';
16
16
  import { validateI18n } from './i18n.mjs';
17
17
  import { frameworkPackages } from './framework-imports.mjs';
18
18
  import { validateTypeScript } from './typescript.mjs';
19
+ import {validateWorkerMemory} from './worker-memory.mjs';
19
20
  import { validateWorkerRetirement } from './worker-retirement.mjs';
21
+ import {validateWorkerPreload} from './worker-preload.mjs';
20
22
  import { validateOptimizePackageImports } from './optimize-imports.mjs';
23
+ import {validateDeploymentRetention} from './deployment-config.mjs';
24
+ import {validatePrerenderConfig} from './prerender-config.mjs';
25
+ import {validateHtmlOnly} from './html-only-config.mjs';
21
26
  import { validateProxyBodySize } from './proxy-body-config.mjs';
27
+ import { normalizeReactCompilerConfig } from './react-compiler.mjs';
22
28
 
23
29
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
30
  const configName = /^(?:next|prnext)\.config\.(?:js|mjs|cjs|ts)$/;
25
31
  const supported = new Set(['env', 'redirects', 'headers', 'rewrites', 'generateBuildId', 'compress', 'poweredByHeader', 'productionBrowserSourceMaps', 'basePath', 'assetPrefix', 'experimental', 'sassOptions', 'transpilePackages', 'serverExternalPackages', 'cacheComponents', 'cacheLife', 'images', 'reactStrictMode', 'pageExtensions', 'trailingSlash', 'skipTrailingSlashRedirect', 'skipMiddlewareUrlNormalize', 'skipProxyUrlNormalize', 'cacheHandlers', 'distDir']);
26
32
  const defaults = { distDir: '.prnext', env: {}, compress: true, poweredByHeader: true, productionBrowserSourceMaps: false, basePath: '', assetPrefix: '', trailingSlash: false, skipTrailingSlashRedirect: false, skipMiddlewareUrlNormalize: false, experimental: { nextScriptWorkers: false } };
27
33
  supported.add('cacheHandler');
34
+ supported.add('partialPrefetching');
35
+ supported.add('navigationCache');
36
+ supported.add('workerMemory');
28
37
  supported.add('workerRetirement');
38
+ supported.add('workerPreload');
39
+ supported.add('deploymentRetention');
40
+ supported.add('prerender');
41
+ supported.add('htmlOnly');
42
+ supported.add('cssEngine');
29
43
  supported.add('cacheMaxMemorySize');
30
44
  supported.add('turbopack');
31
45
  supported.add('i18n');
@@ -47,7 +61,7 @@ export function publicAssetBase(config) {
47
61
  return (config.assetPrefix || config.basePath || '').replace(/\/+$/, '') + '/_prnext/assets';
48
62
  }
49
63
 
50
- function configPlugin() {
64
+ function configPlugin(devInputs) {
51
65
  return {
52
66
  name: 'prnext-project-config',
53
67
  setup(build) {
@@ -56,20 +70,21 @@ function configPlugin() {
56
70
  if (filename.includes(`${path.sep}node_modules${path.sep}`)) return;
57
71
  // Preserve filesystem paths after bundling config's relative helpers.
58
72
  const prefix = `const __prnext_config_url=${JSON.stringify(pathToFileURL(filename).href)};const __prnext_config_file=${JSON.stringify(filename)};const __prnext_config_dir=${JSON.stringify(path.dirname(filename))};\n`;
59
- const source = (await readFile(filename, 'utf8')).replace(/^#![^\n]*(?:\n|$)/, '');
73
+ const raw = await readFile(filename, 'utf8');devInputs?.record(filename,raw);
74
+ const source = raw.replace(/^#![^\n]*(?:\n|$)/, '');
60
75
  return { contents: prefix + source, loader: filename.endsWith('.ts') ? 'ts' : 'js', resolveDir: path.dirname(filename) };
61
76
  });
62
77
  },
63
78
  };
64
79
  }
65
80
 
66
- async function importConfig(filename) {
81
+ async function importConfig(filename,devInputs) {
67
82
  const temporary = path.join(path.dirname(filename), `.prnext-config-${randomUUID()}.mjs`);
68
83
  try {
69
84
  const result = await bundle({ entryPoints: [filename], absWorkingDir: path.dirname(filename), bundle: true, write: false, platform: 'node', target: 'node22', format: 'esm', packages: 'external', logLevel: 'silent',
70
85
  define: { 'import.meta.url': '__prnext_config_url', __filename: '__prnext_config_file', __dirname: '__prnext_config_dir' },
71
86
  banner: { js: `import {createRequire as __prnext_create_require} from 'node:module';const require=__prnext_create_require(${JSON.stringify(pathToFileURL(filename).href)});` },
72
- plugins: [configPlugin()],
87
+ plugins: [configPlugin(devInputs)],
73
88
  });
74
89
  await writeFile(temporary, result.outputFiles[0].contents, { flag: 'wx' });
75
90
  const imported = await import(pathToFileURL(temporary).href);
@@ -85,14 +100,22 @@ export function validateProjectConfig(input, filename = 'project config') {
85
100
  // Optional properties explicitly set to undefined still use their defaults,
86
101
  // as in common process.env-based Next configuration wrappers.
87
102
  const config = { ...defaults, ...Object.fromEntries(Object.entries(input).filter(([,value]) => value !== undefined)) };
88
- for (const key of ['reactCompiler', 'typedRoutes']) {
89
- if (config[key] === undefined || config[key] === false) continue;
90
- if (key === 'reactCompiler') throw new Error('reactCompiler currently supports only false. PRNext does not run the React Compiler transform; keep Next.js for builds that require it.');
91
- throw new Error('typedRoutes currently supports only false. PRNext does not generate or validate typed route declarations; keep Next.js for builds that require them.');
92
- }
103
+ if(config.typedRoutes!==undefined&&typeof config.typedRoutes!=='boolean')throw new TypeError('typedRoutes must be a boolean');
104
+ config.reactCompiler = normalizeReactCompilerConfig(config.reactCompiler);
93
105
  config.typescript = validateTypeScript(config.typescript);
106
+ config.workerMemory = validateWorkerMemory(config.workerMemory);
94
107
  config.workerRetirement = validateWorkerRetirement(config.workerRetirement);
95
- if (config.devIndicators !== undefined && config.devIndicators !== false) throw new TypeError('devIndicators currently supports only false. PRNext provides development errors and Fast Refresh, but no Next.js development indicator menu.');
108
+ config.workerPreload=validateWorkerPreload(config.workerPreload);
109
+ config.deploymentRetention=validateDeploymentRetention(config.deploymentRetention);
110
+ config.prerender=validatePrerenderConfig(config.prerender);
111
+ config.htmlOnly=validateHtmlOnly(config.htmlOnly);
112
+ config.cssEngine ??= 'auto';
113
+ if (!['auto','postcss','lightningcss'].includes(config.cssEngine)) throw new TypeError('cssEngine must be auto, postcss or lightningcss.');
114
+ if (config.devIndicators !== false) {
115
+ const indicator = config.devIndicators ?? {};
116
+ if (config.devIndicators === null || typeof indicator !== 'object' || Array.isArray(indicator) || Object.keys(indicator).some(key => key !== 'position') || (indicator.position !== undefined && !['bottom-right', 'bottom-left', 'top-right', 'top-left'].includes(indicator.position))) throw new TypeError('devIndicators must be false or an object with position: bottom-right, bottom-left, top-right or top-left.');
117
+ config.devIndicators = { position: indicator.position ?? 'bottom-left' };
118
+ }
96
119
  if (config.onDemandEntries !== undefined) {
97
120
  const entries = config.onDemandEntries;
98
121
  if (!entries || typeof entries !== 'object' || Array.isArray(entries)) throw new TypeError('onDemandEntries must be an object');
@@ -114,6 +137,20 @@ export function validateProjectConfig(input, filename = 'project config') {
114
137
  if (config.reactStrictMode !== undefined && typeof config.reactStrictMode !== 'boolean') throw new Error(`${filename}: reactStrictMode must be a boolean.`);
115
138
  config.cacheComponents ??= false;
116
139
  if (typeof config.cacheComponents !== 'boolean') throw new TypeError('cacheComponents must be a boolean');
140
+ config.partialPrefetching ??= false;
141
+ if (typeof config.partialPrefetching !== 'boolean') throw new TypeError('partialPrefetching must be a boolean');
142
+ if (config.partialPrefetching && !config.cacheComponents) throw new Error('partialPrefetching requires cacheComponents: true.');
143
+ if (config.navigationCache === false) config.navigationCache = { maxEntries: 0, ttlMs: 120000 };
144
+ else {
145
+ if (config.navigationCache === null) throw new TypeError('navigationCache must be false or an object with maxEntries and ttlMs.');
146
+ const value = config.navigationCache ?? {};
147
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !['maxEntries', 'ttlMs', 'authCookies'].includes(key))) throw new TypeError('navigationCache must be false or an object with maxEntries, ttlMs and authCookies.');
148
+ const maxEntries = value.maxEntries ?? 4, ttlMs = value.ttlMs ?? 120000;
149
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 0 || maxEntries > 8 || !Number.isSafeInteger(ttlMs) || ttlMs < 1000 || ttlMs > 600000) throw new TypeError('navigationCache requires maxEntries between 0 and 8, and ttlMs between 1000 and 600000.');
150
+ const authCookies = value.authCookies;
151
+ if (authCookies !== undefined && (!Array.isArray(authCookies) || authCookies.length > 32 || authCookies.some(name => typeof name !== 'string' || !name || name.length > 256 || !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name)))) throw new TypeError('navigationCache.authCookies must contain at most 32 valid cookie names.');
152
+ config.navigationCache = { maxEntries, ttlMs, ...(authCookies !== undefined ? {authCookies: [...new Set(authCookies)].sort()} : {}) };
153
+ }
117
154
  config.cacheLife = cacheProfiles.profiles(config.cacheLife);
118
155
  for (const key of ['transpilePackages', 'serverExternalPackages']) {
119
156
  const names = config[key] ?? [];
@@ -182,9 +219,9 @@ export function validateProjectConfig(input, filename = 'project config') {
182
219
  }
183
220
 
184
221
  /** Config and its relative imports are evaluated fresh on every build. */
185
- export async function loadProjectConfig(projectRoot, { dev = false, envMode, phase = dev ? constants.PHASE_DEVELOPMENT_SERVER : constants.PHASE_PRODUCTION_BUILD } = {}) {
222
+ export async function loadProjectConfig(projectRoot, { dev = false, envMode, devInputs, phase = dev ? constants.PHASE_DEVELOPMENT_SERVER : constants.PHASE_PRODUCTION_BUILD } = {}) {
186
223
  const root = path.resolve(projectRoot);
187
- loadEnvConfig(root, { dev, mode: envMode });
224
+ loadEnvConfig(root, { dev, mode: envMode, inputs:devInputs });
188
225
  const files = await readdir(root);
189
226
  const names = files.filter(name => configName.test(name));
190
227
  const unsupportedFiles = files.filter(name => /^(?:next|prnext)\.config\.(?:mts|cts|jsx|tsx|json)$/.test(name));
@@ -192,7 +229,7 @@ export async function loadProjectConfig(projectRoot, { dev = false, envMode, pha
192
229
  if (names.length > 1) throw new Error(`Multiple configuration files found: ${names.sort().join(', ')}. Keep one next.config.* or prnext.config.* file.`);
193
230
  if (!names.length) return validateProjectConfig({});
194
231
  const filename = path.join(root, names[0]);
195
- let input = await importConfig(filename);
232
+ let input = await importConfig(filename,devInputs);
196
233
  if (typeof input === 'function') input = await input(phase, { defaultConfig: { ...defaults, env: {}, experimental: { ...defaults.experimental } } });
197
234
  return validateProjectConfig(input, names[0]);
198
235
  }
@@ -0,0 +1,239 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, mkdir, open, readFile, readdir, realpath, unlink } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { packageManifest } from '../native/resolve.mjs';
6
+ import { supportedNode } from '../native/diagnostics.mjs';
7
+ import { npmCommand } from '../native/npm.mjs';
8
+
9
+ export const createHelp = `Usage: prnext create <directory> [--js] [--no-install] [--json]
10
+
11
+ Create a minimal App Router project and install its dependencies with npm.
12
+ TypeScript is the default; --js creates a JavaScript project.
13
+ The directory must be absent or completely empty. Existing files are never replaced.
14
+ --no-install prepares files without running npm or accessing the network.
15
+ Inside an existing workspace, use --no-install and its own package manager.
16
+
17
+ npx @thomas.f/prnext create my-app
18
+ cd my-app
19
+ npm run dev
20
+ `;
21
+
22
+ export function parseCreateArgs(args, cwd = process.cwd()) {
23
+ const flags = new Set(), positional = [];
24
+ let literal = false;
25
+ for (const arg of args) {
26
+ if (literal) positional.push(arg);
27
+ else if (arg === '--') literal = true;
28
+ else if (['--no-install', '--js', '--json'].includes(arg)) {
29
+ if (flags.has(arg)) throw new Error(`Duplicate create option: ${arg}`);
30
+ flags.add(arg);
31
+ } else if (arg.startsWith('-')) throw new Error(`Unknown create option: ${arg}`);
32
+ else positional.push(arg);
33
+ }
34
+ if (positional.length !== 1 || !positional[0].trim() || /[\0\r\n]/.test(positional[0])) throw new Error('Provide one project directory: prnext create my-app');
35
+ return { directory: path.resolve(cwd, positional[0]), options: { install: !flags.has('--no-install'), typescript: !flags.has('--js'), json: flags.has('--json') } };
36
+ }
37
+
38
+ const samePath = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
39
+ const sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
40
+ async function info(file, options) {
41
+ try { return await lstat(file, options); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
42
+ }
43
+ async function physicalDirectory(directory, missing = false) {
44
+ // macOS exposes standard directories through root-owned /private aliases.
45
+ // Resolve only those exact system targets; every user-controlled descendant
46
+ // still goes through the strict no-symlink walk below, on every write.
47
+ if (process.platform === 'darwin') {
48
+ const alias = /^\/(?:var|tmp|etc)(?=\/|$)/.exec(directory)?.[0];
49
+ if (alias && (await info(alias))?.isSymbolicLink() && await realpath(alias) === `/private${alias}`) {
50
+ directory = `/private${directory}`;
51
+ }
52
+ }
53
+ const root = path.parse(directory).root;
54
+ const inspected = [];
55
+ let current = root;
56
+ for (const part of path.relative(root, directory).split(path.sep).filter(Boolean)) {
57
+ if (process.platform === 'win32' && (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part) || /[<>:"|?*]|[. ]$/.test(part))) throw new Error(`Invalid Windows directory name: ${part}`);
58
+ current = path.join(current, part);
59
+ const entry = await info(current, { bigint: true });
60
+ if (!entry && missing) continue;
61
+ if (!entry?.isDirectory() || entry.isSymbolicLink()) throw new Error(`Project directories must not be files, symbolic links or junctions: ${current}`);
62
+ inspected.push([current, entry]);
63
+ }
64
+ if (!missing) {
65
+ const canonical = await realpath(directory);
66
+ if (!samePath(canonical, directory)) {
67
+ if (process.platform !== 'win32') throw new Error('Use a physical project directory without symbolic links or junctions.');
68
+ // DOS 8.3 names are directory aliases, not reparse points. Accept the
69
+ // expanded name only after rechecking every observed directory identity
70
+ // and proving that the canonical target is the same physical directory.
71
+ for (const [file, original] of inspected) {
72
+ const observed = await info(file, { bigint: true });
73
+ if (!observed?.isDirectory() || observed.isSymbolicLink() || !sameFile(original, observed)) throw new Error(`Project directory changed during creation: ${file}`);
74
+ }
75
+ const original = inspected.at(-1)?.[1] || await info(directory, { bigint: true });
76
+ const resolved = await info(canonical, { bigint: true });
77
+ if (!original?.isDirectory() || original.isSymbolicLink() || original.ino === 0n || !resolved?.isDirectory() || resolved.isSymbolicLink() || !sameFile(original, resolved)) throw new Error('Use a physical project directory without symbolic links or junctions.');
78
+ }
79
+ }
80
+ }
81
+
82
+ async function parentWorkspace(directory) {
83
+ for (let current = path.dirname(directory); ; current = path.dirname(current)) {
84
+ const workspace = await info(path.join(current, 'pnpm-workspace.yaml'));
85
+ if (workspace) return current;
86
+ const file = path.join(current, 'package.json'), entry = await info(file);
87
+ if (entry) {
88
+ if (!entry.isFile() || entry.isSymbolicLink() || entry.size > 1024 * 1024) throw new Error(`Cannot safely inspect parent package.json: ${file}`);
89
+ let manifest;
90
+ try { manifest = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, '')); }
91
+ catch { throw new Error(`Parent package.json is invalid: ${file}`); }
92
+ if (manifest.workspaces) return current;
93
+ }
94
+ if (current === path.dirname(current)) return null;
95
+ }
96
+ }
97
+
98
+ function projectName(directory) {
99
+ const name = path.basename(directory).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+|[._-]+$/g, '').slice(0, 100);
100
+ return !name || ['node_modules', 'favicon.ico'].includes(name) ? 'prnext-app' : name;
101
+ }
102
+ const json = value => JSON.stringify(value, null, 2) + '\n';
103
+
104
+ function projectFiles(name, typescript) {
105
+ const extension = typescript ? 'tsx' : 'jsx';
106
+ const scripts = { dev: 'prnext dev', build: 'prnext build', start: 'prnext start', check: 'prnext check', ...(typescript ? { typecheck: 'prnext typecheck' } : {}) };
107
+ const manifest = { name, version: '0.0.0', private: true, type: 'module', scripts,
108
+ dependencies: { [packageManifest.name]: packageManifest.version, ...Object.fromEntries(Object.entries(packageManifest.peerDependencies).filter(([name])=>!packageManifest.peerDependenciesMeta?.[name]?.optional)) },
109
+ ...(typescript ? { devDependencies: { typescript: '^5.9.0', '@types/node': '^22.0.0', '@types/react': '^19.0.0', '@types/react-dom': '^19.0.0' } } : {}),
110
+ engines: { node: packageManifest.engines.node } };
111
+ const files = new Map([
112
+ ['package.json', json(manifest)],
113
+ [`app/layout.${extension}`, `import './globals.css';\n${typescript ? "import type { ReactNode } from 'react';\n" : ''}
114
+ export const metadata = { title: 'My PRNext app', description: 'A React application built with PRNext.' };
115
+
116
+ export default function RootLayout({ children }${typescript ? ': { children: ReactNode }' : ''}) {
117
+ return <html lang="en"><body>{children}</body></html>;
118
+ }
119
+ `],
120
+ [`app/page.${extension}`, `export default function Home() {
121
+ return <main><h1>PRNext</h1><p>Edit <code>app/page.${extension}</code> to get started.</p></main>;
122
+ }
123
+ `],
124
+ ['app/globals.css', `:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #0a0a0a; color: #ededed; }
125
+ * { box-sizing: border-box; }
126
+ body { margin: 0; }
127
+ main { max-width: 64rem; margin: auto; padding: clamp(2rem, 8vw, 6rem) 1.5rem; }
128
+ h1 { font-size: clamp(2.5rem, 8vw, 4rem); letter-spacing: -.04em; }
129
+ p { line-height: 1.6; }
130
+ code { font-size: .95em; }
131
+ `],
132
+ ['.gitignore', 'node_modules/\n.prnext/\n.prnext-types/\n.prnext-dev/\n.prnext-cache/\n.env*\n!.env.example\n*.tsbuildinfo\n'],
133
+ ['README.md', `# ${name}\n\nA minimal PRNext application.\n\nRun \`npm install\` if dependencies have not been installed, then:\n\n\`\`\`sh\nnpm run dev\n\`\`\`\n\nEdit \`app/page.${extension}\`. Use the local URL printed by the server.\n\n\`\`\`sh\nnpm run check\n${typescript ? 'npm run typecheck\n' : ''}npm run build\nnpm start\n\`\`\`\n\nUse another port with \`npm run dev -- --port 3001\` when needed.\n\n[PRNext documentation](https://prnext.dev) · Installed AI guide: \`npx --no-install prnext docs --ai\`.\nPRNext is experimental; Next.js compatibility is partial.\n`],
134
+ ['AGENTS.md', `# PRNext project\n\nThis application uses \`@thomas.f/prnext\`. Before changing routing, configuration, data fetching or deployment, read the installed guide at \`node_modules/@thomas.f/prnext/AI.md\` or run:\n\n\`\`\`sh\nnpx --no-install prnext docs --ai\nnpx --no-install prnext docs --ai --list\n\`\`\`\n\nRead only the relevant topic with \`npx --no-install prnext docs --ai <topic>\`. The installed documentation is version-specific; do not assume full Next.js compatibility. Preserve the project's package manager and lockfile.\n`],
135
+ ]);
136
+ if (typescript) files.set('tsconfig.json', json({ compilerOptions: { target: 'ES2022', lib: ['DOM', 'DOM.Iterable', 'ES2022'], module: 'ESNext', moduleResolution: 'Bundler', jsx: 'preserve', strict: true, noEmit: true, esModuleInterop: true, skipLibCheck: true }, include: ['**/*.ts', '**/*.tsx'], exclude: ['node_modules', '.prnext', '.prnext-dev', '.prnext-cache'] }));
137
+ return files;
138
+ }
139
+
140
+ function abortError() { const error = new Error('Project creation canceled.'); error.name = 'AbortError'; return error; }
141
+ function aborted(signal) { if (signal?.aborted) throw abortError(); }
142
+
143
+ /** Run npm without a shell; a canceled command waits for its own installer tree. */
144
+ export async function installProject(directory, { signal, onOutput = () => {} } = {}) {
145
+ aborted(signal);
146
+ // npm resolves its working directory physically. Keep its environment on
147
+ // that same path so macOS aliases cannot split the lock tree.
148
+ directory = await realpath(directory);
149
+ aborted(signal);
150
+ const env = { ...process.env, INIT_CWD: directory, PWD: directory };
151
+ for (const key of Object.keys(env)) if (/^npm_config_(?:workspace|workspaces|global|prefix)$/i.test(key)) delete env[key];
152
+ const npm = npmCommand(env);
153
+ await new Promise((resolve, reject) => {
154
+ // The generated package.json already anchors npm's local project here.
155
+ // npm 10 on Windows treats an explicit --prefix as its global top and can
156
+ // add the project to its own dependencies even with --global=false.
157
+ const child = spawn(npm.command, [...npm.args, 'install', '--workspaces=false', '--global=false'], { cwd: directory, env, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] });
158
+ let tail = '', timer, canceling = false;
159
+ const receive = stream => chunk => { tail = (tail + chunk).slice(-16000); onOutput(String(chunk), stream); };
160
+ child.stdout.on('data', receive('stdout'));
161
+ child.stderr.on('data', receive('stderr'));
162
+ function terminate(force) {
163
+ if (!child.pid) return;
164
+ if (process.platform === 'win32') {
165
+ if (child.exitCode !== null || child.signalCode) return;
166
+ const executable = path.join(env.SystemRoot || 'C:\\Windows', 'System32', 'taskkill.exe');
167
+ const killer = spawn(executable, ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
168
+ killer.on('error', () => child.kill());
169
+ } else {
170
+ try { process.kill(-child.pid, force ? 'SIGKILL' : 'SIGTERM'); } catch (error) { if (error.code !== 'ESRCH') child.kill(force ? 'SIGKILL' : 'SIGTERM'); }
171
+ }
172
+ }
173
+ function cancel() { canceling = true; terminate(false); timer ||= setTimeout(() => terminate(true), 5000); }
174
+ function cleanup() { clearTimeout(timer); signal?.removeEventListener('abort', cancel); }
175
+ child.once('error', error => { cleanup(); reject(error); });
176
+ child.once('close', (code, childSignal) => {
177
+ // A detached lifecycle script can close npm's pipes before it exits.
178
+ // Do not let npm's exit cancel escalation for that remaining group.
179
+ if ((canceling || signal?.aborted) && process.platform !== 'win32') terminate(true);
180
+ cleanup();
181
+ if (canceling || signal?.aborted) reject(abortError());
182
+ else if (code === 0) resolve();
183
+ else reject(new Error(`npm install failed (${childSignal || code}).${tail.trim() ? '\n' + tail.trim() : ''}`));
184
+ });
185
+ signal?.addEventListener('abort', cancel, { once: true });
186
+ if (signal?.aborted) cancel();
187
+ });
188
+ }
189
+
190
+ export async function createProject(directory, options = {}, services = {}) {
191
+ const root = path.resolve(directory), typescript = options.typescript !== false, install = options.install !== false;
192
+ aborted(options.signal);
193
+ const nodeVersion = services.nodeVersion || process.version;
194
+ if (!supportedNode(nodeVersion)) throw new Error(`Node ${nodeVersion} is unsupported. Use Node ${packageManifest.engines.node}.`);
195
+ await physicalDirectory(root, true);
196
+ const existing = await info(root);
197
+ if (existing && (await readdir(root)).length) throw new Error(`Project directory must be empty; no files were changed: ${root}`);
198
+ if (install && await parentWorkspace(root)) throw new Error('Automatic installation inside an existing workspace is not supported. Use --no-install, then add the project with the workspace package manager. No files were changed.');
199
+ aborted(options.signal);
200
+ await mkdir(root, { recursive: true });
201
+ await physicalDirectory(root);
202
+ const lockPath = path.join(root, '.prnext-create.lock');
203
+ const lock = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW || 0), 0o600);
204
+ const locked = await lock.stat();
205
+ try {
206
+ if ((await readdir(root)).some(name => name !== '.prnext-create.lock')) throw new Error(`Project directory is no longer empty; existing files were preserved: ${root}`);
207
+ const name = projectName(root), files = projectFiles(name, typescript);
208
+ for (const [relative, content] of files) {
209
+ aborted(options.signal);
210
+ await physicalDirectory(root);
211
+ const target = path.join(root, relative), parent = path.dirname(target);
212
+ await physicalDirectory(parent, true);
213
+ await mkdir(parent, { recursive: true });
214
+ await physicalDirectory(parent);
215
+ const file = await open(target, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW || 0), 0o644);
216
+ try { await file.writeFile(content, 'utf8'); } finally { await file.close(); }
217
+ }
218
+ const report = { ok: true, status: 'prepared', root, name, typescript, files: [...files.keys()], installed: false, next: install ? ['npm run dev'] : ['npm install', 'npm run dev'] };
219
+ if (install) {
220
+ try {
221
+ aborted(options.signal);
222
+ await (services.runInstall || installProject)(root, { signal: options.signal, onOutput: options.onOutput });
223
+ aborted(options.signal);
224
+ report.installed = true;
225
+ report.status = 'created';
226
+ } catch (error) {
227
+ report.ok = false;
228
+ report.status = error.name === 'AbortError' || options.signal?.aborted ? 'cancelled' : 'install-failed';
229
+ report.error = error.message;
230
+ report.next = ['npm install', 'npm run dev'];
231
+ }
232
+ }
233
+ return report;
234
+ } finally {
235
+ await lock.close();
236
+ const current = await info(lockPath);
237
+ if (current?.isFile() && !current.isSymbolicLink() && sameFile(current, locked)) await unlink(lockPath);
238
+ }
239
+ }