@thomas.f/prnext 0.1.17-alpha → 0.1.19-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.
- package/README.md +27 -868
- package/build/actions.mjs +27 -15
- package/build/analyze.mjs +180 -24
- package/build/app-config.mjs +13 -0
- package/build/app-static.mjs +34 -12
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +122 -46
- package/build/bundle-runtime.mjs +22 -9
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +34 -10
- package/build/config.mjs +34 -3
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +56 -12
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-css-assets.mjs +173 -0
- package/build/dev-css-update.mjs +18 -14
- package/build/dev-dependencies.mjs +1 -1
- package/build/dev-discovery.mjs +3 -3
- package/build/dev-lazy-build.mjs +77 -0
- package/build/dev.mjs +15 -8
- package/build/dynamic.mjs +14 -7
- package/build/edge.mjs +3 -3
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +15 -5
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +90 -28
- package/build/instrumentation.mjs +30 -0
- package/build/lazy-dynamic-imports.mjs +137 -0
- package/build/metadata-routes.mjs +1 -1
- package/build/migrate.mjs +4 -4
- package/build/migration-audit.mjs +19 -31
- package/build/migration-imports.mjs +245 -0
- package/build/migration-scenarios-runner.mjs +73 -0
- package/build/migration-scenarios.mjs +72 -0
- package/build/native-discovery.mjs +65 -5
- package/build/native-packages.mjs +2 -0
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +1 -0
- package/build/persistent-cache.mjs +18 -3
- package/build/precompress.mjs +1 -1
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +19 -9
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +5 -5
- package/build/production-builder.mjs +59 -10
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/standalone.mjs +21 -3
- package/build/static-export.mjs +4 -4
- package/build/styles.mjs +6 -4
- package/build/transform.mjs +1 -1
- package/build/typed-routes.mjs +4 -0
- package/build/typescript-dev.mjs +35 -8
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript.mjs +6 -4
- package/build/webpack-graph.mjs +19 -6
- package/build/worker-memory.mjs +9 -0
- package/cli.mjs +70 -5
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-dynamic.cjs +12 -1
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +15 -3
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +152 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +69 -4
- package/compat/link.cjs +13 -10
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +248 -2
- package/docs/ai/data.md +165 -5
- package/docs/ai/deployment.md +158 -0
- package/docs/ai/quickstart.md +29 -4
- package/docs/ai/routing.md +93 -0
- package/docs/ai/troubleshooting.md +116 -2
- package/package.json +40 -9
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/app-client.mjs +316 -92
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +88 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +7 -2
- package/runtime/cache-request.mjs +2 -1
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-client.mjs +13 -3
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-indicator.mjs +8 -3
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-lazy-tool.mjs +16 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +83 -25
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +74 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/fs.mjs +15 -8
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/lazy-import-client.mjs +38 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +14 -7
- package/runtime/pages-navigation.mjs +48 -63
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +34 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/watch-project.mjs +19 -15
- package/runtime/worker.mjs +70 -26
- package/vendor/nft/LICENSE +7 -0
- package/vendor/nft/README.md +16 -0
- package/vendor/nft/out/analyze.d.ts +8 -0
- package/vendor/nft/out/analyze.js +1216 -0
- package/vendor/nft/out/analyze.js.map +1 -0
- package/vendor/nft/out/cli.d.ts +2 -0
- package/vendor/nft/out/cli.js +94 -0
- package/vendor/nft/out/cli.js.map +1 -0
- package/vendor/nft/out/fs.d.ts +22 -0
- package/vendor/nft/out/fs.js +106 -0
- package/vendor/nft/out/fs.js.map +1 -0
- package/vendor/nft/out/index.d.ts +4 -0
- package/vendor/nft/out/index.js +25 -0
- package/vendor/nft/out/index.js.map +1 -0
- package/vendor/nft/out/node-file-trace.d.ts +41 -0
- package/vendor/nft/out/node-file-trace.js +380 -0
- package/vendor/nft/out/node-file-trace.js.map +1 -0
- package/vendor/nft/out/resolve-dependency.d.ts +6 -0
- package/vendor/nft/out/resolve-dependency.js +427 -0
- package/vendor/nft/out/resolve-dependency.js.map +1 -0
- package/vendor/nft/out/types.d.ts +66 -0
- package/vendor/nft/out/types.js +2 -0
- package/vendor/nft/out/types.js.map +1 -0
- package/vendor/nft/out/utils/ast-helpers.js +49 -0
- package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
- package/vendor/nft/out/utils/binary-locators.js +88 -0
- package/vendor/nft/out/utils/binary-locators.js.map +1 -0
- package/vendor/nft/out/utils/get-package-base.js +30 -0
- package/vendor/nft/out/utils/get-package-base.js.map +1 -0
- package/vendor/nft/out/utils/interop-require.js +24 -0
- package/vendor/nft/out/utils/interop-require.js.map +1 -0
- package/vendor/nft/out/utils/node-version.js +10 -0
- package/vendor/nft/out/utils/node-version.js.map +1 -0
- package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
- package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
- package/vendor/nft/out/utils/special-cases.js +347 -0
- package/vendor/nft/out/utils/special-cases.js.map +1 -0
- package/vendor/nft/out/utils/static-eval.js +579 -0
- package/vendor/nft/out/utils/static-eval.js.map +1 -0
- package/vendor/nft/out/utils/types.js +2 -0
- package/vendor/nft/out/utils/types.js.map +1 -0
- package/vendor/nft/out/utils/wrappers.js +659 -0
- package/vendor/nft/out/utils/wrappers.js.map +1 -0
- package/vendor/nft/package.json +4 -0
- package/vendor/nft/upstream.json +51 -0
package/build/dev-css-update.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import {build as bundle} from 'esbuild';
|
|
2
|
-
import {readFile,writeFile,rm,stat
|
|
2
|
+
import {readFile,writeFile,rm,stat} from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import {randomUUID} from 'node:crypto';
|
|
5
5
|
import {rename} from '../runtime/fs.mjs';
|
|
6
6
|
import {createCssModules} from './css-modules.mjs';
|
|
7
7
|
import {recordDevDependencies,developmentStyleDependencies} from './dev-dependencies.mjs';
|
|
8
8
|
import {writeBuildOutput} from './output-contract.mjs';
|
|
9
|
+
import {createStyleOverlay,StyleOverlayFallback} from './dev-css-assets.mjs';
|
|
9
10
|
|
|
10
11
|
/** Publish only styles when all JS-visible class maps remain identical. */
|
|
11
|
-
export async function tryDevCssUpdate({root,source,destination,manifest,changed,sequence,cssEngine,
|
|
12
|
+
export async function tryDevCssUpdate({root,source,baseSource=source,destination,manifest,changed,sequence,cssEngine,onFallback=()=>{}}){
|
|
12
13
|
const fallback=code=>{onFallback(code);return undefined;};
|
|
13
14
|
if(!changed.length||changed.some(file=>!file.endsWith('.css'))||!manifest.devStyles?.bundles.length||manifest.app?.globalError?.css?.length)return;
|
|
14
15
|
if(!manifest.config.assetBase.startsWith('/'))return fallback('external-asset-base');
|
|
@@ -31,9 +32,10 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
|
|
|
31
32
|
for(const dependency of manifest.devDependencies||[])for(const file of dependency.files)if(file.endsWith('.css'))known.add(path.join(root,file));
|
|
32
33
|
if(changed.some(file=>!known.has(path.join(root,file))))return fallback('unknown-style-dependency');
|
|
33
34
|
if(!selected.length)return fallback('unknown-style-dependency');
|
|
34
|
-
const stage=destination,modules=createCssModules({projectRoot:root,stage,assetBase:manifest.config.assetBase,cssEngine});
|
|
35
|
+
const stage=destination,modules=createCssModules({projectRoot:root,stage,assetBase:manifest.config.assetBase,cssEngine,dev:true});
|
|
36
|
+
let overlay;
|
|
35
37
|
try{
|
|
36
|
-
await
|
|
38
|
+
overlay=await createStyleOverlay({root,source,baseSource,destination,manifest,plans,selected});
|
|
37
39
|
for(const previous of manifest.devStyles.modules.filter(module=>(module.watchFiles||[module.file]).some(file=>changedPaths.has(file)))){
|
|
38
40
|
if(!previous.file.endsWith('.css')){await rm(stage,{recursive:true,force:true});return fallback('stylesheet-language');}
|
|
39
41
|
if((await modules.compile(previous.file)).mapping!==previous.mapping){await rm(stage,{recursive:true,force:true});return fallback('css-module-exports');}
|
|
@@ -42,7 +44,7 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
|
|
|
42
44
|
for(const [name,files]of selected){
|
|
43
45
|
const options={absWorkingDir:root,stdin:{contents:files.map(file=>`@import ${JSON.stringify(file.replace(/^prnext-compiled-css:/,'prnext-css:'))};`).join('\n'),resolveDir:root,loader:'css'},outfile:path.join(stage,'assets',name),bundle:true,write:false,metafile:true,logLevel:'silent',publicPath:manifest.config.assetBase,assetNames:'[name]-[hash]',loader:Object.fromEntries(['.png','.jpg','.jpeg','.svg','.gif','.webp','.avif','.woff','.woff2','.ttf'].map(ext=>[ext,'file'])),plugins:[modules.plugin(true)]};
|
|
44
46
|
const result=await bundle(options);recordDevDependencies(state,options,result);
|
|
45
|
-
|
|
47
|
+
await overlay.recordBundle(name,result);
|
|
46
48
|
}
|
|
47
49
|
const compiled=await modules.mappings();
|
|
48
50
|
if(compiled.some(value=>!manifest.devStyles.modules.some(previous=>previous.file===value.file&&previous.mapping===value.mapping))){await rm(stage,{recursive:true,force:true});return fallback('css-module-exports');}
|
|
@@ -51,7 +53,8 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
|
|
|
51
53
|
for(const module of updated)if(files.includes(module.file))for(const file of module.watchFiles)if(!files.includes(file))files.push(file);
|
|
52
54
|
dependencies.set(name,files);
|
|
53
55
|
}
|
|
54
|
-
const
|
|
56
|
+
const assets=await overlay.finish();
|
|
57
|
+
const buildId=randomUUID(),baseClient=manifest.devStyles.baseClient||manifest.devClient,next={...manifest,buildId,devSequence:sequence,devClient:manifest.config.assetBase+'/dev-manifest-'+buildId+'.json',outputDirectory:stage,devStyles:{baseClient,overlay:assets,bundles:plans,dependencies:[...dependencies],modules:updated}};
|
|
55
58
|
// Keep route invalidation conservative as CSS imports are added or removed.
|
|
56
59
|
// Unchanged routes retain their original dependency arrays.
|
|
57
60
|
next.devDependencies=(manifest.devDependencies||[]).map(route=>{
|
|
@@ -66,18 +69,19 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
|
|
|
66
69
|
}
|
|
67
70
|
return {...route,files:[...files]};
|
|
68
71
|
});
|
|
69
|
-
|
|
72
|
+
let old;
|
|
73
|
+
try{old=JSON.parse(await readFile(path.join(source,'assets',path.basename(manifest.devClient)),'utf8'));}
|
|
74
|
+
catch(error){if(error.code==='ENOENT'||error instanceof SyntaxError)throw new StyleOverlayFallback('missing-style-manifest');throw error;}
|
|
75
|
+
if(!Array.isArray(old?.pages)||old.pages.some(page=>!Array.isArray(page.css))||old.app&&(!Array.isArray(old.app.css)||!Array.isArray(old.app.routes)||old.app.routes.some(route=>!Array.isArray(route.css))))throw new StyleOverlayFallback('invalid-style-manifest');
|
|
70
76
|
old.buildId=buildId;
|
|
71
77
|
const changedNames=new Set(selected.map(([name])=>name));
|
|
72
78
|
const revise=urls=>urls.map(url=>changedNames.has(path.posix.basename(url.split('?')[0]))?url.split('?')[0]+'?v='+buildId:url);
|
|
73
|
-
|
|
79
|
+
const reviseScopes=scopes=>scopes?.map(scope=>({...scope,css:revise(scope.css)}));
|
|
80
|
+
for(const page of old.pages)page.css=revise(page.css);if(old.app){old.app.css=revise(old.app.css);for(const route of old.app.routes||[]){route.css=revise(route.css);if(route.cssScopes)route.cssScopes=reviseScopes(route.cssScopes);}}
|
|
81
|
+
next.routes=next.routes.map(route=>({...route,css:revise(route.css||[]),...(route.cssScopes?{cssScopes:reviseScopes(route.cssScopes)}:{})}));
|
|
74
82
|
await writeFile(path.join(stage,'assets',path.basename(next.devClient)),JSON.stringify(old));
|
|
75
|
-
const protectedNames=new Set([path.basename(baseClient),path.basename(next.devClient)]),history=[];
|
|
76
|
-
for(const name of await readdir(path.join(stage,'assets')))if(/^dev-manifest-.*\.json$/.test(name)&&!protectedNames.has(name)){const info=await stat(path.join(stage,'assets',name));history.push({name,size:info.size,time:info.mtimeMs});}
|
|
77
|
-
history.sort((a,b)=>b.time-a.time);let bytes=0;
|
|
78
|
-
for(let i=0;i<history.length;i++){bytes+=history[i].size;if(i>=32||bytes>4*1024*1024)await rm(path.join(stage,'assets',history[i].name));}
|
|
79
83
|
const temporary=path.join(stage,'manifest.'+buildId+'.tmp');await writeFile(temporary,JSON.stringify(next));await rename(temporary,path.join(stage,'manifest.json'));
|
|
80
|
-
await
|
|
84
|
+
await writeBuildOutput(stage,next);
|
|
81
85
|
return next;
|
|
82
|
-
}catch(error){await rm(stage,{recursive:true,force:true});throw error;}
|
|
86
|
+
}catch(error){if(overlay)await rm(stage,{recursive:true,force:true});if(error instanceof StyleOverlayFallback)return fallback(error.code);throw error;}
|
|
83
87
|
}
|
|
@@ -8,7 +8,7 @@ export function recordDevDependencies(state,options,result){
|
|
|
8
8
|
for(const item of input.imports||[])if(!item.external)imports.add(absolute(item.path));
|
|
9
9
|
state.devDependencies.set(file,imports);
|
|
10
10
|
}
|
|
11
|
-
for(const [name,output]of Object.entries(result.metafile.outputs))if(name.endsWith('.css')){
|
|
11
|
+
for(const [name,output]of Object.entries(result.metafile.outputs))if(name.endsWith('.css')&&path.resolve(options.absWorkingDir||state.root,name).startsWith(path.join(state.stage,'assets')+path.sep)){
|
|
12
12
|
const inputs=Object.keys(output.inputs||{}).filter(file=>file!=='<stdin>'),imported=new Set();
|
|
13
13
|
for(const input of inputs)for(const item of result.metafile.inputs[input]?.imports||[])if(item.kind==='import-rule')imported.add(item.path);
|
|
14
14
|
const roots=inputs.filter(input=>!imported.has(input));
|
package/build/dev-discovery.mjs
CHANGED
|
@@ -19,9 +19,9 @@ export async function discoverDevelopment(root) {
|
|
|
19
19
|
try {
|
|
20
20
|
const config=await loadProjectConfig(root,{dev:true,envMode});
|
|
21
21
|
const project=await scanProject(root,{basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n,devAppFiles:new Set()});
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
const eager
|
|
22
|
+
// Destinations unknown before routing are represented by deferred entries.
|
|
23
|
+
// Their worker asks the compiler only after middleware has run once.
|
|
24
|
+
const eager=false;
|
|
25
25
|
return {project,eager,cssFast:!config.webpack&&!Object.keys(config.turbopack?.rules||{}).length,config:{basePath:config.basePath,assetPrefix:config.assetPrefix,i18n:config.i18n,distDir:config.distDir,
|
|
26
26
|
onDemandEntries:config.onDemandEntries,typescript:config.typescript,typedRoutes:config.typedRoutes,cssEngine:config.cssEngine}};
|
|
27
27
|
} finally {restore();}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdir, readdir, lstat } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { compileApp } from './app.mjs';
|
|
5
|
+
import { configureCompiler } from './compiler.mjs';
|
|
6
|
+
import { createCssModules } from './css-modules.mjs';
|
|
7
|
+
import { createFonts } from './fonts.mjs';
|
|
8
|
+
import { createImages } from './images.mjs';
|
|
9
|
+
import { discardLazyBuild } from '../runtime/dev-lazy-staging.mjs';
|
|
10
|
+
import { loadEnvConfig } from '../runtime/env.mjs';
|
|
11
|
+
|
|
12
|
+
// Only the latest successful ordinary generation per project is reusable.
|
|
13
|
+
// This private compiler-process registry is not part of browser metadata or IPC.
|
|
14
|
+
const contexts = new Map();
|
|
15
|
+
const CONTEXT_LIMIT = 8, FILE_LIMIT = 16384, BYTE_LIMIT = 128 * 1024 * 1024;
|
|
16
|
+
export function forgetLazyBuildContext(root) {
|
|
17
|
+
if (root === undefined) contexts.clear(); else contexts.delete(root);
|
|
18
|
+
}
|
|
19
|
+
export function rememberLazyBuildContext(root, { manifest, config, environment, tsconfig, envMode }) {
|
|
20
|
+
contexts.delete(root);
|
|
21
|
+
if (!manifest.devLazy || !manifest.dev || !config.experimental.lazyDynamicImports) return;
|
|
22
|
+
while (contexts.size >= CONTEXT_LIMIT) contexts.delete(contexts.keys().next().value);
|
|
23
|
+
contexts.set(root, { config, environment, tsconfig, envMode, cacheId: manifest.cacheId,
|
|
24
|
+
buildId: manifest.buildId, session: manifest.devSession,
|
|
25
|
+
manifestConfig: structuredClone(manifest.config) });
|
|
26
|
+
}
|
|
27
|
+
/** Reuse the validated configuration of the running generation, not its routes. */
|
|
28
|
+
export async function tryBuildLazyGraph(root, { base, request, devInputs, devSession,
|
|
29
|
+
devActionSecret, incremental = true } = {}) {
|
|
30
|
+
if (!base || typeof base.cacheId !== 'string' || !request) throw Error('Invalid deferred build generation');
|
|
31
|
+
const context = contexts.get(root);
|
|
32
|
+
// Compiler recycling loses its private context. The caller retains the ordinary
|
|
33
|
+
// build path for this recovery case; it must never guess another generation.
|
|
34
|
+
if (!context || context.cacheId !== base.cacheId || context.session !== devSession) return;
|
|
35
|
+
// Preserve the running generation's env-file mode for supported build-time
|
|
36
|
+
// hooks. The enclosing build() scope restores the caller's env on every exit.
|
|
37
|
+
loadEnvConfig(root,{dev:true,mode:context.envMode,inputs:devInputs});
|
|
38
|
+
const { config, environment, tsconfig } = context;
|
|
39
|
+
const stage = path.join(root, '.prnext-build-' + randomUUID());
|
|
40
|
+
try {
|
|
41
|
+
await mkdir(path.join(stage, '.entries'), { recursive: true });
|
|
42
|
+
const manifest = { version: 1, dev: true, buildId: context.buildId,
|
|
43
|
+
cacheId: context.cacheId, devSession, config: structuredClone(context.manifestConfig),
|
|
44
|
+
routes: [], prerendered: [] };
|
|
45
|
+
const project = { root, routes: [] };
|
|
46
|
+
configureCompiler({ config, root, stage, buildId: context.buildId, dev: true,
|
|
47
|
+
incremental, devInputs, tsconfig, assetBase: manifest.config.assetBase });
|
|
48
|
+
const fonts = createFonts({ projectRoot: root, stage, assetBase: manifest.config.assetBase });
|
|
49
|
+
const cssModules = createCssModules({ projectRoot: root, stage,
|
|
50
|
+
assetBase: manifest.config.assetBase, sassOptions: config.sassOptions,
|
|
51
|
+
cssEngine: config.cssEngine, dev: true, config });
|
|
52
|
+
const images = await createImages({ projectRoot: root, stage,
|
|
53
|
+
assetBase: manifest.config.assetBase, config: config.images });
|
|
54
|
+
await compileApp({ project, stage, manifest, cssModules, fonts, images, dev: true,
|
|
55
|
+
devActionSecret, reactCompiler: config.reactCompiler, defineEnvironment: environment,
|
|
56
|
+
moduleResolution: config.turbopack, serverExternalPackages: config.serverExternalPackages,
|
|
57
|
+
lazyDynamicImports: true, devLazyRequest: request, devLazyOnly: true });
|
|
58
|
+
const files = [];
|
|
59
|
+
let bytes = 0;
|
|
60
|
+
// This unpublished stage contains only this deferred graph, never other
|
|
61
|
+
// routes or a copied framework runtime. Publication uses this exact receipt.
|
|
62
|
+
for (const entry of await readdir(path.join(stage, 'assets'), { withFileTypes: true })) {
|
|
63
|
+
if (!entry.isFile() || files.length >= FILE_LIMIT) throw Error('Invalid or excessive deferred output assets');
|
|
64
|
+
const info = await lstat(path.join(stage, 'assets', entry.name));
|
|
65
|
+
bytes += info.size;
|
|
66
|
+
if (!info.isFile() || bytes > BYTE_LIMIT) throw Error('Deferred output exceeds the 128 MiB generation budget');
|
|
67
|
+
files.push({ name: entry.name, bytes: info.size });
|
|
68
|
+
}
|
|
69
|
+
files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
70
|
+
return { ...manifest, outputDirectory: stage, devLazyTemporary: true,
|
|
71
|
+
devLazyAssets: { version: 1, baseCacheId: context.cacheId, files } };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
try { await discardLazyBuild(root, { outputDirectory: stage, devLazyTemporary: true }); }
|
|
74
|
+
catch (cleanupError) { error.cleanupError = cleanupError; error.message += '; deferred compiler staging cleanup also failed'; }
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
package/build/dev.mjs
CHANGED
|
@@ -5,7 +5,8 @@ import { createHash } from 'node:crypto';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { cachedAsyncTransform } from './transform-cache.mjs';
|
|
8
|
-
import {inlineSourceMap} from './source-maps.mjs';
|
|
8
|
+
import {inlineSourceMap,extractSourceMap} from './source-maps.mjs';
|
|
9
|
+
import {TraceMap,originalPositionFor} from '@jridgewell/trace-mapping';
|
|
9
10
|
|
|
10
11
|
const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
11
12
|
export const devClientFile = path.join(packageRoot, 'runtime/dev-client.mjs');
|
|
@@ -25,10 +26,14 @@ function namedDefaultComponents({types:t}){
|
|
|
25
26
|
}
|
|
26
27
|
}}}};
|
|
27
28
|
}
|
|
28
|
-
function sourceElements(id){return ({types:t})=>({visitor:{JSXOpeningElement(node){
|
|
29
|
+
function sourceElements(id,trace){return ({types:t})=>({visitor:{JSXOpeningElement(node){
|
|
29
30
|
const name=node.node.name;
|
|
30
31
|
if(name.type!=='JSXIdentifier'||!/^[a-z]/.test(name.name)||node.node.attributes.some(attribute=>attribute.type==='JSXAttribute'&&attribute.name.name==='data-prnext-source'))return;
|
|
31
|
-
|
|
32
|
+
let start=node.node.loc?.start;if(!start)return;
|
|
33
|
+
// React Compiler and custom loaders may move JSX before instrumentation.
|
|
34
|
+
// Follow their map back to the editable source; generated, unmapped elements
|
|
35
|
+
// must not send the inspector to an invented line in the user's component.
|
|
36
|
+
if(trace){const original=originalPositionFor(trace,start);if(original.line==null||original.column==null)return;start=original;}
|
|
32
37
|
node.node.attributes.push(t.jsxAttribute(t.jsxIdentifier('data-prnext-source'),t.stringLiteral(`${id}:${start.line}:${start.column+1}`)));
|
|
33
38
|
}}});}
|
|
34
39
|
|
|
@@ -41,8 +46,9 @@ export function createSourceLocationTransform(projectRoot){
|
|
|
41
46
|
const root=filename.startsWith(projectRoot+path.sep)?projectRoot:await canonicalRoot;
|
|
42
47
|
if(!filename.startsWith(root+path.sep))return source;
|
|
43
48
|
const id=path.relative(root,filename).split(path.sep).join('/');
|
|
44
|
-
return cachedAsyncTransform('dev-source-elements',source,[root,filename,'
|
|
45
|
-
const
|
|
49
|
+
return cachedAsyncTransform('dev-source-elements',source,[root,filename,'v2-original-locations'],async()=>{
|
|
50
|
+
const input=extractSourceMap(source),trace=input.map?new TraceMap(input.map):undefined;
|
|
51
|
+
const result=await transformAsync(source,{filename,babelrc:false,configFile:false,sourceMaps:true,sourceFileName:filename,sourceType:'unambiguous',parserOpts:{plugins:['jsx',...(/\.[cm]?tsx?$/.test(filename)?['typescript']:[])]},plugins:[sourceElements(id,trace)]});
|
|
46
52
|
return inlineSourceMap(result.code,result.map);
|
|
47
53
|
});
|
|
48
54
|
};
|
|
@@ -96,12 +102,13 @@ export function createRefreshTransform(projectRoot, { generation,forceGeneration
|
|
|
96
102
|
const root = filename.startsWith(projectRoot + path.sep) ? projectRoot : await canonicalRoot;
|
|
97
103
|
if (!filename.startsWith(root + path.sep)) return source;
|
|
98
104
|
const hash = createHash('sha256').update(source).digest('hex');
|
|
99
|
-
const transformed = await cachedAsyncTransform('fast-refresh',source,[projectRoot,filename,'source-elements-
|
|
105
|
+
const transformed = await cachedAsyncTransform('fast-refresh',source,[projectRoot,filename,'source-elements-v3-original-locations'],async () => {
|
|
100
106
|
const id = path.relative(root, filename).replaceAll(path.sep, '/');
|
|
107
|
+
const input=extractSourceMap(source),trace=input.map?new TraceMap(input.map):undefined;
|
|
101
108
|
const result = await transformAsync(source, { filename, babelrc: false, configFile: false,
|
|
102
109
|
ast: true, sourceMaps: true, sourceFileName:filename, sourceType: 'unambiguous',
|
|
103
|
-
parserOpts: { plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] },
|
|
104
|
-
plugins: [namedDefaultComponents,[refresh, { skipEnvCheck: true, refreshReg: '__prnextRefreshReg', refreshSig: '__prnextRefreshSig' }],sourceElements(id)],
|
|
110
|
+
parserOpts: { plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] },
|
|
111
|
+
plugins: [namedDefaultComponents,[refresh, { skipEnvCheck: true, refreshReg: '__prnextRefreshReg', refreshSig: '__prnextRefreshSig' }],sourceElements(id,trace)],
|
|
105
112
|
});
|
|
106
113
|
// An ESM import changes webpack's interpretation of module.exports.
|
|
107
114
|
// Keep script/CommonJS inputs in their original module format.
|
package/build/dynamic.mjs
CHANGED
|
@@ -59,17 +59,19 @@ function loaderBody(loader) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/** Add shared hydration IDs without resolving, evaluating or eagerly importing loaders. */
|
|
62
|
-
export function transformDynamicImports(source, filename, { projectRoot, mode = 'browser' } = {}) {
|
|
62
|
+
export function transformDynamicImports(source, filename, { projectRoot, mode = 'browser',dev=false } = {}) {
|
|
63
63
|
if (![...sources].some(name => source.includes(name))) return source;
|
|
64
64
|
projectRoot ||= process.cwd();
|
|
65
|
-
return cachedTransform('dynamic',source,[filename,projectRoot,mode],()=>transformDynamicImportsUncached(source,filename,{projectRoot,mode}));
|
|
65
|
+
return cachedTransform('dynamic',source,[filename,projectRoot,mode,dev],()=>transformDynamicImportsUncached(source,filename,{projectRoot,mode,dev}));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function transformDynamicImportsUncached(source, filename, {projectRoot,mode}) {
|
|
68
|
+
function transformDynamicImportsUncached(source, filename, {projectRoot,mode,dev}) {
|
|
69
69
|
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: filename,
|
|
70
|
-
plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] });
|
|
70
|
+
plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] });
|
|
71
71
|
const relative = path.relative(projectRoot || process.cwd(), filename).replaceAll(path.sep, '/');
|
|
72
|
+
const developmentRevision=dev&&mode==='browser'?createHash('sha256').update(generate(ast,{compact:true,comments:false}).code).digest('hex'):undefined;
|
|
72
73
|
let changed = false;
|
|
74
|
+
let ordinal=0;
|
|
73
75
|
traverse(ast, {
|
|
74
76
|
CallExpression(call) {
|
|
75
77
|
let callee = call.get('callee');
|
|
@@ -105,6 +107,10 @@ function transformDynamicImportsUncached(source, filename, {projectRoot,mode}) {
|
|
|
105
107
|
const generated = t.objectProperty(t.identifier('loadableGenerated'), t.objectExpression([
|
|
106
108
|
t.objectProperty(t.identifier('modules'), t.arrayExpression(ids.map(id => t.stringLiteral(id)))),
|
|
107
109
|
]));
|
|
110
|
+
const statement=call.findParent(value=>value.isStatement());
|
|
111
|
+
const declaration=statement?.parentPath?.isExportNamedDeclaration()?statement.parentPath:statement;
|
|
112
|
+
const stableModuleCall=call.scope===call.scope.getProgramParent()&&declaration?.parentPath?.isProgram()&&!t.isLoop(declaration.node);
|
|
113
|
+
const identity=developmentRevision&&stableModuleCall?t.objectProperty(t.identifier('__prnextDevIdentity'),t.stringLiteral(createHash('sha256').update(relative+'\0'+ordinal+++'\0'+developmentRevision).digest('hex').slice(0,24))):undefined;
|
|
108
114
|
|
|
109
115
|
if (noSSR && mode !== 'browser') {
|
|
110
116
|
const empty = t.arrowFunctionExpression([], t.nullLiteral(), true);
|
|
@@ -115,9 +121,10 @@ function transformDynamicImportsUncached(source, filename, {projectRoot,mode}) {
|
|
|
115
121
|
loader.replaceWith(t.arrowFunctionExpression([], loader.node));
|
|
116
122
|
}
|
|
117
123
|
const target = secondOptions || firstOptions;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
else
|
|
124
|
+
const fields=[generated,...(identity?[identity]:[])];
|
|
125
|
+
if (target?.isObjectExpression()) target.node.properties.push(...fields);
|
|
126
|
+
else if (target?.node) target.replaceWith(t.objectExpression([t.spreadElement(target.node), ...fields]));
|
|
127
|
+
else call.node.arguments.push(t.objectExpression(fields));
|
|
121
128
|
changed = true;
|
|
122
129
|
},
|
|
123
130
|
});
|
package/build/edge.mjs
CHANGED
|
@@ -60,7 +60,7 @@ function validateUncached(source,file,reactMode) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/** Bundle the entire application graph as Web ESM before entering the V8 realm. */
|
|
63
|
-
export async function compileEdge({ file, outfile, projectRoot, dev = false, defineEnvironment = {}, defaultExport = false, reactMode, plugins = [], transformSource }) {
|
|
63
|
+
export async function compileEdge({ file, outfile, projectRoot, dev = false, defineEnvironment = {}, defaultExport = false, reactMode, plugins = [], transformSource, transformTypedHelpers = false }) {
|
|
64
64
|
const runtime = path.resolve(path.dirname(outfile), '../runtime');
|
|
65
65
|
await mkdir(runtime, { recursive: true });
|
|
66
66
|
try { await access(path.join(runtime, 'edge-vm.cjs')); }
|
|
@@ -85,7 +85,7 @@ export async function compileEdge({ file, outfile, projectRoot, dev = false, def
|
|
|
85
85
|
mainFields: ['browser', 'module', 'main'], logLevel: 'silent', metafile: true,
|
|
86
86
|
define: { 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), ...defineEnvironment, 'process.env.NEXT_RUNTIME': '"edge"' },
|
|
87
87
|
plugins: [...plugins, { name: 'prnext-edge', setup(builder) {
|
|
88
|
-
applicationEntry(builder,readCompilerFile);
|
|
88
|
+
applicationEntry(builder,readCompilerFile,{transformTypedHelpers});
|
|
89
89
|
builder.onResolve({ filter: /.*/ }, args => {
|
|
90
90
|
if (builtin.has(args.path) || args.path.startsWith('node:')) return { errors: [{ text: `Node.js module ${args.path} cannot run in the Edge Runtime (${args.importer}).` }] };
|
|
91
91
|
if (reactMode && ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom', 'react-server-dom-webpack/server', 'react-server-dom-webpack/server.node'].includes(args.path)) return { path: args.path, namespace: 'prnext-edge-react' };
|
|
@@ -112,7 +112,7 @@ export async function compileEdge({ file, outfile, projectRoot, dev = false, def
|
|
|
112
112
|
: names[args.path].map(name => `export const ${name}=globalThis.__PRNEXT_EDGE_HOST.${args.path}.${name};`).join('\n') }));
|
|
113
113
|
builder.onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: 'file' }, async args => {
|
|
114
114
|
let source = args.prnextSource ?? await readFile(args.path, 'utf8');
|
|
115
|
-
if (transformSource) source = await transformSource(source, args.path, builder);
|
|
115
|
+
if (transformSource) source = await transformSource(source, args.path, builder, {loaderOutput:args.prnextSource!==undefined});
|
|
116
116
|
if (!trusted.has(args.path)) source = validate(source, args.path, reactMode);
|
|
117
117
|
return { contents: source, loader: /\.[cm]?tsx?$/.test(args.path) ? 'tsx' : 'jsx', resolveDir: path.dirname(args.path) };
|
|
118
118
|
});
|
package/build/fonts.mjs
CHANGED
|
@@ -51,7 +51,7 @@ export function createFonts({ projectRoot, stage, assetBase }) {
|
|
|
51
51
|
}
|
|
52
52
|
async function transformSource(source, file) {
|
|
53
53
|
if (/(?:^|[/\\])pages[/\\]_document\.[cm]?[jt]sx?$/.test(file)) throw new Error('next/font cannot be used in pages/_document. Define fonts in pages/_app or a page component.');
|
|
54
|
-
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.tsx?$/.test(file) ? ['typescript'] : [])] });
|
|
54
|
+
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(file) ? ['typescript'] : [])] });
|
|
55
55
|
let program;
|
|
56
56
|
traverse(ast, { Program(value) { program = value; value.stop(); } });
|
|
57
57
|
const imports = [], calls = [];
|
|
@@ -4,6 +4,7 @@ import {readFile} from 'node:fs/promises';
|
|
|
4
4
|
import {analyzeSource} from './source-analysis.mjs';
|
|
5
5
|
import {cachedTransform} from './transform-cache.mjs';
|
|
6
6
|
const adapters = new Map([
|
|
7
|
+
['cache/redis','redis-cache'],
|
|
7
8
|
['dist/shared/lib/router-context.shared-runtime','next-router-context'],
|
|
8
9
|
['dist/shared/lib/router-context','next-router-context'],
|
|
9
10
|
['dist/shared/lib/app-router-context.shared-runtime','next-app-router-context'],
|
|
@@ -23,33 +24,42 @@ export function frameworkImportName(specifier) {
|
|
|
23
24
|
|
|
24
25
|
// Application graphs must not traverse the programmatic build API's native tools.
|
|
25
26
|
// Outside a compiled application, the package root retains its full Node API.
|
|
26
|
-
export function applicationEntry(builder,readSource=(file,override)=>override??readFile(file,'utf8')){
|
|
27
|
-
//
|
|
27
|
+
export function applicationEntry(builder,readSource=(file,override)=>override??readFile(file,'utf8'),{transformTypedHelpers=false}={}){
|
|
28
|
+
// Server/client module boundaries in .mts/.cts are not supported. Never
|
|
28
29
|
// silently bundle a server implementation or misclassify a client boundary.
|
|
29
30
|
builder.onLoad({filter:/\.[cm]ts$/i,namespace:'file'},async args=>{
|
|
30
31
|
// Loader bridges dispatch framework callbacks independently of their filters.
|
|
31
32
|
if(!/\.[cm]ts$/i.test(args.path))return;
|
|
32
33
|
const source=await readSource(args.path,args.prnextSource);
|
|
33
34
|
const original=args.prnextSource===undefined?source:await readSource(args.path);
|
|
35
|
+
let nativeDecorators=false;
|
|
34
36
|
// A loader may remove a directive as well as introduce one. Both versions
|
|
35
37
|
// must be safe before its output can become an ordinary browser module.
|
|
36
38
|
for(const candidate of new Set([original,source])){
|
|
37
|
-
const unsupported=cachedTransform('unsupported-framework-directives',candidate,[args.path,'
|
|
39
|
+
const unsupported=cachedTransform('unsupported-framework-directives',candidate,[args.path,'v4-native-decorators'],()=>{
|
|
38
40
|
const analysis=analyzeSource(candidate,args.path);
|
|
39
41
|
if(analysis.frameworkBoundary)return 'yes';
|
|
40
|
-
const pending=[analysis.program];
|
|
42
|
+
const pending=[analysis.program];let decorators=false;
|
|
41
43
|
while(pending.length){
|
|
42
44
|
const node=pending.pop();
|
|
43
45
|
if(node.type==='ExpressionStatement'&&node.directive==='use server')return 'yes';
|
|
46
|
+
if(node.type==='Decorator'||node.decorators?.length)decorators=true;
|
|
44
47
|
for(const value of Object.values(node)){
|
|
45
48
|
if(Array.isArray(value)){for(const item of value)if(item?.type)pending.push(item);}
|
|
46
49
|
else if(value&&typeof value==='object'&&value.type)pending.push(value);
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
|
-
return 'no';
|
|
52
|
+
return decorators?'decorated':'no';
|
|
50
53
|
});
|
|
51
54
|
if(unsupported==='yes')throw new Error(`React server/client directives are not supported in ${args.path}. Rename this file to .ts or .tsx.`);
|
|
55
|
+
nativeDecorators ||= unsupported==='decorated';
|
|
52
56
|
}
|
|
57
|
+
// Compiler-enabled render graphs can transform ordinary typed helpers after
|
|
58
|
+
// this boundary check. Other consumers keep the historical esbuild path.
|
|
59
|
+
// Decorated typed helpers already have a native esbuild implementation.
|
|
60
|
+
// Keep that path instead of widening the syntax accepted by every Babel
|
|
61
|
+
// transform, while their importing React components can still be compiled.
|
|
62
|
+
if(transformTypedHelpers && !nativeDecorators)return;
|
|
53
63
|
return {contents:source,loader:args.prnextSource===undefined?'ts':'tsx',resolveDir:dirname(args.path)};
|
|
54
64
|
});
|
|
55
65
|
builder.onResolve({filter:/^(?:prnext|@thomas\.f\/prnext)(?:\/route(?:\.js)?)?$/},()=>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {assertHtmlOnlyDocument} from '../runtime/html-only.mjs';
|
|
2
|
+
const MAX_PATTERNS = 128;
|
|
3
|
+
|
|
4
|
+
export function validateHtmlOnly(value) {
|
|
5
|
+
if (value === undefined) return [];
|
|
6
|
+
if (!Array.isArray(value) || value.length > MAX_PATTERNS) throw new TypeError('htmlOnly must be an array of at most 128 App route patterns.');
|
|
7
|
+
for (const pattern of value) {
|
|
8
|
+
if (typeof pattern !== 'string' || pattern.length > 512 || !pattern.startsWith('/') || pattern.startsWith('//') ||
|
|
9
|
+
/[\s\\?#\u0000-\u001f\u007f]/.test(pattern) || /%(?![\da-f]{2})/i.test(pattern)) throw new TypeError('htmlOnly entries must be clean absolute route patterns without queries, fragments, whitespace or backslashes.');
|
|
10
|
+
const pathname = pattern.endsWith('/**') ? pattern.slice(0, -3) || '/' : pattern;
|
|
11
|
+
if (pathname.includes('*') || (pathname !== '/' && pathname.endsWith('/')) || pathname.slice(1).includes('//')) throw new TypeError('htmlOnly supports exact registered route patterns and a final /** subtree suffix only.');
|
|
12
|
+
for (const segment of pathname.split('/').slice(1)) {
|
|
13
|
+
let decoded;
|
|
14
|
+
try { decoded = decodeURIComponent(segment); } catch { throw new TypeError('htmlOnly patterns must use valid URL encoding.'); }
|
|
15
|
+
if (decoded === '.' || decoded === '..' || /[\\/\u0000-\u001f\u007f]/.test(decoded)) throw new TypeError('htmlOnly patterns cannot contain dot segments or encoded path separators.');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...new Set(value)];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function matchesHtmlOnly(pattern, routePattern) {
|
|
22
|
+
if (!pattern.endsWith('/**')) return pattern === routePattern;
|
|
23
|
+
const prefix = pattern.slice(0, -3);
|
|
24
|
+
return !prefix || routePattern === prefix || routePattern.startsWith(prefix + '/');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function selectHtmlOnlyRoutes(routes, patterns, { cacheComponents = false } = {}) {
|
|
28
|
+
const selected = new Set();
|
|
29
|
+
for (const pattern of validateHtmlOnly(patterns)) {
|
|
30
|
+
const matches = routes.filter(route => !route.internal && matchesHtmlOnly(pattern, route.pattern));
|
|
31
|
+
if (!matches.length) throw new Error(`htmlOnly pattern ${JSON.stringify(pattern)} does not match a registered route.`);
|
|
32
|
+
for (const route of matches) {
|
|
33
|
+
if (route.router !== 'app' || route.kind !== 'page') throw new Error(`htmlOnly route ${route.pattern} must be an App Router page; Pages Router and Route Handlers are not supported.`);
|
|
34
|
+
const config = route.cacheConfig || {};
|
|
35
|
+
if (cacheComponents || config.runtime === 'edge' || config.dynamic === 'force-dynamic' || config.forceNoStore || (config.revalidate !== undefined && config.revalidate !== false)) {
|
|
36
|
+
throw new Error(`htmlOnly route ${route.pattern} must be fully prerendered without Edge, Cache Components/PPR, dynamic rendering or ISR.`);
|
|
37
|
+
}
|
|
38
|
+
if (route.parallel || route.interception) throw new Error(`htmlOnly route ${route.pattern} does not support parallel or intercepted routing.`);
|
|
39
|
+
if (route.pattern.includes('[') && config.dynamicParams !== false) throw new Error(`htmlOnly route ${route.pattern} requires dynamicParams: false and complete generateStaticParams results.`);
|
|
40
|
+
selected.add(route.id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return selected;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateHtmlOnlyPrerender(route, rendered) {
|
|
47
|
+
if (!route.htmlOnly) return;
|
|
48
|
+
if (rendered.dynamic || rendered.partial || rendered.revalidate !== false || (rendered.status ?? 200) !== 200) {
|
|
49
|
+
throw new Error(`htmlOnly route ${route.pattern} must produce a complete successful static HTML document without dynamic data, partial rendering, redirects or ISR.`);
|
|
50
|
+
}
|
|
51
|
+
const html = Buffer.isBuffer(rendered.body) ? rendered.body.toString('utf8') : rendered.body;
|
|
52
|
+
assertHtmlOnlyDocument(html, route.pattern);
|
|
53
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { builtinModules } from 'node:module';
|
|
3
|
+
import { analyzeSource } from './source-analysis.mjs';
|
|
4
|
+
import { frameworkImportName, frameworkImportPattern } from './framework-imports.mjs';
|
|
5
|
+
|
|
6
|
+
const builtins = new Set([...builtinModules, ...builtinModules.map(name => 'node:' + name)]);
|
|
7
|
+
const interactive = new Set(['link','image','head','script','dynamic','router','compat/router','next-router-context','next-app-router-context']);
|
|
8
|
+
const namespace = /^prnext-(?:css-mapping|compiled-css|app-empty|edge-empty):/;
|
|
9
|
+
|
|
10
|
+
export function createHtmlOnlyGraph(root) {
|
|
11
|
+
const graph = new Map(), issues = new Map();
|
|
12
|
+
const file = value => path.resolve(root, value.replace(namespace, ''));
|
|
13
|
+
function issue(filename, reason) {
|
|
14
|
+
const key = file(filename);
|
|
15
|
+
if (!issues.has(key)) issues.set(key, reason);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
issue,
|
|
19
|
+
alias(source, target) {
|
|
20
|
+
if (file(source) !== file(target)) graph.set(file(source), [{ path:file(target) }]);
|
|
21
|
+
},
|
|
22
|
+
source(filename, source) {
|
|
23
|
+
const { program, directives, computedRequire, imports } = analyzeSource(source, filename);
|
|
24
|
+
if (directives.includes('use client')) issue(filename, 'a Client Component (use client)');
|
|
25
|
+
if (computedRequire) issue(filename, 'a computed require() whose dependencies cannot be validated');
|
|
26
|
+
for (const dependency of imports) if (frameworkImportPattern.test(dependency.path) && interactive.has(frameworkImportName(dependency.path))) issue(filename, `interactive framework import ${dependency.path}; use ordinary HTML elements instead`);
|
|
27
|
+
const pending = [program];
|
|
28
|
+
while (pending.length) {
|
|
29
|
+
const node = pending.pop();
|
|
30
|
+
if (node.directive === 'use server') issue(filename, 'a Server Action (use server)');
|
|
31
|
+
if (node.type === 'ImportExpression' && typeof node.source?.value !== 'string') issue(filename, 'a computed import() whose dependencies cannot be validated');
|
|
32
|
+
if (node.type === 'JSXOpeningElement' && node.name?.type === 'JSXIdentifier' && ['script','iframe','object','embed'].includes(node.name.name.toLowerCase())) issue(filename, `a <${node.name.name}> element`);
|
|
33
|
+
if (node.type === 'JSXAttribute' && node.name?.type === 'JSXIdentifier' && /^on[A-Z]/.test(node.name.name)) issue(filename, `interactive event property ${node.name.name}`);
|
|
34
|
+
for (const value of Object.values(node)) {
|
|
35
|
+
if (Array.isArray(value)) { for (const child of value) if (child?.type) pending.push(child); }
|
|
36
|
+
else if (value && typeof value === 'object' && value.type) pending.push(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
record(metafile) {
|
|
41
|
+
for (const [name, input] of Object.entries(metafile?.inputs || {})) {
|
|
42
|
+
const key=file(name), dependencies=graph.get(key) || [];
|
|
43
|
+
for (const dependency of input.imports || []) {
|
|
44
|
+
if (!dependency.external) dependencies.push({path:file(dependency.path)});
|
|
45
|
+
else if (!builtins.has(dependency.path) && !/^(?:react|react-dom|react-server-dom-webpack)(?:\/|$)/.test(dependency.path) && !/^\.\.\/compat\/(?:navigation|headers|cache|server|font-local|font-google|og)\.cjs$/.test(dependency.path)) {
|
|
46
|
+
dependencies.push({external:dependency.path});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
graph.set(key,dependencies);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
validate(route) {
|
|
53
|
+
const roots = new Set([route.file, ...Object.values(route.segments || {}).flatMap(segment => Object.values(segment).filter(value => typeof value === 'string' && /\.[cm]?[jt]sx?$/.test(value))), ...(route.metadataFiles || []).map(item => item.file)].filter(Boolean));
|
|
54
|
+
const pending=[...roots].map(file), seen=new Set();
|
|
55
|
+
while (pending.length) {
|
|
56
|
+
const current=pending.pop();
|
|
57
|
+
if (seen.has(current)) continue;
|
|
58
|
+
seen.add(current);
|
|
59
|
+
if (issues.has(current)) throw new Error(`htmlOnly route ${route.pattern} reaches ${issues.get(current)} in ${path.relative(root,current)}.`);
|
|
60
|
+
for (const dependency of graph.get(current) || []) {
|
|
61
|
+
if (dependency.external) throw new Error(`htmlOnly route ${route.pattern} reaches unverified external dependency ${dependency.external} from ${path.relative(root,current)}. Bundle a static server dependency or remove it from this HTML-only route.`);
|
|
62
|
+
pending.push(dependency.path);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|