@thomas.f/prnext 0.1.15-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 +387 -12
  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 +28 -10
  69. package/release/ai-docs.mjs +48 -0
  70. package/release/verify.mjs +6 -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,90 @@
1
+ import path from 'node:path';
2
+ import {pathToFileURL} from 'node:url';
3
+ import {writeFile} from 'node:fs/promises';
4
+ import {createHash} from 'node:crypto';
5
+ import {localizedStaticPaths} from './i18n.mjs';
6
+ import {mapConcurrent} from './concurrent.mjs';
7
+ const isDynamic=pattern=>pattern.includes('[');
8
+
9
+ export async function prerenderPages(stage, manifest, dev) {
10
+ // Development renders on demand. Do not load a fresh renderer or execute
11
+ // getStaticProps/getStaticPaths for every route on every edit.
12
+ const renderer = dev ? null : await import(pathToFileURL(path.join(stage, 'runtime/render.mjs')).href);
13
+ const paths = new Set();
14
+ const seedsByRoute = new Map();
15
+ const validatedDocuments = new Set();
16
+ const staticListings = new Map();
17
+ // Error page data is available when another static page returns notFound.
18
+ const ordered = [...manifest.routes].sort((left, right) => Number(!!right.errorStatus) - Number(!!left.errorStatus));
19
+ async function renderRoute(route) {
20
+ if (route.kind !== 'page' || route.router === 'app') return;
21
+ const modulePath = path.join(stage, route.module);
22
+ const page = await import(pathToFileURL(modulePath).href);
23
+ if (page.__prnextDocument && !validatedDocuments.has(page.__prnextDocument)) {
24
+ for (const name of ['getStaticProps', 'getStaticPaths', 'getServerSideProps']) {
25
+ if (Object.hasOwn(page.__prnextDocument, name) || typeof page.Document?.[name] === 'function') throw new Error(`pages/_document does not support ${name}. Use Document.getInitialProps for server document rendering.`);
26
+ }
27
+ validatedDocuments.add(page.__prnextDocument);
28
+ }
29
+ if (route.errorStatus && (page.getServerSideProps || page.default?.getInitialProps)) throw new Error(`pages${route.pattern} must be static and cannot use getServerSideProps or getInitialProps. Use getStaticProps instead.`);
30
+ route.gip = typeof page.default?.getInitialProps === 'function';
31
+ route.appGip = !!page.App && page.App.getInitialProps !== page.App.origGetInitialProps;
32
+ if (route.internal && route.errorStatus && route.appGip) {
33
+ // A custom App hook makes the built-in Error request-dependent. Select
34
+ // its actual _error module so the default status props and visible URL
35
+ // reach App.getInitialProps instead of a synthetic static wrapper.
36
+ const kind = route.errorStatus === 404 ? 'notFound' : 'serverError';
37
+ if (manifest.pagesErrors?.[kind] === route.id) delete manifest.pagesErrors[kind];
38
+ }
39
+ if (route.gip && (page.getStaticProps || page.getServerSideProps)) throw new Error(`Page ${route.pattern} cannot combine getInitialProps with getStaticProps or getServerSideProps.`);
40
+ if (route.internal && !route.errorStatus) {
41
+ if (page.getStaticProps || page.getServerSideProps || page.getStaticPaths) throw new Error('pages/_error does not support getStaticProps, getServerSideProps or getStaticPaths. Use ErrorComponent.getInitialProps instead.');
42
+ return;
43
+ }
44
+ if (page.getServerSideProps && page.getStaticProps) throw new Error(`Page ${route.pattern} exports both getServerSideProps and getStaticProps. Choose one.`);
45
+ if (page.getStaticPaths && !page.getStaticProps) throw new Error(`Page ${route.pattern} exports getStaticPaths without getStaticProps.`);
46
+ if (page.getServerSideProps) { route.ssp = true; return; }
47
+ if (page.getStaticProps) route.ssg = true;
48
+ else if (route.gip || route.appGip) return;
49
+ if (isDynamic(route.pattern) && page.getStaticProps && !page.getStaticPaths) throw new Error(`Dynamic page ${route.pattern} with getStaticProps must export getStaticPaths.`);
50
+ if (!isDynamic(route.pattern) && page.getStaticPaths) throw new Error(`getStaticPaths is only valid on dynamic pages (${route.pattern}).`);
51
+ // Preserve data/export validation and SSG metadata. The dev PageCache calls
52
+ // getStaticPaths per request, including fallback:false and locale checks.
53
+ if (dev) return;
54
+ let entries = [{ path: route.pattern, params: {} }];
55
+ if (isDynamic(route.pattern)) {
56
+ if (!page.getStaticProps) return;
57
+ if (!staticListings.has(modulePath)) staticListings.set(modulePath, Promise.resolve().then(() => page.getStaticPaths(manifest.config.i18n ? {locales: manifest.config.i18n.locales, defaultLocale: manifest.config.i18n.defaultLocale} : {})));
58
+ const result = localizedStaticPaths(route, await staticListings.get(modulePath), manifest.config.i18n);
59
+ route.fallback = result.fallback;
60
+ entries = result.paths;
61
+ }
62
+ for (const entry of entries) {
63
+ if (paths.has(entry.path)) throw new Error(`Multiple pages prerender the same path: ${entry.path}.`);
64
+ paths.add(entry.path);
65
+ const rendered = await renderer.prerenderRoute({ ...manifest.config, manifest, distDir: stage, route, modulePath, path: entry.path, params: entry.params, client: route.client, css: route.css, pattern: route.pattern, errorStatus: route.errorStatus, buildId: manifest.buildId, production: !dev });
66
+ const file = `static/${createHash('sha256').update(entry.path).digest('hex').slice(0, 20)}.html`;
67
+ await writeFile(path.join(stage, file), rendered.body);
68
+ const seed = { routeId: route.id, path: entry.path, file, status: route.errorStatus || rendered.status || 200, headers: rendered.headers || {} };
69
+ if (route.ssg) {
70
+ seed.dataFile = file.replace(/\.html$/, '.json');
71
+ seed.revalidate = rendered.revalidate ?? false;
72
+ seed.generatedAt = rendered.generatedAt;
73
+ await writeFile(path.join(stage, seed.dataFile), rendered.dataJSON);
74
+ }
75
+ manifest.prerendered.push(seed);
76
+ if (!seedsByRoute.has(route.id)) seedsByRoute.set(route.id, []);
77
+ seedsByRoute.get(route.id).push(seed);
78
+ }
79
+ if (route.fallback === true) {
80
+ const rendered = await renderer.renderFallback({ ...manifest.config, modulePath, path: route.pattern, params: {}, client: route.client, css: route.css, pattern: route.pattern, buildId: manifest.buildId, production: !dev });
81
+ route.fallbackFile = `static/fallback-${route.id}.html`;
82
+ await writeFile(path.join(stage, route.fallbackFile), rendered.body);
83
+ }
84
+ }
85
+ // Error seeds must exist before ordinary pages can render notFound/failures.
86
+ for(const route of ordered.filter(route=>route.errorStatus))await renderRoute(route);
87
+ await mapConcurrent(ordered.filter(route=>!route.errorStatus),renderRoute);
88
+ // Rendering errors first must not change the published route/seed ordering.
89
+ manifest.prerendered = manifest.routes.flatMap(route => seedsByRoute.get(route.id) || []);
90
+ }
@@ -0,0 +1,10 @@
1
+ import {prerenderPages} from './prerender-pages.mjs';
2
+ import {prerenderApp} from './app-static.mjs';
3
+ process.once('message',async({stage,manifest})=>{
4
+ try{
5
+ await prerenderPages(stage,manifest,false);
6
+ await prerenderApp(stage,manifest,false);
7
+ process.send({ok:true,manifest},()=>process.exit(0));
8
+ }catch(error){process.send({ok:false,error:error.stack||error.message},()=>process.exit(1));}
9
+ });
10
+ process.once('disconnect',()=>process.exit(1));
@@ -0,0 +1,80 @@
1
+ import {fork} from 'node:child_process';
2
+ import {readFile,realpath} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import {watchProject} from '../runtime/watch-project.mjs';
5
+ import {createDevChangeFilter} from '../runtime/dev-file-changes.mjs';
6
+ import {incrementalCompilerStats} from './incremental.mjs';
7
+
8
+ /** Explicit reusable build session for integrations; always dispose it. */
9
+ export async function createBuilder(projectRoot,options={}){
10
+ const root=await realpath(projectRoot);let disposed=false,pending=Promise.resolve();
11
+ return {
12
+ build(overrides={}){
13
+ if(disposed)return Promise.reject(new Error('Builder has been disposed'));
14
+ const task=pending.then(async()=>{const {build}=await import('./index.mjs');return build(root,{...options,...overrides,dev:false,incremental:true});});
15
+ pending=task.catch(()=>{});return task;
16
+ },
17
+ stats:incrementalCompilerStats,
18
+ async dispose(){disposed=true;await pending;const {disposeBuildSession}=await import('./index.mjs');await disposeBuildSession(root);},
19
+ };
20
+ }
21
+
22
+ // Application imports, SSG and RSC isolates belong to one build only. Keeping
23
+ // esbuild's graph must not retain application globals or execute stale modules.
24
+ export function isolatedPrerender(root,stage,manifest){
25
+ return new Promise((resolve,reject)=>{
26
+ const child=fork(new URL('./prerender-worker.mjs',import.meta.url),[],{cwd:root,execArgv:[],stdio:['ignore','inherit','inherit','ipc'],windowsHide:true});
27
+ let result;
28
+ const timeout=setTimeout(()=>{child.kill();reject(new Error('Production prerender exceeded ten minutes'));},600000);
29
+ child.once('error',error=>{clearTimeout(timeout);reject(error);});
30
+ child.on('message',value=>{result=value;});
31
+ child.once('exit',(code,signal)=>{clearTimeout(timeout);result?.ok&&code===0?resolve(result.manifest):reject(new Error(result?.error||`Prerender exited ${signal||code}`));});
32
+ child.send({stage,manifest});
33
+ });
34
+ }
35
+
36
+ /** Retain production graphs during local rebuilds; no HTTP listener/daemon. */
37
+ export async function watchProduction(root,options={}){
38
+ let worker,watcher,stopping=false,timer,busy=false,queued=false;
39
+ const changed=new Set(),filter=createDevChangeFilter(root);
40
+ let output='.prnext';
41
+ const stopWorker=async()=>{
42
+ const old=worker;worker=undefined;if(!old||old.exitCode!==null||old.signalCode)return;
43
+ await new Promise(resolve=>{const timer=setTimeout(()=>old.kill(),5000);old.once('exit',()=>{clearTimeout(timer);resolve();});old.disconnect();});
44
+ };
45
+ const shutdown=async()=>{if(stopping)return;stopping=true;clearTimeout(timer);watcher?.close();await stopWorker();};
46
+ process.once('SIGINT',shutdown);process.once('SIGTERM',shutdown);
47
+ async function rebuild(){
48
+ if(stopping)return;if(busy){queued=true;return;}busy=true;
49
+ const pending=[...changed];changed.clear();
50
+ try{
51
+ const actual=await filter(pending);
52
+ if(worker&&!actual.length)return;
53
+ if(actual.some(file=>/(?:^|\/)(?:(?:next|prnext|contentlayer)\.config\.|package(?:-lock)?\.json$|pnpm-lock\.yaml$|yarn\.lock$|\.env)/.test(file)))await stopWorker();
54
+ if(!worker){worker=fork(new URL('./dev-worker.mjs',import.meta.url),[],{cwd:root,execArgv:[],stdio:['inherit','inherit','inherit','ipc'],windowsHide:true});}
55
+ const current=worker;
56
+ const result=await new Promise((resolve,reject)=>{
57
+ const cleanup=()=>{current.off('message',message);current.off('exit',exit);current.off('error',error);};
58
+ const message=value=>{if(value?.type==='result'){cleanup();resolve(value);}};
59
+ const exit=()=>{cleanup();reject(new Error('Production compiler exited'));};
60
+ const error=value=>{cleanup();reject(value);};
61
+ current.on('message',message);current.once('exit',exit);current.once('error',error);
62
+ current.send({type:'build',root,options:{...options,dev:false,incremental:true}});
63
+ });
64
+ if(result.recycle)await stopWorker();
65
+ if(!result.ok)throw new Error(result.error);
66
+ filter.acceptInputs(result.inputs||[]);
67
+ try{output=JSON.parse(await readFile(path.join(root,'.prnext-output.json'),'utf8')).distDir||output;}catch{}
68
+ await watcher?.refresh();
69
+ console.log(`Watching production sources (${result.compiler?.reused||0} compiler graph reuse(s)).`);
70
+ }catch(error){console.error(error.stack||error);}
71
+ finally{busy=false;if(queued&&!stopping){queued=false;void rebuild();}}
72
+ }
73
+ try{
74
+ watcher=await watchProject(root,{ignore:relative=>relative===output||relative.startsWith(output+'/'),onError:error=>{console.error(error);process.exitCode=1;void shutdown();},onChange:file=>{changed.add(String(file).replaceAll(path.sep,'/'));clearTimeout(timer);timer=setTimeout(()=>void rebuild(),150);}});
75
+ await rebuild();
76
+ // The watcher owns the event-loop lifetime. Stop signals close it and the
77
+ // compiler child; builds continue to publish through the existing lock.
78
+ await new Promise(resolve=>{const poll=setInterval(()=>{if(stopping&&!worker){clearInterval(poll);resolve();}},100);});
79
+ }finally{await shutdown();process.off('SIGINT',shutdown);process.off('SIGTERM',shutdown);}
80
+ }
package/build/scan.mjs CHANGED
@@ -51,7 +51,7 @@ function validatePattern(segments, relativeFile) {
51
51
  return pattern;
52
52
  }
53
53
 
54
- export async function scanProject(projectRoot, { basePath = '', pageExtensions, i18n } = {}) {
54
+ export async function scanProject(projectRoot, { basePath = '', pageExtensions, i18n, devAppFiles } = {}) {
55
55
  const sourceExtension = pageExtensionPattern(pageExtensions);
56
56
  const root = path.resolve(projectRoot);
57
57
  const middleware = await scanMiddleware(root, { basePath, pageExtensions, i18n });
@@ -96,7 +96,7 @@ export async function scanProject(projectRoot, { basePath = '', pageExtensions,
96
96
  });
97
97
  }
98
98
  const appRoot = appRoots[0];
99
- const { appNotFound, appGlobalError } = appRoot ? await scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension, pageExtensions }) : {};
99
+ const { appNotFound, appGlobalError } = appRoot ? await scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension, pageExtensions, devAppFiles }) : {};
100
100
  const metadataManifest = appRoot ? await scanMetadataRoutes({ root, appRoot, files: await walk(appRoot), routes, patterns, validatePattern, basePath, pageExtensions, appNotFound }) : undefined;
101
101
  if (metadataManifest) for (const route of routes) if (route.router === 'app' && route.kind === 'page') route.staticMetadata = { manifest: metadataManifest };
102
102
  if (!routes.length && !appNotFound) throw new Error('No page or API route found in the Pages Router or App Router directories.');
@@ -108,7 +108,7 @@ const appConventions = new Map([
108
108
  ['loading', 'loading'], ['error', 'error'], ['not-found', 'notFound'], ['default', 'default'],
109
109
  ]);
110
110
 
111
- async function scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension, pageExtensions = defaultPageExtensions }) {
111
+ async function scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension, pageExtensions = defaultPageExtensions, devAppFiles }) {
112
112
  const directories = new Map();
113
113
  const configs = new Map();
114
114
  let appGlobalError;
@@ -135,7 +135,7 @@ async function scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension,
135
135
  const descriptor = directories.get(directory) || { directory, files: {} };
136
136
  const name = appConventions.get(filename);
137
137
  if (descriptor.files[name]) { console.warn(`Duplicate ${filename} file ${relative}; using ${path.relative(appRoot, descriptor.files[name])} according to pageExtensions.`); continue; }
138
- if (['page', 'layout', 'route'].includes(filename)) configs.set(file, await validateAppConfig(file));
138
+ if (['page', 'layout', 'route'].includes(filename)) configs.set(file, !devAppFiles || devAppFiles.has(file) ? await validateAppConfig(file) : {});
139
139
  descriptor.files[name] = file;
140
140
  directories.set(directory, descriptor);
141
141
  }
@@ -0,0 +1,42 @@
1
+ import {parseSync} from 'oxc-parser';
2
+ import {createHash} from 'node:crypto';
3
+
4
+ // Read-only analyses share one Rust parse. Transformations still use Babel's
5
+ // binding-aware AST: ESTree nodes must never be passed to Babel transforms.
6
+ const cache=new Map();
7
+ let bytes=0,hits=0,misses=0;
8
+ export const sourceAnalysisStats=()=>({entries:cache.size,bytes,hits,misses});
9
+ export function analyzeSource(source,file='source.js') {
10
+ const key=file+'\0'+createHash('sha256').update(source).digest('hex');
11
+ if(cache.has(key)){const hit=cache.get(key);cache.delete(key);cache.set(key,hit);hits++;return hit.value;}
12
+ misses++;
13
+ const parsed=parseSync(file,source,{sourceType:'unambiguous',allowReturnOutsideFunction:true});
14
+ if(parsed.errors.length)throw new SyntaxError(`${file}: ${parsed.errors.map(error=>error.message).join('; ')}`);
15
+ const program=parsed.program,directives=[];
16
+ for(const node of program.body){if(!node.directive)break;directives.push(node.directive);}
17
+ const imports=[];
18
+ let computedRequire=false;
19
+ const pending=[program];
20
+ while(pending.length){
21
+ const node=pending.pop();
22
+ if(['ImportDeclaration','ExportNamedDeclaration','ExportAllDeclaration'].includes(node.type)&&node.source&&node.importKind!=='type'&&node.exportKind!=='type')imports.push({path:node.source.value,kind:'import-statement'});
23
+ if(node.type==='ImportExpression'&&typeof node.source?.value==='string')imports.push({path:node.source.value,kind:'dynamic-import'});
24
+ if(node.type==='CallExpression'&&node.callee?.type==='Identifier'&&node.callee.name==='require'){
25
+ const argument=node.arguments[0];
26
+ if(argument?.type==='Literal'&&typeof argument.value==='string')imports.push({path:argument.value,kind:'require-call'});else computedRequire=true;
27
+ }
28
+ for(const value of Object.values(node)){
29
+ if(Array.isArray(value)){for(let i=value.length-1;i>=0;i--)if(value[i]?.type)pending.push(value[i]);}
30
+ else if(value&&typeof value==='object'&&value.type)pending.push(value);
31
+ }
32
+ }
33
+ const value={program,directives,imports,computedRequire,frameworkBoundary:directives.some(item=>item==='use client'||item==='use server')};
34
+ // ASTs are substantially larger than source. Bound both count and estimated
35
+ // retained size; large individual npm modules are analyzed without retention.
36
+ const size=Buffer.byteLength(source)*16+Buffer.byteLength(file);
37
+ if(size<=4*1024*1024){
38
+ while(cache.size&&(cache.size>=64||bytes+size>16*1024*1024)){const [old,hit]=cache.entries().next().value;cache.delete(old);bytes-=hit.size;}
39
+ cache.set(key,{value,size});bytes+=size;
40
+ }
41
+ return value;
42
+ }
@@ -7,6 +7,7 @@ import { OUTPUT_POINTER, prepareBuildDirectory, readBuildDirectory, validateDist
7
7
  import { writeBuildOutput } from './output-contract.mjs';
8
8
  import { precompressBuild } from './precompress.mjs';
9
9
  import { withBuildLock } from './build-lock.mjs';
10
+ import { plainStaticPath } from './static-input.mjs';
10
11
 
11
12
  const MAX_FILES = 20000, MAX_BYTES = 512 * 1024 * 1024;
12
13
  const inside = (root, file) => file.startsWith(root + path.sep);
@@ -23,17 +24,18 @@ async function importStatic(directory, { from, spa, distDir, dev }) {
23
24
  const root = await realpath(directory);
24
25
  // Reuse the output-directory restrictions to keep imports within this project
25
26
  // and away from source/dependency/repository trees.
26
- const relative = validateDistDir(from);
27
+ const rootInput = from === '.';
28
+ const relative = rootInput ? '.' : validateDistDir(from);
27
29
  const output = distDir === undefined ? await readBuildDirectory(root) : validateDistDir(distDir);
28
30
  const source = path.join(root, relative), destination = await prepareBuildDirectory(root, output);
29
- if (source === destination || inside(source, destination) || inside(destination, source)) throw new Error('Static input and PRNext output directories must not overlap.');
31
+ if (!rootInput && (source === destination || inside(source, destination) || inside(destination, source))) throw new Error('Static input and PRNext output directories must not overlap.');
30
32
  let ancestor = root;
31
- for (const part of relative.split('/')) {
33
+ for (const part of rootInput ? [] : relative.split('/')) {
32
34
  ancestor = path.join(ancestor, part);
33
35
  const entry = await lstat(ancestor);
34
36
  if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error('Static input must use real directories, not symbolic links.');
35
37
  }
36
- if (!inside(root, await realpath(source))) throw new Error('Static input must stay inside the project.');
38
+ if (!rootInput && !inside(root, await realpath(source))) throw new Error('Static input must stay inside the project.');
37
39
  const index = await lstat(path.join(source, 'index.html')).catch(() => null);
38
40
  if (!index?.isFile() || index.isSymbolicLink()) throw new Error('Static input needs index.html. Run your framework build first and select its public output with --from.');
39
41
 
@@ -50,12 +52,18 @@ async function importStatic(directory, { from, spa, distDir, dev }) {
50
52
  async function copy(folder = '', depth = 0) {
51
53
  if (depth > 64) throw new Error('Static input exceeds 64 directory levels.');
52
54
  for (const name of (await readdir(path.join(source, folder))).sort()) {
55
+ const file = folder ? `${folder}/${name}` : name;
56
+ if (rootInput && (!plainStaticPath(file, true) || file === output || file.startsWith(output + '/'))) continue;
57
+ if (rootInput) {
58
+ const entry = await lstat(path.join(source, file));
59
+ if (entry.isSymbolicLink()) throw new Error(`Static input cannot contain symbolic links: ${file}`);
60
+ if (!entry.isDirectory() && !plainStaticPath(file)) continue;
61
+ }
53
62
  if (name.includes('\\') || /[\x00-\x1f\x7f:]/.test(name) ||
54
63
  name.startsWith('.') && !(folder === '' && name === '.well-known') ||
55
64
  ['node_modules', '_prnext', '_next'].includes(name) || folder === '' && name === 'server') {
56
65
  throw new Error(`Static input contains a private, reserved or server path: ${folder ? folder + '/' : ''}${name}. Select only browser-ready files.`);
57
66
  }
58
- const file = folder ? `${folder}/${name}` : name;
59
67
  const entry = await lstat(path.join(source, file));
60
68
  if (entry.isSymbolicLink()) throw new Error(`Static input cannot contain symbolic links: ${file}`);
61
69
  if (entry.isDirectory()) { await copy(file, depth + 1); continue; }
@@ -0,0 +1,48 @@
1
+ import { lstat, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const outputs = ['dist', 'build', 'out', '.output/public'];
5
+ async function file(root, name) {
6
+ try { const entry = await lstat(path.join(root, name)); return entry.isFile() && !entry.isSymbolicLink(); }
7
+ catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return false; throw error; }
8
+ }
9
+
10
+ /** Root imports deliberately publish browser assets only, never a project tree. */
11
+ export function plainStaticPath(file, directory = false) {
12
+ const parts = file.split('/'), name = parts.at(-1);
13
+ if (parts.some(part => part.startsWith('.') && part !== '.well-known' ||
14
+ /^(?:node_modules|server|api|functions|config|src|app|pages|components|lib|packages|tests?|coverage|dist|build|out)$/i.test(part))) return false;
15
+ if (directory) return true;
16
+ if (/(?:^|[.-])(?:config|server|test|spec)(?:[.-]|$)/i.test(name)) return false;
17
+ if (parts[0] === '.well-known') return /\.(?:txt|json)$/.test(name);
18
+ return /\.(?:html?|css|js|mjs|wasm|webmanifest|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|otf|eot|mp4|webm|mp3|ogg|wav|pdf)$/i.test(name) || /^(?:robots|ads|security)\.txt$/i.test(name);
19
+ }
20
+
21
+ /** Inspect metadata only; never execute a framework config or build command. */
22
+ export async function detectBuildAdapter(root, { prepared = false } = {}) {
23
+ let manifest = {};
24
+ try {
25
+ const contents = await readFile(path.join(root, 'package.json'));
26
+ if (contents.length > 256 * 1024) throw new Error('package.json exceeds 256 KiB');
27
+ manifest = JSON.parse(contents);
28
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) throw new Error('Invalid package.json');
29
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
30
+ const deps = { ...manifest.devDependencies, ...manifest.dependencies };
31
+ if ('next' in deps) return { adapter: 'react' };
32
+ // A Vite index.html is source, and must not be mistaken for a completed site.
33
+ const builder = ['vite', 'astro', '@sveltejs/kit', 'nuxt', 'react-scripts', '@angular/core', '@tanstack/react-start', '@tanstack/solid-start'].some(name => name in deps);
34
+ if (!builder) for (const name of ['app', 'pages', 'src/app', 'src/pages']) {
35
+ try { if ((await lstat(path.join(root, name))).isDirectory()) return { adapter: 'react' }; }
36
+ catch (error) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error; }
37
+ }
38
+ const script = manifest.scripts?.build;
39
+ if (!builder && (!script || /^(?:prnext|prn) build(?:\s|$)/.test(script)) && await file(root, 'index.html')) return { adapter: 'static', from: '.' };
40
+ if (prepared) {
41
+ const found = [];
42
+ for (const from of outputs) if (await file(root, from + '/index.html')) found.push(from);
43
+ if (found.length > 1) throw new Error('Multiple static outputs found. Select one with --adapter static --from <directory>.');
44
+ if (found.length === 1) return { adapter: 'static', from: found[0] };
45
+ throw new Error('No completed browser build found. Run your framework build, then use --adapter static --from <directory>. Plain HTML sites need index.html at the root.');
46
+ }
47
+ return { adapter: 'react' };
48
+ }
package/build/styles.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  import { build as bundle } from 'esbuild';
2
2
  import { access, readFile, readdir, writeFile, rm } from 'node:fs/promises';
3
3
  import { createRequire } from 'node:module';
4
+ import {transform as transformCss, browserslistToTargets} from 'lightningcss';
5
+ import browserslist from 'browserslist';
4
6
  import { randomUUID } from 'node:crypto';
5
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
8
  import path from 'node:path';
@@ -37,8 +39,18 @@ async function loadConfiguration(root) {
37
39
  return /\.(?:js|mjs|cjs)$/.test(filename) ? importConfiguration(filename) : JSON.parse(await readFile(filename, 'utf8'));
38
40
  }
39
41
 
40
- async function createProcessor(root) {
42
+ async function createProcessor(root, engine) {
41
43
  const config = await loadConfiguration(root);
44
+ if (config === undefined && engine !== 'postcss') {
45
+ const targets=browserslistToTargets(browserslist(undefined,{path:root}));
46
+ return {async process(css,options){
47
+ const result=transformCss({filename:options.from,code:Buffer.from(css),targets,minify:false,sourceMap:!!options.map,
48
+ ...(options.map?.prev ? {inputSourceMap:JSON.stringify(options.map.prev)} : {}),drafts:{customMedia:true}});
49
+ // Preserve source origins for Sass URL rebasing and the ICSS extraction
50
+ // below. Custom PostCSS configurations always retain their own pipeline.
51
+ return postcss([]).process(result.code,{...options,map:result.map?{prev:JSON.parse(result.map.toString()),inline:false,annotation:false}:false});
52
+ }};
53
+ }
42
54
  if (config === undefined) return postcss([flexbugs(), presetEnv({ autoprefixer: { flexbox: 'no-2009' }, stage: 3, features: { 'custom-properties': false } })]);
43
55
  if (!config || typeof config !== 'object' || Array.isArray(config) || !config.plugins || typeof config.plugins !== 'object') throw new Error('PostCSS configuration must export an object with a plugins object or array; configuration functions are not supported.');
44
56
  const entries = Array.isArray(config.plugins)
@@ -122,7 +134,7 @@ function packageImporter(require) {
122
134
  }
123
135
 
124
136
  /** Build-only transformations, shared and deduplicated across all three React graphs. */
125
- export function createStyleProcessor({ projectRoot, sassOptions = {} }) {
137
+ export function createStyleProcessor({ projectRoot, sassOptions = {}, cssEngine = 'auto' }) {
126
138
  const root = path.resolve(projectRoot);
127
139
  const require = createRequire(path.join(root, 'package.json'));
128
140
  const cache = new Map();
@@ -156,7 +168,7 @@ export function createStyleProcessor({ projectRoot, sassOptions = {} }) {
156
168
  sourceMap = compiled.sourceMap;
157
169
  for (const url of compiled.loadedUrls) if (url.protocol === 'file:') dependencies.add(fileURLToPath(url));
158
170
  }
159
- processor ??= createProcessor(root);
171
+ processor ??= createProcessor(root,cssEngine);
160
172
  const processed = await (await processor).process(css, { from: file, to: file, map: sourceMap ? { prev: sourceMap, inline: false, annotation: false } : false });
161
173
  for (const message of processed.messages) if (message.type === 'dependency' && message.file) dependencies.add(path.resolve(root, message.file));
162
174
  rebaseReferences(processed.root, file);
@@ -1,23 +1,54 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import {sourceMapsEnabled} from './source-maps.mjs';
3
+ import {AsyncLocalStorage} from 'node:async_hooks';
4
+ import {buildCacheIdentity,privateCacheDirectory,readCacheRecord,writeCacheRecord} from './persistent-cache.mjs';
3
5
 
4
6
  const entries = new Map();
5
7
  const byteLimit = 2 * 1024 * 1024, entryLimit = 256, itemLimit = 128 * 1024;
6
8
  let bytes = 0, hits = 0, misses = 0;
7
- export const transformCacheStats = () => ({entries:entries.size,bytes,hits,misses});
9
+ let diskHits=0,diskWrites=0;
10
+ const persistent=new AsyncLocalStorage();
11
+ export const transformCacheStats = () => ({entries:entries.size,bytes,hits,misses,diskHits,diskWrites});
12
+ export async function withPersistentTransforms(root,{enabled=true,dev=false}={},run){
13
+ if(!enabled||process.env.PRNEXT_BUILD_CACHE==='0')return run();
14
+ let directory,key,record;
15
+ try{directory=await privateCacheDirectory(root,dev?'transforms-dev-v1':'transforms-v1');key=directory&&await buildCacheIdentity(root);record=key&&await readCacheRecord(directory,key);}catch{return run();}
16
+ if(!directory||!key)return run();
17
+ const scope={key,values:new Map(),bytes:0,dirty:false};
18
+ for(const item of Array.isArray(record?.entries)?record.entries.slice(-4096):[]){
19
+ if(!Array.isArray(item)||item.length!==2||!/^[a-f0-9]{64}$/.test(item[0])||typeof item[1]!=='string')continue;
20
+ const size=Buffer.byteLength(item[1]);if(size>128*1024||scope.bytes+size>8*1024*1024)continue;
21
+ scope.values.set(item[0],item[1]);scope.bytes+=size;
22
+ }
23
+ const result=await persistent.run(scope,run);
24
+ if(scope.dirty&&await writeCacheRecord(directory,key,{entries:[...scope.values]}))diskWrites++;
25
+ return result;
26
+ }
27
+ function identity(kind,source,settings){return createHash('sha256').update(persistent.getStore()?.key||'memory').update(JSON.stringify([kind,settings,sourceMapsEnabled()])).update('\0').update(source).digest('hex');}
28
+ function diskValue(key){const scope=persistent.getStore();if(!scope?.values.has(key))return;const value=scope.values.get(key);scope.values.delete(key);scope.values.set(key,value);diskHits++;hits++;return value;}
29
+ function remember(key,output){
30
+ const scope=persistent.getStore();if(!scope||typeof output!=='string')return;
31
+ const size=Buffer.byteLength(output);if(size>128*1024)return;
32
+ const previous=scope.values.get(key);if(previous!==undefined){scope.bytes-=Buffer.byteLength(previous);scope.values.delete(key);}
33
+ while(scope.values.size&&(scope.values.size>=4096||scope.bytes+size>8*1024*1024)){const [old,value]=scope.values.entries().next().value;scope.values.delete(old);scope.bytes-=Buffer.byteLength(value);}
34
+ scope.values.set(key,output);scope.bytes+=size;scope.dirty=true;
35
+ }
8
36
 
9
37
  // Only pure string transformations belong here. No AST, input source, plugin
10
38
  // closure or mutable build metadata survives in the cache. Include all options
11
39
  // affecting output in settings, including build identity and target mode.
12
40
  export function cachedTransform(kind, source, settings, transform) {
13
- const key = createHash('sha256').update(JSON.stringify([kind,settings,sourceMapsEnabled()])).update('\0').update(source).digest('hex');
41
+ const key = identity(kind,source,settings);
14
42
  const found = entries.get(key);
15
43
  if (found) {
16
44
  hits++; entries.delete(key); entries.set(key,found);
45
+ if(persistent.getStore()&&!persistent.getStore().values.has(key))remember(key,found.output);
17
46
  return found.output;
18
47
  }
48
+ const saved=diskValue(key);if(saved!==undefined)return saved;
19
49
  misses++;
20
50
  const output = transform();
51
+ remember(key,output);
21
52
  // UTF-16 is an upper bound for the retained string payload. Metadata adds
22
53
  // engine overhead; the entry count also bounds that overhead.
23
54
  const size = output.length * 2 + key.length * 2;
@@ -33,11 +64,13 @@ export function cachedTransform(kind, source, settings, transform) {
33
64
 
34
65
  /** Async transforms share the same payload and entry budget as synchronous ones. */
35
66
  export async function cachedAsyncTransform(kind, source, settings, transform) {
36
- const key = createHash('sha256').update(JSON.stringify([kind,settings,sourceMapsEnabled()])).update('\0').update(source).digest('hex');
67
+ const key = identity(kind,source,settings);
37
68
  const found = entries.get(key);
38
- if (found) { hits++; entries.delete(key); entries.set(key,found); return found.output; }
69
+ if (found) { hits++; entries.delete(key); entries.set(key,found); if(persistent.getStore()&&!persistent.getStore().values.has(key))remember(key,found.output);return found.output; }
70
+ const saved=diskValue(key);if(saved!==undefined)return saved;
39
71
  misses++;
40
72
  const output = await transform();
73
+ remember(key,output);
41
74
  const size = output.length * 2 + key.length * 2;
42
75
  if (size <= itemLimit) {
43
76
  if (entries.has(key)) {bytes -= entries.get(key).size;entries.delete(key);}
@@ -0,0 +1,41 @@
1
+ import path from 'node:path';
2
+ import { createRequire } from 'node:module';
3
+ import { loadProjectConfig } from './config.mjs';
4
+ import { scanProject } from './scan.mjs';
5
+ import { writeTypedRoutes } from './typed-routes.mjs';
6
+ import { checkTypeScript, projectTsconfig } from './typescript.mjs';
7
+ import { snapshotEnvConfig } from '../runtime/env.mjs';
8
+
9
+ /** Generate/check from route discovery only; no bundler, native server or install. */
10
+ export async function runTypeCommand(directory, { check = false } = {}) {
11
+ const root = path.resolve(directory);
12
+ const report = { version: 1, root, command: check ? 'typecheck' : 'typegen', ok: false, typedRoutes: false,
13
+ tsconfig: null, routes: 0, updated: [], configUpdates: [], notes: [], errors: [] };
14
+ const restore = snapshotEnvConfig();
15
+ try {
16
+ const config = await loadProjectConfig(root);
17
+ const tsconfig = await projectTsconfig(root, config);
18
+ if (!tsconfig) throw new Error('Type commands require an existing tsconfig.json, or typescript.tsconfigPath pointing to an existing configuration.');
19
+ const resolve = createRequire(path.join(root, 'package.json'));
20
+ try { resolve.resolve('typescript'); }
21
+ catch { throw new Error('Install typescript as a dev dependency in the application before running typegen or typecheck.'); }
22
+ report.tsconfig = path.relative(root, tsconfig).replaceAll(path.sep, '/');
23
+ // Explicit type commands opt into route generation unless the application
24
+ // explicitly disables it. A clean checkout gets fresh declarations first.
25
+ report.typedRoutes = config.typedRoutes !== false;
26
+ const project = await scanProject(root, { basePath: config.basePath, pageExtensions: config.pageExtensions, i18n: config.i18n });
27
+ report.routes = project.routes.filter(route => route.kind === 'page' && !route.internal && !route.errorStatus).length;
28
+ const updates = await writeTypedRoutes(root, project, { ...config, typedRoutes: report.typedRoutes });
29
+ report.updated = (updates || []).map(file => path.relative(root, file).replaceAll(path.sep, '/')).sort();
30
+ report.configUpdates = report.updated.filter(file => file === report.tsconfig || file === 'prnext-env.d.ts');
31
+ if (report.configUpdates.length) report.notes.push('Updated the selected tsconfig roots/Next type aliases and/or prnext-env.d.ts to include generated declarations. Existing JSONC comments and inherited settings are preserved.');
32
+ if (!report.typedRoutes) report.notes.push('Route declarations are disabled by typedRoutes: false; application TypeScript is still checked by typecheck.');
33
+ if (check) {
34
+ await checkTypeScript(root, config, { tsconfig, force: true });
35
+ if (config.typescript?.ignoreBuildErrors) report.notes.push('typecheck checks errors even when typescript.ignoreBuildErrors is enabled for builds.');
36
+ }
37
+ report.ok = true;
38
+ } catch (error) { report.errors.push(error.message); }
39
+ finally { restore(); }
40
+ return report;
41
+ }
@@ -0,0 +1,91 @@
1
+ import {readFile,writeFile,mkdir,rename,lstat} from 'node:fs/promises';
2
+ import {createRequire} from 'node:module';
3
+ import {randomUUID} from 'node:crypto';
4
+ import path from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+
7
+ const marker='// Generated by PRNext: typed route registry. Do not edit.';
8
+ async function changed(file,contents,updates){
9
+ const info=await lstat(file).catch(error=>{if(error.code!=='ENOENT')throw error;});
10
+ if(info&&(!info.isFile()||info.isSymbolicLink()))throw Error('Typed routes cannot replace a non-regular file: '+file);
11
+ if(await readFile(file,'utf8').catch(()=>null)===contents)return;
12
+ const temporary=file+'.'+randomUUID()+'.tmp';try{await writeFile(temporary,contents,{mode:0o600,flag:'wx'});await rename(temporary,file);updates?.add(file);}finally{const {rm}=await import('node:fs/promises');await rm(temporary,{force:true});}
13
+ }
14
+ export function routeDeclarations(project,{enabled=true}={}){
15
+ const patterns=[...new Set(project.routes.filter(route=>route.kind==='page'&&!route.internal&&!route.errorStatus).map(route=>route.pattern))].sort();
16
+ if(patterns.length>10000)throw Error('Typed routes supports at most 10,000 page patterns');
17
+ const fields=patterns.map(pattern=>{
18
+ const segments=pattern.split('/');if(segments.length>33)throw Error('Typed routes supports at most 32 route segments');
19
+ const params=segments.flatMap(part=>{
20
+ const optional=/^\[\[\.\.\.([^\]]+)\]\]$/.exec(part),rest=/^\[\.\.\.([^\]]+)\]$/.exec(part),single=/^\[([^\]]+)\]$/.exec(part);
21
+ const match=optional||rest||single;return match?[`${JSON.stringify(match[1])}${optional?'?':''}: ${single?'string':'string[]'}`]:[];
22
+ });
23
+ return ` ${JSON.stringify(pattern)}: {${params.join('; ')}};`;
24
+ });
25
+ return marker+'\nexport {};\ndeclare global {\n namespace PRNext {\n'+(enabled?' interface RouteConfig { enabled: true }\n':'')+' interface RouteMap {\n'+fields.join('\n')+'\n }\n }\n}\n';
26
+ }
27
+ // Bind missing Next-style imports to the matching PRNext declarations. Existing
28
+ // aliases remain authoritative; inherited aliases retain their resolved targets.
29
+ async function bindTypes(filename,ts,updates){
30
+ const source=await readFile(filename,'utf8');
31
+ const parsed=ts.getParsedCommandLineOfConfigFile(filename,{},{...ts.sys,onUnRecoverableConfigFileDiagnostic:()=>{}});
32
+ const paths=parsed.options.paths||{},missing=['next','next/*'].filter(name=>!Object.hasOwn(paths,name));if(!missing.length)return;
33
+ const ast=ts.parseJsonText(filename,source),object=ast.statements[0].expression;
34
+ const options=object.properties.find(p=>p.name?.text==='compilerOptions')?.initializer;
35
+ const ownPaths=options?.properties?.find(p=>p.name?.text==='paths')?.initializer;
36
+ const base=parsed.options.baseUrl||path.dirname(filename),oldBase=parsed.options.baseUrl||parsed.options.pathsBasePath||path.dirname(filename);
37
+ const relative=file=>{let value=path.relative(base,file).replaceAll(path.sep,'/');return value.startsWith('.')?value:'./'+value;};
38
+ const compat=fileURLToPath(new URL('../compat/',import.meta.url));
39
+ const additions=Object.fromEntries(missing.map(name=>[name,[relative(path.join(compat,name==='next'?'index.d.ts':'*.d.cts'))]]));
40
+ let target,values;
41
+ if(ownPaths){if(!ts.isObjectLiteralExpression(ownPaths))throw Error('tsconfig.compilerOptions.paths must be an object');target=ownPaths;values=additions;}
42
+ else{
43
+ const merged={...Object.fromEntries(Object.entries(paths).map(([name,values])=>[name,values.map(value=>relative(path.resolve(oldBase,value)))])),...additions};
44
+ if(options){if(!ts.isObjectLiteralExpression(options))throw Error('tsconfig.compilerOptions must be an object');target=options;values={paths:merged};}
45
+ else{target=object;values={compilerOptions:{paths:merged}};}
46
+ }
47
+ const position=target.end-1,separator=target.properties.length&&!target.properties.hasTrailingComma?',':'';
48
+ const text=Object.entries(values).map(([name,value])=>JSON.stringify(name)+': '+JSON.stringify(value)).join(',\n ');
49
+ await changed(filename,source.slice(0,position)+separator+'\n '+text+'\n'+source.slice(position),updates);
50
+ }
51
+ /** Include one generated root without replacing JSONC comments or inherited include globs. */
52
+ async function includeTypes(root,config,helper,updates){
53
+ const filename=path.resolve(root,config.typescript?.tsconfigPath||'tsconfig.json');
54
+ const source=await readFile(filename,'utf8').catch(error=>{if(error.code==='ENOENT')return;throw error;});if(source===undefined)return;
55
+ let ts;try{ts=createRequire(path.join(root,'package.json'))('typescript');}catch{throw Error('typedRoutes requires TypeScript installed in the application');}
56
+ const parsed=ts.getParsedCommandLineOfConfigFile(filename,{noEmit:true},{...ts.sys,onUnRecoverableConfigFileDiagnostic:diagnostic=>{throw Error(ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n'));}});
57
+ if(!parsed||parsed.errors.some(error=>error.code!==18003))throw Error('Cannot configure typed routes: invalid tsconfig');
58
+ if(parsed.fileNames.some(name=>path.resolve(name)===helper)){await bindTypes(filename,ts,updates);return;}
59
+ const ast=ts.parseJsonText(filename,source),object=ast.statements[0]?.expression;
60
+ if(!object||!ts.isObjectLiteralExpression(object))throw Error('Cannot add typed route declarations to tsconfig');
61
+ const property=object.properties.find(p=>p.name?.text==='files');
62
+ let edited;
63
+ if(property){
64
+ if(!ts.isArrayLiteralExpression(property.initializer))throw Error('tsconfig.files must be an array');
65
+ const array=property.initializer,position=array.end-1,separator=array.elements.length&&!array.elements.hasTrailingComma?',':'';
66
+ edited=source.slice(0,position)+separator+JSON.stringify(path.relative(path.dirname(filename),helper).replaceAll(path.sep,'/'))+source.slice(position);
67
+ }else{
68
+ // TS resolves inherited files relative to this config in parsed.raw.files.
69
+ const files=[...(parsed.raw.files||[]),path.relative(path.dirname(filename),helper).replaceAll(path.sep,'/')];
70
+ const position=object.end-1,separator=object.properties.length&&!object.properties.hasTrailingComma?',':'';
71
+ edited=source.slice(0,position)+separator+'\n "files": '+JSON.stringify(files)+'\n'+source.slice(position);
72
+ }
73
+ await changed(filename,edited,updates);
74
+ await bindTypes(filename,ts,updates);
75
+ }
76
+ export async function writeTypedRoutes(root,project,config){
77
+ const updates=new Set();
78
+ const directory=path.join(root,'.prnext-types'),file=path.join(directory,'routes.d.ts');
79
+ const info=await lstat(directory).catch(error=>{if(error.code!=='ENOENT')throw error;});
80
+ if(info&&(!info.isDirectory()||info.isSymbolicLink())){if(config.typedRoutes)throw Error('.prnext-types must be a real directory');return;}
81
+ if(!config.typedRoutes){const existing=await readFile(file,'utf8').catch(()=>null);if(existing?.startsWith(marker))await changed(file,routeDeclarations({routes:[]},{enabled:false}),updates);return [...updates];}
82
+ await mkdir(directory,{recursive:true,mode:0o700});
83
+ const previous=await readFile(file,'utf8').catch(error=>{if(error.code!=='ENOENT')throw error;});
84
+ if(previous&&!previous.startsWith(marker))throw Error('Refusing to overwrite a non-generated route registry');
85
+ await changed(file,routeDeclarations(project),updates);
86
+ const helper=path.join(root,'prnext-env.d.ts'),reference='/// <reference path="./.prnext-types/routes.d.ts" />';
87
+ const contents=await readFile(helper,'utf8').catch(error=>{if(error.code==='ENOENT')return '';throw error;});
88
+ if(!contents.includes(reference))await changed(helper,reference+'\n'+contents,updates);
89
+ await includeTypes(root,config,helper,updates);
90
+ return [...updates];
91
+ }