@thomas.f/prnext 0.1.16-alpha → 0.1.17-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 (126) hide show
  1. package/AI.md +63 -0
  2. package/README.md +384 -9
  3. package/build/ADAPTERS.md +27 -2
  4. package/build/actions.mjs +5 -0
  5. package/build/analyze.mjs +184 -0
  6. package/build/app-config.mjs +7 -8
  7. package/build/app-static.mjs +12 -7
  8. package/build/app.mjs +23 -10
  9. package/build/build-lock.mjs +6 -6
  10. package/build/bundle-runtime.mjs +20 -5
  11. package/build/check.mjs +56 -11
  12. package/build/compiler.mjs +25 -1
  13. package/build/concurrent.mjs +12 -0
  14. package/build/config.mjs +21 -13
  15. package/build/css-modules.mjs +15 -4
  16. package/build/dev-compiler-health.mjs +11 -0
  17. package/build/dev-css-update.mjs +83 -0
  18. package/build/dev-dependencies.mjs +44 -0
  19. package/build/dev-discovery.mjs +28 -0
  20. package/build/dev-worker.mjs +14 -5
  21. package/build/dev.mjs +55 -4
  22. package/build/edge.mjs +3 -2
  23. package/build/framework-imports.mjs +40 -0
  24. package/build/incremental.mjs +8 -4
  25. package/build/index.mjs +96 -101
  26. package/build/inspect.mjs +3 -0
  27. package/build/inspection-summary.mjs +37 -4
  28. package/build/loader-cache.mjs +53 -26
  29. package/build/metadata-routes.mjs +2 -3
  30. package/build/migrate.mjs +60 -57
  31. package/build/npm-packages.mjs +3 -18
  32. package/build/package-manager.mjs +308 -0
  33. package/build/persistent-cache.mjs +97 -0
  34. package/build/precompress.mjs +62 -30
  35. package/build/prerender-pages.mjs +90 -0
  36. package/build/prerender-worker.mjs +10 -0
  37. package/build/production-builder.mjs +80 -0
  38. package/build/scan.mjs +4 -4
  39. package/build/source-analysis.mjs +42 -0
  40. package/build/static-adapter.mjs +13 -5
  41. package/build/static-input.mjs +48 -0
  42. package/build/styles.mjs +15 -3
  43. package/build/transform-cache.mjs +37 -4
  44. package/build/type-command.mjs +41 -0
  45. package/build/typed-routes.mjs +91 -0
  46. package/build/typescript-dev-worker.mjs +18 -0
  47. package/build/typescript-dev.mjs +22 -0
  48. package/build/typescript-watch.mjs +22 -0
  49. package/build/typescript.mjs +5 -4
  50. package/build/webpack-graph.mjs +164 -27
  51. package/build/worker-preload.mjs +9 -0
  52. package/cli.mjs +105 -121
  53. package/compat/index.d.ts +21 -7
  54. package/compat/link.d.cts +4 -3
  55. package/compat/navigation.d.cts +4 -3
  56. package/compat/router.d.cts +4 -3
  57. package/compat/typed-route.d.cts +17 -0
  58. package/docs/ai/compatibility.md +106 -0
  59. package/docs/ai/data.md +128 -0
  60. package/docs/ai/deployment.md +111 -0
  61. package/docs/ai/manifest.json +11 -0
  62. package/docs/ai/quickstart.md +146 -0
  63. package/docs/ai/routing.md +132 -0
  64. package/docs/ai/troubleshooting.md +120 -0
  65. package/index.mjs +2 -0
  66. package/native/diagnostics.mjs +34 -0
  67. package/native/resolve.mjs +7 -2
  68. package/package.json +25 -7
  69. package/release/ai-docs.mjs +48 -0
  70. package/release/verify.mjs +2 -0
  71. package/runtime/ai-docs.mjs +202 -0
  72. package/runtime/app-client.mjs +17 -6
  73. package/runtime/app-render.mjs +12 -0
  74. package/runtime/cloud/archive.mjs +5 -6
  75. package/runtime/compile-cache.mjs +8 -0
  76. package/runtime/dev-ai-brands.mjs +26 -0
  77. package/runtime/dev-ai-style.mjs +99 -0
  78. package/runtime/dev-build-reasons.mjs +25 -0
  79. package/runtime/dev-cache-control.mjs +53 -0
  80. package/runtime/dev-client.mjs +239 -45
  81. package/runtime/dev-console-copy.mjs +19 -0
  82. package/runtime/dev-console.mjs +106 -0
  83. package/runtime/dev-dependency-graph.mjs +40 -0
  84. package/runtime/dev-element-picker-style.mjs +27 -0
  85. package/runtime/dev-element-picker.mjs +213 -0
  86. package/runtime/dev-error-overlay-style.mjs +41 -0
  87. package/runtime/dev-error-overlay.mjs +167 -0
  88. package/runtime/dev-error-report.mjs +121 -0
  89. package/runtime/dev-file-changes.mjs +1 -1
  90. package/runtime/dev-gateway.mjs +44 -0
  91. package/runtime/dev-host-relay.mjs +66 -0
  92. package/runtime/dev-indicator.mjs +186 -0
  93. package/runtime/dev-issue-motion.mjs +214 -0
  94. package/runtime/dev-lazy-tool.mjs +18 -0
  95. package/runtime/dev-logo.mjs +2 -0
  96. package/runtime/dev-notification.mjs +81 -0
  97. package/runtime/dev-popout.mjs +34 -0
  98. package/runtime/dev-position.mjs +107 -0
  99. package/runtime/dev-preview-touch.mjs +122 -0
  100. package/runtime/dev-runtime-pool.mjs +45 -0
  101. package/runtime/dev-runtime-spare.mjs +57 -0
  102. package/runtime/dev-tools-client.mjs +155 -0
  103. package/runtime/dev-tools-mcp.mjs +126 -0
  104. package/runtime/dev-tools-metrics.mjs +31 -0
  105. package/runtime/dev-tools-model.mjs +42 -0
  106. package/runtime/dev-tools-panel.mjs +341 -0
  107. package/runtime/dev-tools-result.mjs +37 -0
  108. package/runtime/dev-tools-server.mjs +256 -0
  109. package/runtime/dev-tools-source.mjs +104 -0
  110. package/runtime/dev-tooltip.mjs +94 -0
  111. package/runtime/dev-ui-icons.mjs +8 -0
  112. package/runtime/dev-workbench-style.mjs +106 -0
  113. package/runtime/dev-workbench.mjs +428 -0
  114. package/runtime/dev-worker-logs.mjs +31 -0
  115. package/runtime/development.mjs +307 -0
  116. package/runtime/env.mjs +2 -1
  117. package/runtime/pages-client.mjs +10 -1
  118. package/runtime/pages-navigation.mjs +6 -1
  119. package/runtime/persistent/client.mjs +6 -5
  120. package/runtime/persistent/daemon.mjs +66 -56
  121. package/runtime/persistent/native-proxy.mjs +63 -0
  122. package/runtime/preload.mjs +12 -0
  123. package/runtime/rsc-worker.mjs +7 -0
  124. package/runtime/typed-route.mjs +19 -0
  125. package/runtime/watch-project.mjs +12 -5
  126. package/runtime/worker.mjs +36 -2
@@ -0,0 +1,18 @@
1
+ import {createRequire} from 'node:module';
2
+ import path from 'node:path';
3
+ import {createTypeScriptWatchSystem} from './typescript-watch.mjs';
4
+ const [compiler,filename,cache]=process.argv.slice(2),ts=createRequire(import.meta.url)(compiler);
5
+ const format={getCanonicalFileName:name=>name,getCurrentDirectory:()=>path.dirname(filename),getNewLine:()=>ts.sys.newLine};
6
+ const send=diagnostics=>{
7
+ let bytes=0;const errors=[];
8
+ for(const diagnostic of diagnostics){if(diagnostic.category!==ts.DiagnosticCategory.Error)continue;const text=ts.formatDiagnostics([diagnostic],format);if(bytes+text.length>20000||errors.length>=40)break;bytes+=text.length;errors.push(text);}
9
+ if(process.connected)process.send({type:'diagnostics',errors});
10
+ };
11
+ const host=ts.createWatchCompilerHost(filename,{noEmit:true,incremental:true,...(cache?{tsBuildInfoFile:cache}:{})},createTypeScriptWatchSystem(ts,process.cwd()),ts.createSemanticDiagnosticsBuilderProgram,diagnostic=>send([diagnostic]),()=>{});
12
+ host.writeFile=(filename,contents)=>{if(cache&&path.resolve(filename)===path.resolve(cache))ts.sys.writeFile(filename,contents);};
13
+ host.afterProgramCreate=builder=>{
14
+ try{send(ts.getPreEmitDiagnostics(builder.getProgram()));builder.emit(undefined,host.writeFile);}catch(error){send([{category:ts.DiagnosticCategory.Error,code:0,messageText:error.message}]);}
15
+ };
16
+ const watcher=ts.createWatchProgram(host);
17
+ const close=()=>{watcher.close();process.exit(0);};
18
+ process.once('disconnect',close);process.once('SIGTERM',close);process.once('SIGINT',close);
@@ -0,0 +1,22 @@
1
+ import {fork} from 'node:child_process';
2
+ import {access} from 'node:fs/promises';
3
+ import {createRequire} from 'node:module';
4
+ import path from 'node:path';
5
+ import {privateCacheDirectory} from './persistent-cache.mjs';
6
+ export async function startTypeChecker(root,config,notify){
7
+ const noop={inactive:true,async close(){}};
8
+ if(config.typescript?.devChecks===false)return noop;
9
+ const filename=path.resolve(root,config.typescript?.tsconfigPath||'tsconfig.json');try{await access(filename);}catch{return noop;}
10
+ let compiler;try{compiler=createRequire(path.join(root,'package.json')).resolve('typescript');}catch{console.warn('TypeScript diagnostics require typescript installed in the application.');return noop;}
11
+ const directory=await privateCacheDirectory(root,'typescript-dev');
12
+ let child,closed=false,failures=0,timer;
13
+ function launch(){
14
+ if(closed)return;
15
+ child=fork(new URL('./typescript-dev-worker.mjs',import.meta.url),[compiler,filename,directory?path.join(directory,'project.tsbuildinfo'):''],{cwd:root,execArgv:['--max-old-space-size=512'],stdio:['ignore','ignore','pipe','ipc'],windowsHide:true});
16
+ let details='',healthyAt=0;child.stderr.on('data',chunk=>{details=(details+chunk).slice(-2048);});
17
+ child.on('message',message=>{if(message?.type==='diagnostics'&&Array.isArray(message.errors)){healthyAt ||= Date.now();notify(message.errors.filter(value=>typeof value==='string').slice(0,40));}});
18
+ child.on('error',error=>{notify(['TypeScript checker: '+error.message]);});
19
+ child.once('exit',()=>{if(closed)return;if(healthyAt&&Date.now()-healthyAt>=60000)failures=0;if(++failures>=3){notify(['TypeScript checker stopped after repeated failures. '+details]);return;}timer=setTimeout(launch,1000*failures);});
20
+ }
21
+ launch();return {async close(){closed=true;clearTimeout(timer);const old=child;if(!old||old.exitCode!==null||old.signalCode)return;await new Promise(resolve=>{const timer=setTimeout(()=>old.kill('SIGKILL'),3000);old.once('exit',()=>{clearTimeout(timer);resolve();});if(old.connected)old.disconnect();else old.kill();});}};
22
+ }
@@ -0,0 +1,22 @@
1
+ import path from 'node:path';
2
+
3
+ // macOS native file notifications can miss an edit while the watch program is
4
+ // compiling. Use TypeScript's adaptive metadata polling for project inputs;
5
+ // installed dependencies keep native watchers and incur no extra polling.
6
+ export function createTypeScriptWatchSystem(ts,root,{platform=process.platform,env=process.env}={}){
7
+ const system=ts.sys;
8
+ if(platform!=='darwin'||env.TSC_WATCHFILE||env.TSC_NONPOLLING_WATCHER)return system;
9
+ const requestedRoot=path.resolve(root);
10
+ const physical=filename=>{try{return path.resolve(system.realpath?.(filename)||filename);}catch{return path.resolve(filename);}};
11
+ const physicalRoot=physical(requestedRoot),normalize=filename=>system.useCaseSensitiveFileNames?filename:filename.toLowerCase();
12
+ const inside=(base,filename)=>{
13
+ const relative=path.relative(normalize(base),normalize(filename));
14
+ return relative&&relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative)&&!relative.split(path.sep).includes('node_modules');
15
+ };
16
+ return {...system,watchFile(filename,callback,interval,options){
17
+ const absolute=path.resolve(filename);
18
+ if(options?.watchFile===undefined&&(inside(requestedRoot,absolute)||inside(physicalRoot,absolute))&&inside(physicalRoot,physical(absolute)))
19
+ options={...options,watchFile:ts.WatchFileKind.DynamicPriorityPolling};
20
+ return system.watchFile(filename,callback,interval,options);
21
+ }};
22
+ }
@@ -10,11 +10,12 @@ const execute = promisify(execFile);
10
10
  export function validateTypeScript(input) {
11
11
  if (input === undefined) return undefined;
12
12
  if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('typescript must be an object');
13
- for (const key of Object.keys(input)) if (!['ignoreBuildErrors', 'tsconfigPath'].includes(key)) throw new TypeError(`typescript.${key} is not supported`);
13
+ for (const key of Object.keys(input)) if (!['ignoreBuildErrors', 'tsconfigPath', 'devChecks'].includes(key)) throw new TypeError(`typescript.${key} is not supported`);
14
14
  const { ignoreBuildErrors = false, tsconfigPath = 'tsconfig.json' } = input;
15
15
  if (typeof ignoreBuildErrors !== 'boolean') throw new TypeError('typescript.ignoreBuildErrors must be a boolean');
16
16
  if (typeof tsconfigPath !== 'string' || !tsconfigPath || tsconfigPath.includes('\0') || Buffer.byteLength(tsconfigPath) > 4096) throw new TypeError('typescript.tsconfigPath must be a nonempty filesystem path of at most 4096 bytes');
17
- return { ignoreBuildErrors, tsconfigPath };
17
+ if(input.devChecks!==undefined&&typeof input.devChecks!=='boolean')throw new TypeError('typescript.devChecks must be a boolean');
18
+ return { ignoreBuildErrors, tsconfigPath, ...(input.devChecks===undefined?{}:{devChecks:input.devChecks}) };
18
19
  }
19
20
 
20
21
  export async function projectTsconfig(root, config) {
@@ -32,8 +33,8 @@ export async function projectTsconfig(root, config) {
32
33
 
33
34
  /** Run the checker outside the build/dev process so its graph is released on exit.
34
35
  * Never emit files, rewrite the user's config, or install packages implicitly. */
35
- export async function checkTypeScript(root, config, { dev = false, tsconfig } = {}) {
36
- if (dev || !config.typescript || config.typescript.ignoreBuildErrors) return;
36
+ export async function checkTypeScript(root, config, { dev = false, tsconfig, force = false } = {}) {
37
+ if (!force && (dev || !config.typescript || config.typescript.ignoreBuildErrors)) return;
37
38
  tsconfig ??= await projectTsconfig(root, config);
38
39
  if (!tsconfig) throw new Error('TypeScript checking was explicitly enabled, but tsconfig.json does not exist. Create a TypeScript configuration, set typescript.tsconfigPath to an existing configuration, or explicitly set typescript.ignoreBuildErrors: true if types are checked separately.');
39
40
  let compiler;
@@ -1,11 +1,12 @@
1
1
  import {build as esbuild, transform} from 'esbuild';
2
- import {mkdir, mkdtemp, readFile, writeFile, rm, realpath} from 'node:fs/promises';
2
+ import {mkdir, mkdtemp, readFile, writeFile, rm, rmdir, realpath} from 'node:fs/promises';
3
3
  import {createHash, randomUUID} from 'node:crypto';
4
4
  import {createRequire} from 'node:module';
5
5
  import {fileURLToPath} from 'node:url';
6
6
  import path from 'node:path';
7
7
  import { cachedAsyncTransform } from './transform-cache.mjs';
8
8
  import {inlineSourceMap} from './source-maps.mjs';
9
+ import {buildCacheIdentity} from './persistent-cache.mjs';
9
10
 
10
11
  const require = createRequire(import.meta.url);
11
12
  const {active} = require('./webpack-bridge.cjs');
@@ -13,6 +14,107 @@ const loaderPath = fileURLToPath(new URL('./webpack-loader.cjs', import.meta.url
13
14
  const hash = value => createHash('sha256').update(value).digest('hex').slice(0, 20);
14
15
  const slash = value => value.replaceAll(path.sep, '/');
15
16
  const safeAsset = name => { if (path.isAbsolute(name) || /[\\\0?#]/.test(name) || name.split('/').some(part => !part || part === '.' || part === '..')) throw new Error(`Unsafe webpack asset path: ${name}`); };
17
+ async function discardPrivateFile(file,boundary){
18
+ await rm(file,{force:true});
19
+ for(let directory=path.dirname(file);directory!==boundary&&directory.startsWith(boundary+path.sep);directory=path.dirname(directory)){
20
+ try{await rmdir(directory);}catch(error){if(['ENOTEMPTY','EEXIST'].includes(error.code))break;if(error.code!=='ENOENT')throw error;}
21
+ }
22
+ }
23
+ const sessions=new Map(),sessionLimit=6,sessionBuildLimit=64,objectIds=new WeakMap();
24
+ let nextId=0,created=0,reused=0,graphQueue=Promise.resolve();
25
+ const objectId=value=>{if(!objectIds.has(value))objectIds.set(value,++nextId);return objectIds.get(value);};
26
+ // Arbitrary plugin closures are not interchangeable just because their source
27
+ // text matches. Only known webpack constructors with the same options may be
28
+ // recreated; other plugins must retain their actual instance identity.
29
+ function fingerprint(value,seen=new Set()){
30
+ if(typeof value==='function')return ['function',objectId(value)];
31
+ if(value instanceof RegExp)return ['regexp',value.source,value.flags];
32
+ if(value===undefined)return ['undefined'];
33
+ if(typeof value==='symbol')throw new Error('Opaque symbol in webpack configuration');
34
+ if(typeof value==='bigint')return ['bigint',String(value)];
35
+ if(typeof value==='number'&&(!Number.isFinite(value)||Object.is(value,-0)))return ['number',String(value),Object.is(value,-0)];
36
+ if(!value||typeof value!=='object')return value;
37
+ if(seen.has(value))return ['object',objectId(value)];
38
+ seen.add(value);
39
+ const properties=(skipIndices=false)=>{
40
+ const keys=Reflect.ownKeys(value);
41
+ if(keys.length>10000||keys.some(key=>typeof key==='symbol'))throw new Error('Opaque or excessive webpack configuration');
42
+ return keys.filter(key=>!skipIndices||!/^(?:0|[1-9]\d*)$/.test(key)).sort().map(key=>{
43
+ const descriptor=Object.getOwnPropertyDescriptor(value,key);if(descriptor.get||descriptor.set)throw new Error('Accessor in webpack configuration');
44
+ return[key,descriptor.enumerable,descriptor.configurable,descriptor.writable,fingerprint(descriptor.value,seen)];
45
+ });
46
+ };
47
+ const prototype=Object.getPrototypeOf(value);let result;
48
+ if(Array.isArray(value))result=['array',properties()];
49
+ else if(prototype===Map.prototype){
50
+ const extras=properties();
51
+ if(value.size>10000)throw new Error('Excessive webpack Map configuration');
52
+ const key=item=>item&&(typeof item==='object'||typeof item==='function')?['identity',objectId(item),fingerprint(item,seen)]:fingerprint(item,seen);
53
+ result=['map',[...Map.prototype.entries.call(value)].map(([name,item])=>[key(name),fingerprint(item,seen)]),extras];
54
+ }else if(prototype===Set.prototype){
55
+ const extras=properties();
56
+ if(value.size>10000)throw new Error('Excessive webpack Set configuration');
57
+ result=['set',[...Set.prototype.values.call(value)].map(item=>item&&(typeof item==='object'||typeof item==='function')?['identity',objectId(item),fingerprint(item,seen)]:fingerprint(item,seen)),extras];
58
+ }else if(prototype===Date.prototype)result=['date',String(Date.prototype.getTime.call(value)),properties()];
59
+ else if(prototype===ArrayBuffer.prototype){
60
+ if(value.byteLength>1024*1024)throw new Error('Excessive webpack binary configuration');
61
+ result=['array-buffer',Buffer.from(value).toString('base64'),value.resizable,value.maxByteLength,properties()];
62
+ }else if(ArrayBuffer.isView(value)){
63
+ if(value.byteLength>10000)throw new Error('Excessive webpack binary view configuration');
64
+ const types=[Buffer,Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array,DataView];
65
+ const type=types.find(type=>prototype===type.prototype);
66
+ if(!type)throw new Error('Opaque webpack binary configuration');
67
+ result=[type.name,fingerprint(value.buffer,seen),value.byteOffset,value.byteLength,properties(true)];
68
+ }else if(prototype===Object.prototype||prototype===null)result=['record',prototype===null,properties()];
69
+ else throw new Error('Opaque webpack configuration instance');
70
+ seen.delete(value);return result;
71
+ }
72
+ function pluginIdentity(plugin,webpack){
73
+ const fields={NormalModuleReplacementPlugin:['resourceRegExp','newResource'],IgnorePlugin:['options'],ProvidePlugin:['definitions'],DefinePlugin:['definitions'],BannerPlugin:['options']};
74
+ for(const[name,keys]of Object.entries(fields))if(plugin.constructor===webpack[name])return[name,fingerprint(Object.fromEntries(keys.map(key=>[key,plugin[key]])))];
75
+ return ['instance',objectId(plugin),fingerprint(plugin)];
76
+ }
77
+ export const webpackCompilerStats=()=>({contexts:sessions.size,created,reused,limit:sessionLimit,buildLimit:sessionBuildLimit});
78
+ function queued(callback){const task=graphQueue.then(callback);graphQueue=task.catch(()=>{});return task;}
79
+ export function closeWebpackCompiler(root){return queued(async()=>{
80
+ const closing=[];for(const[key,session]of sessions)if(root===undefined||session.root===root){sessions.delete(key);closing.push(session.dispose());}
81
+ const results=await Promise.allSettled(closing),errors=results.filter(result=>result.status==='rejected').map(result=>result.reason);
82
+ if(errors.length)throw new AggregateError(errors,'Unable to dispose webpack compiler sessions');
83
+ });}
84
+
85
+ async function compileGraph(resolver,loads,options,{state,config,target}){
86
+ return queued(async()=>{
87
+ const webpack=(await import('webpack')).default;
88
+ let reusable=state.dev&&state.incremental,identity;
89
+ if(reusable){try{identity=await buildCacheIdentity(state.root);}catch{}if(!identity)reusable=false;}
90
+ let base,key;
91
+ if(reusable)try{
92
+ const normalize=value=>typeof value==='string'?value.replaceAll(state.stage,'<stage>'):Array.isArray(value)?value.map(normalize):value&&typeof value==='object'&&[Object.prototype,null].includes(Object.getPrototypeOf(value))?Object.fromEntries(Object.entries(value).map(([key,item])=>[key,normalize(item)])):value;
93
+ const normalized=normalize({...options,plugins:(options.plugins||[]).map(plugin=>plugin.name),stdin:options.stdin&&{...options.stdin,contents:undefined}});
94
+ base=hash(JSON.stringify(fingerprint([state.root,target,normalized])));
95
+ key=hash(JSON.stringify([base,identity,fingerprint({...config,graphPlugins:undefined}),config.graphPlugins.map(plugin=>pluginIdentity(plugin,webpack))]));
96
+ }catch{reusable=false;}
97
+ // Unknown data structures, safe mode and unavailable identities all keep
98
+ // compilation correct through a one-shot compiler. Hidden mutable closure
99
+ // state cannot be fingerprinted: replace the plugin instance when it changes.
100
+ if(state.dev&&!reusable)for(const[key,session]of sessions)if(session.root===state.root){sessions.delete(key);await session.dispose();}
101
+ let session=reusable&&sessions.get(key);
102
+ if(session&&session.builds>=sessionBuildLimit){sessions.delete(key);await session.dispose();session=undefined;}
103
+ if(reusable){
104
+ for(const[old,previous]of sessions)if(previous.root===state.root&&(previous.identity!==identity||previous.base===base&&old!==key)){sessions.delete(old);await previous.dispose();}
105
+ }
106
+ if(session){sessions.delete(key);reused++;}
107
+ else{
108
+ if(reusable)while(sessions.size>=sessionLimit){const[old,previous]=sessions.entries().next().value;sessions.delete(old);await previous.dispose();}
109
+ session=await createGraphSession(state.root,{webpack,reusable,stage:state.stage});created++;
110
+ Object.assign(session,{root:state.root,base,identity,builds:0});
111
+ }
112
+ if(reusable)sessions.set(key,session);
113
+ try{session.builds++;return await session.run(resolver,loads,options,{state,config,target});}
114
+ catch(error){if(reusable)sessions.delete(key);await session.dispose();throw error;}
115
+ finally{if(!reusable)await session.dispose();}
116
+ });
117
+ }
16
118
 
17
119
  /** Webpack owns the actual module/chunk graph. PRNext's resolve/load transforms
18
120
  * remain authoritative for React boundaries and the Node/Edge/browser targets.
@@ -37,27 +139,41 @@ export async function webpackGraph(options, {state, config, target}) {
37
139
  finally { for (const dispose of disposes) await dispose(); }
38
140
  }
39
141
 
40
- async function compileGraph(resolver, loads, options, {state,config,target}) {
41
- const webpack = (await import('webpack')).default;
142
+ async function createGraphSession(root,{webpack,reusable,stage}) {
42
143
  const bridgeId = randomUUID();
43
- await mkdir(path.join(state.stage,'.entries'),{recursive:true});
44
- const virtualDirectory = await realpath(await mkdtemp(path.join(state.stage,'.entries/webpack-')));
45
- const temporary = await mkdtemp(path.join(state.stage,'.webpack-output-'));
46
- const outdir = options.outdir || (options.outfile ? path.dirname(options.outfile) : temporary);
47
- const actualOut = options.write === false ? temporary : outdir;
48
- const extension = options.outExtension?.['.js'] || '.js';
49
- const modules = new Map();
50
- const projectRequire = createRequire(path.join(state.root,'package.json'));
51
- let compiler;
52
- const entries = options.stdin ? {entry:path.join(virtualDirectory,'entry.js')}
53
- : Array.isArray(options.entryPoints) ? Object.fromEntries(options.entryPoints.map(file=>[path.basename(file,path.extname(file)),file])) : options.entryPoints;
144
+ await mkdir(path.join(stage,'.entries'),{recursive:true});
145
+ const home=await realpath(await mkdtemp(path.join(reusable?root:path.join(stage,'.entries'),'.prnext-webpack-')));
146
+ const virtualDirectory=path.join(home,'modules'),virtualStage=path.join(home,'stage'),scratchOut=path.join(home,'output');
147
+ await mkdir(virtualDirectory,{recursive:true});
148
+ const projectRequire = createRequire(path.join(root,'package.json'));
149
+ let compiler,resolver,loads,options,state,config,target,outdir,actualOut,extension,modules,entries,runtime,disposed=false;
150
+ const virtualFiles=new Set(),loaderFiles=new Set(),previousOutputs=new Set();let usedVirtualFiles;
151
+ const fromVirtual=value=>typeof value==='string'&&(value===virtualStage||value.startsWith(virtualStage+path.sep))?state.stage+value.slice(virtualStage.length):value;
152
+ const toVirtual=value=>reusable&&typeof value==='string'&&(value===state.stage||value.startsWith(state.stage+path.sep))?virtualStage+value.slice(state.stage.length):value;
153
+ const stageFile=async file=>{
154
+ const mapped=toVirtual(file);if(mapped===file)return file;
155
+ virtualFiles.add(mapped);usedVirtualFiles.add(mapped);
156
+ const contents=await readFile(file);await mkdir(path.dirname(mapped),{recursive:true});
157
+ let previous;try{previous=await readFile(mapped);}catch(error){if(error.code!=='ENOENT')throw error;}
158
+ if(!previous?.equals(contents))await writeFile(mapped,contents);return mapped;
159
+ };
160
+ const run=async(nextResolver,nextLoads,nextOptions,next)=>{
161
+ resolver=nextResolver;loads=nextLoads;options=nextOptions;({state,config,target}=next);
162
+ usedVirtualFiles=new Set();
163
+ outdir=options.outdir||(options.outfile?path.dirname(options.outfile):scratchOut);actualOut=reusable||options.write===false?scratchOut:outdir;extension=options.outExtension?.['.js']||'.js';modules=new Map();
164
+ // Webpack may retain SizeOnlySource for unchanged assets and assume the last
165
+ // emitted file still exists. Keep current outputs until the next run finishes.
166
+ await mkdir(actualOut,{recursive:true});
167
+ entries=options.stdin?{entry:path.join(virtualDirectory,'entry.js')}:Array.isArray(options.entryPoints)?Object.fromEntries(options.entryPoints.map(file=>[path.basename(file,path.extname(file)),toVirtual(file)])):Object.fromEntries(Object.entries(options.entryPoints).map(([name,file])=>[name,toVirtual(file)]));
168
+ for(const file of Object.values(entries))if(file.startsWith(virtualStage+path.sep))await stageFile(fromVirtual(file));
54
169
  if (options.stdin) {
55
170
  const file = entries.entry;
56
171
  await writeFile(file,options.stdin.contents);
172
+ virtualFiles.add(file);usedVirtualFiles.add(file);
57
173
  modules.set(file,{path:file,namespace:'file',resolveDir:options.stdin.resolveDir || state.root,stdin:true});
58
174
  }
59
175
  const identify = record => record.namespace === 'file' ? slash(path.relative(state.root,record.path)) : record.namespace+':'+slash(path.relative(state.root,record.path));
60
- const lookup = resource => modules.get(resource) || modules.get(resource?.split('?')[0]) || {path:resource?.split('?')[0],namespace:'file',suffix:resource?.includes('?')?'?'+resource.split('?').slice(1).join('?'):''};
176
+ const lookup = resource => modules.get(resource) || modules.get(resource?.split('?')[0]) || {path:fromVirtual(resource?.split('?')[0]),namespace:'file',suffix:resource?.includes('?')?'?'+resource.split('?').slice(1).join('?'):''};
61
177
  const load = async(record,source,webpackContext,sourceMap) => {
62
178
  if (record.stdin) return {contents:source,loader:options.stdin.loader || 'js',resolveDir:record.resolveDir};
63
179
  for (const {settings,callback,framework,configuredLoaders} of loads) {
@@ -106,16 +222,16 @@ async function compileGraph(resolver, loads, options, {state,config,target}) {
106
222
  // A changed synchronous chunk may leave webpack's runtime bytes unchanged.
107
223
  // Give each published build its own module cache; React's dev singletons
108
224
  // still retain their identities across the new runtime instance.
109
- const runtime = 'webpack-runtime-'+hash(JSON.stringify([state.stage,Object.keys(entries),options.conditions,target]));
225
+ runtime = 'webpack-runtime-'+hash(JSON.stringify([state.stage,Object.keys(entries),options.conditions,target]));
110
226
  try {
111
- compiler = webpack({name:target,context:state.root,mode:state.dev?'development':'production',
227
+ if(!compiler)compiler = webpack({name:target,context:state.root,mode:state.dev?'development':'production',
112
228
  ...(config.watchOptions ? {watchOptions:config.watchOptions} : {}),
113
- target:target==='node'?'node22':['web','es2022'],entry:entries,cache:false,devtool:options.sourcemap?'source-map':false,
229
+ target:target==='node'?'node22':['web','es2022'],entry:entries,cache:reusable?{type:'memory',maxGenerations:1}:false,devtool:options.sourcemap?'source-map':false,
114
230
  experiments:{outputModule:true,css:true},externalsType:'module',
115
231
  output:{path:actualOut,filename:options.outfile?path.basename(options.outfile):naming(options.entryNames)+extension,
116
232
  chunkFilename:naming(options.chunkNames || 'chunk-[hash]')+extension,cssFilename:naming(options.entryNames)+'.css',
117
233
  cssChunkFilename:'style-[contenthash:16].css',module:true,library:{type:'module'},chunkFormat:'module',chunkLoading:'import',
118
- publicPath:target==='node'?'./':state.assetBase+'/',environment:{module:true,dynamicImport:true},clean:false},
234
+ publicPath:target==='node'?'./':state.assetBase+'/',environment:{module:true,dynamicImport:true},compareBeforeEmit:false,clean:false},
119
235
  performance:false,
120
236
  optimization:{minimize:!!options.minify,minimizer:[{apply(compiler){
121
237
  compiler.hooks.compilation.tap('PRNextMinify',compilation=>{
@@ -130,10 +246,10 @@ async function compileGraph(resolver, loads, options, {state,config,target}) {
130
246
  }
131
247
  });
132
248
  });
133
- }}],runtimeChunk:options.splitting?{name:runtime}:false,
249
+ }}],runtimeChunk:options.splitting?{name:()=>runtime}:false,
134
250
  splitChunks:options.splitting?{chunks:'all',minSize:0,cacheGroups:{default:false,defaultVendors:false,shared:{minChunks:2,test:module=>!module.type.startsWith('css'),name:false}}}:false},
135
251
  resolve:{alias:config.resolution.resolveAlias || {},conditionNames:[...(options.conditions || []),'...'],extensions:config.resolution.resolveExtensions || ['.tsx','.ts','.jsx','.js','.mjs','.json'],fullySpecified:false},
136
- module:{rules:[{test:/./,type:'javascript/auto',enforce:'post',use:[{loader:loaderPath,options:{bridge:bridgeId}}]},
252
+ module:{unsafeCache:false,rules:[{test:/./,type:'javascript/auto',enforce:'post',use:[{loader:loaderPath,options:{bridge:bridgeId}}]},
137
253
  {test:/\.json$/,type:'json'},{test:/\.(?:css|scss|sass)$/,type:'css'},
138
254
  {dependency:'url',type:'asset/resource'}]},
139
255
  plugins:[...config.graphPlugins,
@@ -143,7 +259,7 @@ async function compileGraph(resolver, loads, options, {state,config,target}) {
143
259
  {apply(compiler) {
144
260
  if (state.devInputs && config.graphPlugins.some(plugin=>plugin.constructor?.name==='ContentlayerWebpackPlugin')) {
145
261
  compiler.hooks.beforeCompile.tapPromise({name:'PRNextGeneratedInputs',stage:Number.MAX_SAFE_INTEGER},()=>
146
- state.generatedInputs ||= state.devInputs.captureDirectory('.contentlayer/generated'));
262
+ state.devInputs&&(state.generatedInputs ||= state.devInputs.captureDirectory('.contentlayer/generated')));
147
263
  }
148
264
  compiler.hooks.normalModuleFactory.tap('PRNextFramework',factory=>{
149
265
  factory.hooks.beforeResolve.tapPromise({name:'PRNextFramework',stage:10000},async data=>{
@@ -151,12 +267,12 @@ async function compileGraph(resolver, loads, options, {state,config,target}) {
151
267
  const importer = lookup(data.contextInfo.issuer);
152
268
  const dependency = data.dependencies[0];
153
269
  const kind = dependency?.type?.includes('url') ? 'url-token' : dependency?.type==='css @import'?'import-rule':data.dependencyType==='commonjs'?'require-call':dependency?.type==='import()'?'dynamic-import':data.contextInfo.issuer?'import-statement':'entry-point';
154
- const settings={resolveDir:importer.resolveDir || data.context || state.root,importer:importer.path || '',namespace:importer.namespace || 'file',kind};
270
+ const settings={resolveDir:fromVirtual(importer.resolveDir || data.context || state.root),importer:importer.path || '',namespace:importer.namespace || 'file',kind};
155
271
  // Let webpack resolve/execute the inline loader chain itself. Only
156
272
  // its resource passes through framework resolution.
157
273
  const bang=data.request.lastIndexOf('!');
158
274
  const inline=bang<0?'':data.request.slice(0,bang+1);
159
- const request=bang<0?data.request:data.request.slice(bang+1);
275
+ const request=fromVirtual(bang<0?data.request:data.request.slice(bang+1));
160
276
  const resolved = await resolver.resolve(request,settings);
161
277
  if (resolved.errors.length) throw new Error(resolved.errors.map(error=>error.text).join('\n'));
162
278
  if (resolved.external) {
@@ -174,6 +290,9 @@ async function compileGraph(resolver, loads, options, {state,config,target}) {
174
290
  if(record.namespace!=='file'){
175
291
  resource=path.join(virtualDirectory,hash(JSON.stringify([record.namespace,record.path,record.suffix]))+(/css$/.test(record.namespace)?'.css':'.js'));
176
292
  if(!modules.has(resource))await writeFile(resource,'');
293
+ virtualFiles.add(resource);usedVirtualFiles.add(resource);
294
+ }else if(toVirtual(record.path)!==record.path){
295
+ resource=await stageFile(record.path)+(record.suffix||'');
177
296
  }
178
297
  data.request=inline+resource;
179
298
  modules.set(data.request,record);
@@ -206,6 +325,11 @@ async function compileGraph(resolver, loads, options, {state,config,target}) {
206
325
  });
207
326
  }},
208
327
  ]});
328
+ // Native loader modules live outside webpack's graph cache. Reload tracked
329
+ // implementations so editing a loader cannot retain its require() closure.
330
+ const clearLoader=file=>{const loaded=require.cache[file];delete require.cache[file];for(const child of loaded?.children||[])if(child.filename!==loaderPath)clearLoader(child.filename);};
331
+ for(const file of loaderFiles)clearLoader(file);
332
+ compiler.inputFileSystem.purge?.();
209
333
  const stats=await new Promise((resolve,reject)=>compiler.run((error,stats)=>error?reject(error):resolve(stats)));
210
334
  if(stats.hasErrors())throw new Error(stats.toString({all:false,errors:true,errorDetails:true}));
211
335
  for(const warning of stats.compilation.warnings){
@@ -218,10 +342,11 @@ async function compileGraph(resolver, loads, options, {state,config,target}) {
218
342
  for(const chunk of compilation.chunks)for(const file of chunk.files)chunkFiles.add(file);
219
343
  for(const [name,entry] of compilation.entrypoints){
220
344
  const file=[...entry.getEntrypointChunk().files].find(file=>file.endsWith(extension));
221
- if(file)entryFiles.set(file,entries[name]);
345
+ if(file)entryFiles.set(file,fromVirtual(entries[name]));
222
346
  const css=entry.getFiles().find(file=>file.endsWith('.css'));if(file&&css)styles.set(file,css);
223
347
  }
224
348
  for(const module of compilation.modules){
349
+ for(const loader of module.loaders||[])if(loader.loader!==loaderPath)loaderFiles.add(loader.loader);
225
350
  if(!module.resource)continue;
226
351
  const record=lookup(module.resource), imports=[];
227
352
  for(const connection of compilation.moduleGraph.getOutgoingConnections(module)){
@@ -234,6 +359,7 @@ async function compileGraph(resolver, loads, options, {state,config,target}) {
234
359
  safeAsset(asset.name);
235
360
  const contents=await readFile(path.join(actualOut,asset.name));
236
361
  const file=path.join(outdir,asset.name), key=slash(path.relative(state.root,file));
362
+ if(options.write!==false&&file!==path.join(actualOut,asset.name)){await mkdir(path.dirname(file),{recursive:true});await writeFile(file,contents);}
237
363
  outputs[key]={bytes:contents.length,imports:[],exports:[],inputs:inputSizes,
238
364
  ...(entryFiles.has(asset.name)?{entryPoint:slash(path.relative(state.root,entryFiles.get(asset.name)))}:{}),
239
365
  ...(styles.has(asset.name)?{cssBundle:slash(path.relative(state.root,path.join(outdir,styles.get(asset.name))))}:{})};
@@ -248,9 +374,20 @@ async function compileGraph(resolver, loads, options, {state,config,target}) {
248
374
  }
249
375
  const rank = file => entryFiles.has(slash(path.relative(outdir,file.path))) ? 0 : file.path.endsWith(extension) ? 1 : 2;
250
376
  outputFiles.sort((a,b)=>rank(a)-rank(b));
377
+ if(reusable){
378
+ const currentOutputs=new Set(compilation.getAssets().map(asset=>asset.name));
379
+ for(const name of previousOutputs)if(!currentOutputs.has(name))await discardPrivateFile(path.join(actualOut,name),actualOut);
380
+ previousOutputs.clear();for(const name of currentOutputs)previousOutputs.add(name);
381
+ }
251
382
  return {errors:[],warnings:[],metafile:{inputs,outputs},...(options.write===false?{outputFiles}:{})};
252
383
  } finally {
253
- try {if(compiler)await new Promise((resolve,reject)=>compiler.close(error=>error?reject(error):resolve()));}
254
- finally {active.delete(bridgeId);await rm(temporary,{recursive:true,force:true});await rm(virtualDirectory,{recursive:true,force:true});}
384
+ active.delete(bridgeId);
385
+ for(const file of virtualFiles)if(!usedVirtualFiles.has(file)){virtualFiles.delete(file);await discardPrivateFile(file,home);}
255
386
  }
387
+ };
388
+ return {run,async dispose(){
389
+ if(disposed)return;disposed=true;
390
+ try{if(compiler)await new Promise((resolve,reject)=>compiler.close(error=>error?reject(error):resolve()));}
391
+ finally{active.delete(bridgeId);await rm(home,{recursive:true,force:true});compiler=resolver=state=config=options=modules=undefined;loads=[];virtualFiles.clear();loaderFiles.clear();previousOutputs.clear();}
392
+ }};
256
393
  }
@@ -0,0 +1,9 @@
1
+ export function validateWorkerPreload(value){
2
+ if(value===undefined||value===false)return undefined;
3
+ if(!value||typeof value!=='object'||Array.isArray(value))throw new TypeError('workerPreload must be false or an object');
4
+ for(const key of Object.keys(value))if(!['routes','maxRssMb','timeoutMs'].includes(key))throw new TypeError(`workerPreload.${key} is not supported`);
5
+ if(!Array.isArray(value.routes)||!value.routes.length||value.routes.length>16||value.routes.some(route=>typeof route!=='string'||!route.startsWith('/')||route.length>1024||/[?#\x00-\x1f]/.test(route))||new Set(value.routes).size!==value.routes.length)throw new TypeError('workerPreload.routes must contain 1 to 16 distinct route patterns');
6
+ const options={routes:value.routes,maxRssMb:value.maxRssMb??256,timeoutMs:value.timeoutMs??5000};
7
+ for(const [key,min,max] of [['maxRssMb',16,2048],['timeoutMs',100,30000]])if(!Number.isInteger(options[key])||options[key]<min||options[key]>max)throw new TypeError(`workerPreload.${key} must be between ${min} and ${max}`);
8
+ return options;
9
+ }