@thomas.f/prnext 0.1.16-alpha → 0.1.18-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI.md +63 -0
- package/README.md +35 -503
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +6 -1
- package/build/analyze.mjs +340 -0
- package/build/app-config.mjs +20 -8
- package/build/app-static.mjs +41 -14
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +99 -31
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +39 -11
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/check.mjs +56 -11
- 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 +51 -4
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +50 -13
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +65 -10
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +85 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +64 -6
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +5 -4
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +50 -0
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +140 -112
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/instrumentation.mjs +30 -0
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +3 -4
- package/build/migrate.mjs +60 -57
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +112 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +100 -0
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +129 -0
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-export.mjs +4 -4
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +17 -3
- package/build/transform-cache.mjs +37 -4
- package/build/transform.mjs +1 -1
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +95 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +49 -0
- 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-watch.mjs +22 -0
- package/build/typescript.mjs +11 -8
- package/build/webpack-graph.mjs +170 -30
- package/build/worker-memory.mjs +9 -0
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +162 -122
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +12 -2
- 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 +150 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +89 -10
- package/compat/link.cjs +12 -9
- package/compat/link.d.cts +4 -3
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/navigation.d.cts +4 -3
- 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/router.d.cts +4 -3
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/typed-route.d.cts +17 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +275 -0
- package/docs/ai/data.md +288 -0
- package/docs/ai/deployment.md +269 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +171 -0
- package/docs/ai/routing.md +214 -0
- package/docs/ai/troubleshooting.md +155 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +44 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +226 -68
- 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 +100 -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/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +245 -46
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +191 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +34 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +324 -0
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/env.mjs +2 -1
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +50 -0
- package/runtime/flight-tap.mjs +28 -0
- 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/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +24 -8
- package/runtime/pages-navigation.mjs +54 -64
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/preload.mjs +12 -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 +41 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +29 -18
- package/runtime/worker.mjs +102 -24
package/build/css-modules.mjs
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { build as bundle } from 'esbuild';
|
|
2
|
-
import { mkdir, writeFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, writeFile, realpath, readdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { runInNewContext } from 'node:vm';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
|
-
import { createStyleProcessor, stylesheetPattern, moduleStylesheetPattern } from './styles.mjs';
|
|
7
|
+
import { createStyleProcessor, resolveStandardBrowserTargets, stylesheetPattern, moduleStylesheetPattern } from './styles.mjs';
|
|
8
|
+
import {rename} from '../runtime/fs.mjs';
|
|
9
|
+
import {randomUUID} from 'node:crypto';
|
|
10
|
+
import {rm} from 'node:fs/promises';
|
|
11
|
+
import {cachedCompilation} from './compilation-cache.mjs';
|
|
12
|
+
import {compactCssModule,globalCssNames} from './compact-css.mjs';
|
|
8
13
|
|
|
9
14
|
const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
|
|
10
15
|
|
|
@@ -20,18 +25,41 @@ async function resolveCss(esbuild, args) {
|
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
/** One cached CSS compilation supplies identical class maps to the server and the browser. */
|
|
23
|
-
export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/assets', sassOptions }) {
|
|
24
|
-
const cache = new Map();
|
|
25
|
-
|
|
28
|
+
export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/assets', sassOptions, cssEngine, dev = false, config = {} }) {
|
|
29
|
+
const cache = new Map(), compactNames = new Map();
|
|
30
|
+
let targets;
|
|
31
|
+
const browserTargets=()=>targets ??= Promise.resolve().then(()=>resolveStandardBrowserTargets(projectRoot));
|
|
32
|
+
const processor = createStyleProcessor({ projectRoot, sassOptions, cssEngine, browserTargets });
|
|
26
33
|
const hash = file => createHash('sha256').update(path.relative(projectRoot, file).replaceAll(path.sep, '/')).digest('hex').slice(0, 12);
|
|
34
|
+
let eligibility;
|
|
35
|
+
async function cacheEligible() {
|
|
36
|
+
if (dev || cssEngine==='postcss' || config.webpack || Object.keys(config.turbopack?.rules || {}).length || process.env.BROWSERSLIST_CONFIG || process.env.BROWSERSLIST_STATS || /\b(?:extends|stats)\b/i.test(process.env.BROWSERSLIST || '')) return false;
|
|
37
|
+
// A user PostCSS plugin can read arbitrary files, glob directories or use
|
|
38
|
+
// network/time without declaring dependencies. Its output is never cached.
|
|
39
|
+
const names = await readdir(projectRoot);
|
|
40
|
+
if (names.some(name => /^(?:postcss\.config|\.postcssrc)(?:\.|$)/.test(name))) return false;
|
|
41
|
+
try { if (JSON.parse(await readFile(path.join(projectRoot, 'package.json'), 'utf8')).postcss !== undefined) return false; }
|
|
42
|
+
catch (error) { if (error.code !== 'ENOENT') return false; }
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const validMapping = value => {try{const parsed=JSON.parse(value);return parsed&&typeof parsed==='object'&&!Array.isArray(parsed)&&Object.values(parsed).every(value=>typeof value==='string');}catch{return false;}};
|
|
46
|
+
const validResult = value => value && typeof value.mapping === 'string' && value.mapping.length <= 16*1024*1024 && validMapping(value.mapping) && typeof value.css === 'string' && value.css.length <= 16*1024*1024
|
|
47
|
+
&& Array.isArray(value.compactNames) && value.compactNames.length <= 65536 && value.compactNames.every(item=>Array.isArray(item)&&item.length===2&&typeof item[0]==='string'&&item[0].length<=4096&&typeof item[1]==='string'&&/^p[\w-]{10}$/.test(item[1]))
|
|
48
|
+
&& Array.isArray(value.watchFiles) && value.watchFiles.length <= 4096 && value.watchFiles.every(file => typeof file==='string' && path.isAbsolute(file) && !file.includes('\0') && !/\.s[ac]ss$/i.test(file))
|
|
49
|
+
&& Array.isArray(value.assets) && value.assets.length <= 4096 && value.assets.every(asset => asset && typeof asset.name === 'string'
|
|
50
|
+
&& asset.name && asset.name === path.basename(asset.name) && !/[\\/\0<>:"|?*#]/.test(asset.name) && asset.name !== '.' && asset.name !== '..'
|
|
51
|
+
&& typeof asset.data === 'string' && asset.data.length<=24*1024*1024 && asset.data.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(asset.data))
|
|
52
|
+
&& new Set(value.assets.map(asset=>asset.name)).size===value.assets.length && value.assets.reduce((sum,asset)=>sum+asset.data.length,0)<=48*1024*1024;
|
|
27
53
|
|
|
28
54
|
async function compile(file) {
|
|
55
|
+
const dependencies=new Set([file]), globals = new Set();
|
|
29
56
|
const result = await bundle({
|
|
30
57
|
absWorkingDir: projectRoot,
|
|
31
58
|
stdin: { contents: `import styles from ${JSON.stringify(file)}; export default styles;`, resolveDir: projectRoot, loader: 'js' },
|
|
32
59
|
outfile: path.join(stage, '.cssmodules', hash(file) + '.js'),
|
|
33
60
|
bundle: true,
|
|
34
61
|
write: false,
|
|
62
|
+
metafile:true,
|
|
35
63
|
format: 'cjs',
|
|
36
64
|
platform: 'browser',
|
|
37
65
|
logLevel: 'silent',
|
|
@@ -52,10 +80,14 @@ export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/ass
|
|
|
52
80
|
});
|
|
53
81
|
esbuild.onLoad({ filter: /.*/, namespace: 'prnext-local-css' }, async args => {
|
|
54
82
|
const transformed = await processor.process(args.pluginData.source);
|
|
83
|
+
if (!dev) globalCssNames(transformed.css, {local:true}, globals);
|
|
84
|
+
for(const file of transformed.watchFiles)dependencies.add(file);
|
|
55
85
|
return { contents: transformed.css, loader: 'local-css', resolveDir: path.dirname(args.pluginData.source), watchFiles: transformed.watchFiles };
|
|
56
86
|
});
|
|
57
87
|
esbuild.onLoad({ filter: stylesheetPattern, namespace: 'file' }, async args => {
|
|
58
88
|
const transformed = await processor.process(args.path);
|
|
89
|
+
if (!dev) globalCssNames(transformed.css, {local:false}, globals);
|
|
90
|
+
for(const file of transformed.watchFiles)dependencies.add(file);
|
|
59
91
|
return { contents: transformed.css, loader: 'css', resolveDir: path.dirname(args.path), watchFiles: transformed.watchFiles };
|
|
60
92
|
});
|
|
61
93
|
esbuild.onResolve({ filter: /^\// }, args => args.kind === 'url-token' ? { path: args.path, external: true } : undefined);
|
|
@@ -64,26 +96,49 @@ export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/ass
|
|
|
64
96
|
});
|
|
65
97
|
const javascript = result.outputFiles.find(output => output.path.endsWith('.js'));
|
|
66
98
|
const stylesheet = result.outputFiles.find(output => output.path.endsWith('.css'));
|
|
99
|
+
for(const name of Object.keys(result.metafile.inputs))if(name!=='<stdin>'&&(!name.includes(':')||path.isAbsolute(name)))dependencies.add(path.resolve(projectRoot,name));
|
|
67
100
|
if (!javascript || !stylesheet) throw new Error(`Unable to compile CSS Module ${file}.`);
|
|
68
101
|
// This is esbuild's generated export map, whose only inputs are CSS files.
|
|
69
102
|
// The isolated context deliberately has no require, process, network, or filesystem.
|
|
70
103
|
const module = { exports: {} };
|
|
71
104
|
runInNewContext(javascript.text, { module }, { timeout: 1000, filename: 'prnext-css-exports.cjs' });
|
|
72
|
-
const
|
|
73
|
-
|
|
105
|
+
const compact = dev ? {mapping:module.exports.default,css:stylesheet.text,names:[]} : compactCssModule(stylesheet.text,module.exports.default,{globals});
|
|
106
|
+
const mapping = JSON.stringify({ ...compact.mapping, ...(await processor.process(file)).exports });
|
|
107
|
+
const assets = [];
|
|
74
108
|
for (const output of result.outputFiles) {
|
|
75
109
|
if (output === javascript || output === stylesheet) continue;
|
|
76
|
-
|
|
110
|
+
assets.push({name:path.basename(output.path),data:Buffer.from(output.contents).toString('base64')});
|
|
77
111
|
}
|
|
78
|
-
return { mapping, css:
|
|
112
|
+
return { mapping, css: compact.css,compactNames:compact.names,watchFiles:[...dependencies],assets };
|
|
79
113
|
}
|
|
80
114
|
|
|
81
115
|
function get(file) {
|
|
82
|
-
if (!cache.has(file)) cache.set(file,
|
|
116
|
+
if (!cache.has(file)) cache.set(file, (async () => {
|
|
117
|
+
eligibility ??= cacheEligible();
|
|
118
|
+
const enabled = await eligibility && /\.css$/i.test(file);
|
|
119
|
+
const result = await cachedCompilation({root:projectRoot,kind:'css-modules',inputs:[file],excluded:[path.resolve(projectRoot,config.distDir || '.prnext'),...(config.output==='export'?[path.join(projectRoot,'out')]:[])],styleTargets:true,settings:{file,assetBase,cssEngine:cssEngine || 'auto',...(enabled?{browserTargets:await browserTargets()}:{})},
|
|
120
|
+
enabled,reason:dev?'development':'custom-style-pipeline',validate:validResult,
|
|
121
|
+
bytes:value=>Buffer.byteLength(value.mapping)+Buffer.byteLength(value.css)+Buffer.byteLength(JSON.stringify(value.compactNames))+value.assets.reduce((sum,asset)=>sum+Buffer.byteLength(asset.data,'base64'),0),
|
|
122
|
+
produce:async()=>{const value=await compile(file);return {value,dependencies:value.watchFiles};}});
|
|
123
|
+
await mkdir(path.join(stage,'assets'),{recursive:true});
|
|
124
|
+
for(const [original,short] of result.compactNames || []) {
|
|
125
|
+
if(compactNames.has(short)&&compactNames.get(short)!==original)throw new Error('CSS Module compact-name collision across modules.');
|
|
126
|
+
compactNames.set(short,original);
|
|
127
|
+
}
|
|
128
|
+
for (const asset of result.assets) {
|
|
129
|
+
const target=path.join(stage,'assets',asset.name),temporary=target+'.'+randomUUID()+'.tmp';
|
|
130
|
+
try{await writeFile(temporary,Buffer.from(asset.data,'base64'));await rename(temporary,target);}finally{await rm(temporary,{force:true});}
|
|
131
|
+
}
|
|
132
|
+
// Keep the original public result and release replay-only binary payloads.
|
|
133
|
+
// The per-build mapping cache must not retain a base64 copy of every asset.
|
|
134
|
+
return {mapping:result.mapping,css:result.css,watchFiles:result.watchFiles};
|
|
135
|
+
})());
|
|
83
136
|
return cache.get(file);
|
|
84
137
|
}
|
|
85
138
|
|
|
86
139
|
return {
|
|
140
|
+
compile:get,
|
|
141
|
+
async mappings(){return Promise.all([...cache].map(async([file,result])=>({file,mapping:(await result).mapping,watchFiles:(await result).watchFiles})));},
|
|
87
142
|
plugin(browser, { emitStyles = () => true } = {}) {
|
|
88
143
|
return {
|
|
89
144
|
name: 'prnext-shared-css-modules',
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const INDEX = 'deployment-assets.json';
|
|
6
|
+
const MAX_FILES = 1024, MAX_BYTES = 64 * 1024 * 1024, MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
const MAX_AGE = 60 * 60 * 1000, MAX_GENERATIONS = 2;
|
|
8
|
+
// Only build-owned, immutable code/styles. Never JSON, maps, HTML, public/ or server/.
|
|
9
|
+
const safeName = value => typeof value === 'string' && value.length < 240 && /^[a-zA-Z0-9_.-]+-[a-zA-Z0-9]{8,64}\.(?:js|mjs|css|woff2?|ttf|otf|eot|png|jpe?g|avif|webp|gif|svg|ico|wasm)$/.test(value);
|
|
10
|
+
const assetKey = name => process.platform === 'win32' ? name.toLowerCase() : name;
|
|
11
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
12
|
+
async function regular(file, limit) {
|
|
13
|
+
const info = await lstat(file);
|
|
14
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > limit) return;
|
|
15
|
+
return info;
|
|
16
|
+
}
|
|
17
|
+
async function indexAt(directory) {
|
|
18
|
+
try {
|
|
19
|
+
if (!await regular(path.join(directory, INDEX), 1024 * 1024)) return;
|
|
20
|
+
const value = JSON.parse(await readFile(path.join(directory, INDEX), 'utf8'));
|
|
21
|
+
if (value.version !== 1 || !Array.isArray(value.generations) || value.generations.length > 6) return;
|
|
22
|
+
for (const generation of value.generations) {
|
|
23
|
+
if (typeof generation.id !== 'string' || generation.id.length > 128 || !Number.isSafeInteger(generation.createdAt) || !Array.isArray(generation.files) || generation.files.length > MAX_FILES) return;
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const file of generation.files) {
|
|
26
|
+
if (!safeName(file.name) || seen.has(assetKey(file.name)) || !Number.isSafeInteger(file.bytes) || file.bytes < 0 || file.bytes > MAX_FILE_BYTES || !/^[a-f0-9]{64}$/.test(file.hash)) return;
|
|
27
|
+
seen.add(assetKey(file.name));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
} catch (error) { if (!['ENOENT', 'ENOTDIR'].includes(error.code)) { /* Corrupt previous metadata is an optional retention miss. */ } }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Copy only public immutable assets into a stage, before its atomic publication. */
|
|
35
|
+
export async function retainDeploymentAssets({ stage, destination, manifest, policy, now = Date.now() }) {
|
|
36
|
+
if (manifest.dev) return { generations: 0, files: 0, bytes: 0 };
|
|
37
|
+
const maxAge = (policy && policy.maxAge !== undefined ? policy.maxAge * 1000 : MAX_AGE);
|
|
38
|
+
const maxGenerations = policy && policy.maxGenerations !== undefined ? policy.maxGenerations : MAX_GENERATIONS;
|
|
39
|
+
const maxBytes = policy && policy.maxBytes !== undefined ? policy.maxBytes : MAX_BYTES;
|
|
40
|
+
const assets = path.join(stage, 'assets');
|
|
41
|
+
const assetInfo = await lstat(assets);
|
|
42
|
+
if (!assetInfo.isDirectory() || assetInfo.isSymbolicLink()) throw new Error('Deployment assets must be a regular directory');
|
|
43
|
+
const current = { id: manifest.cacheId, createdAt: now, eligible: true, files: [] };
|
|
44
|
+
let indexedBytes = 0;
|
|
45
|
+
for (const name of (await readdir(assets)).sort()) {
|
|
46
|
+
if (!safeName(name)) {
|
|
47
|
+
// Maps/manifests do not participate in executable chunk graphs. Unknown
|
|
48
|
+
// content-addressed media and auxiliary directories make retention unsafe.
|
|
49
|
+
if (/\.(?:map|json|html|txt|gz|br)$/.test(name)) continue;
|
|
50
|
+
current.eligible = false; current.files = []; break;
|
|
51
|
+
}
|
|
52
|
+
const file = path.join(assets, name), info = await regular(file, MAX_FILE_BYTES);
|
|
53
|
+
if (!info || (indexedBytes += info.size) > 256 * 1024 * 1024) { current.eligible = false; current.files = []; break; }
|
|
54
|
+
const bytes = await readFile(file);
|
|
55
|
+
current.files.push({ name, bytes: bytes.length, hash: hash(bytes) });
|
|
56
|
+
if (current.files.length > MAX_FILES) { current.eligible = false; current.files = []; break; }
|
|
57
|
+
}
|
|
58
|
+
const previousRoot = await lstat(path.join(destination, 'assets')).catch(() => null);
|
|
59
|
+
const previous = !current.eligible || policy === false || !previousRoot?.isDirectory() || previousRoot.isSymbolicLink() ? undefined : await indexAt(destination), generations = [current], known = new Map(current.files.map(file => [assetKey(file.name), file]));
|
|
60
|
+
const retained = Object.create(null); let bytes = 0, count = 0;
|
|
61
|
+
for (const generation of previous?.generations || []) {
|
|
62
|
+
if (generation.eligible !== true || generations.length > maxGenerations || generation.id === current.id || generation.createdAt > now || now - generation.createdAt >= maxAge) continue;
|
|
63
|
+
if (generation.files.some(file => { const value = known.get(assetKey(file.name)); return value && (value.hash !== file.hash || value.bytes !== file.bytes); })) continue;
|
|
64
|
+
const candidates = generation.files.filter(file => !known.has(assetKey(file.name)));
|
|
65
|
+
const size = candidates.reduce((sum, file) => sum + file.bytes, 0);
|
|
66
|
+
if (bytes + size > maxBytes || count + candidates.length > MAX_FILES) continue;
|
|
67
|
+
const verified = [];
|
|
68
|
+
for (const file of candidates) {
|
|
69
|
+
const source = path.join(destination, 'assets', file.name);
|
|
70
|
+
try {
|
|
71
|
+
if (!await regular(source, MAX_FILE_BYTES)) continue;
|
|
72
|
+
const contents = await readFile(source);
|
|
73
|
+
if (contents.length !== file.bytes || hash(contents) !== file.hash) continue;
|
|
74
|
+
verified.push({file, contents});
|
|
75
|
+
} catch { /* An unreadable previous asset is an optional retention miss. */ }
|
|
76
|
+
}
|
|
77
|
+
// An incomplete previous generation cannot promise a working chunk graph.
|
|
78
|
+
if (verified.length !== candidates.length) continue;
|
|
79
|
+
for (const {file, contents} of verified) {
|
|
80
|
+
await writeFile(path.join(assets, file.name), contents, {flag:'wx'});
|
|
81
|
+
known.set(assetKey(file.name), file); retained[file.name] = generation.createdAt + maxAge;
|
|
82
|
+
bytes += file.bytes; count++;
|
|
83
|
+
}
|
|
84
|
+
generations.push(generation);
|
|
85
|
+
}
|
|
86
|
+
manifest.config.retainedAssets = retained;
|
|
87
|
+
await writeFile(path.join(stage, INDEX), JSON.stringify({ version: 1, generations }) + '\n');
|
|
88
|
+
return { generations: generations.length - 1, files: count, bytes };
|
|
89
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const defaults=Object.freeze({maxAge:3600,maxGenerations:2,maxBytes:64*1024*1024});
|
|
2
|
+
const limits={maxAge:[1,86400],maxGenerations:[1,5],maxBytes:[1024,256*1024*1024]};
|
|
3
|
+
|
|
4
|
+
/** Retain only bounded, immutable public assets; false revokes all carry-over. */
|
|
5
|
+
export function validateDeploymentRetention(value){
|
|
6
|
+
if(value===false)return false;
|
|
7
|
+
if(value===undefined)return {...defaults};
|
|
8
|
+
if(!value||typeof value!=='object'||Array.isArray(value)||![Object.prototype,null].includes(Object.getPrototypeOf(value)))throw new TypeError('deploymentRetention must be false or an object.');
|
|
9
|
+
const output={...defaults};
|
|
10
|
+
for(const key of Reflect.ownKeys(value)){
|
|
11
|
+
if(!Object.hasOwn(limits,key))throw new TypeError(`deploymentRetention.${String(key)} is not supported.`);
|
|
12
|
+
if(value[key]===undefined)continue;
|
|
13
|
+
const [minimum,maximum]=limits[key];
|
|
14
|
+
if(!Number.isSafeInteger(value[key])||value[key]<minimum||value[key]>maximum)throw new TypeError(`deploymentRetention.${key} must be an integer from ${minimum} to ${maximum}.`);
|
|
15
|
+
output[key]=value[key];
|
|
16
|
+
}
|
|
17
|
+
return output;
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function createCompilerHealth({maxRssMb=768,maxHeapMb=384,maxBuilds=256}={}) {
|
|
2
|
+
let builds=0;const samples=[];
|
|
3
|
+
return ({rss,heapUsed})=>{
|
|
4
|
+
builds++;const rssMb=rss/1024/1024,heapMb=heapUsed/1024/1024;
|
|
5
|
+
samples.push(rssMb);if(samples.length>12)samples.shift();
|
|
6
|
+
const floor=Math.min(...samples),growth=rssMb-floor;
|
|
7
|
+
const reason=rssMb>=maxRssMb?'rss-limit':heapMb>=maxHeapMb?'heap-limit':
|
|
8
|
+
samples.length>=8&&growth>128&&rssMb>floor*1.35?'sustained-growth':builds>=maxBuilds?'session-limit':null;
|
|
9
|
+
return {builds,rssMb:Math.round(rssMb),heapMb:Math.round(heapMb),recycle:reason!==null,reason};
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {build as bundle} from 'esbuild';
|
|
2
|
+
import {readFile,writeFile,rm,stat,readdir} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {randomUUID} from 'node:crypto';
|
|
5
|
+
import {rename} from '../runtime/fs.mjs';
|
|
6
|
+
import {createCssModules} from './css-modules.mjs';
|
|
7
|
+
import {recordDevDependencies,developmentStyleDependencies} from './dev-dependencies.mjs';
|
|
8
|
+
import {writeBuildOutput} from './output-contract.mjs';
|
|
9
|
+
|
|
10
|
+
/** Publish only styles when all JS-visible class maps remain identical. */
|
|
11
|
+
export async function tryDevCssUpdate({root,source,destination,manifest,changed,sequence,cssEngine,snapshot,onFallback=()=>{}}){
|
|
12
|
+
const fallback=code=>{onFallback(code);return undefined;};
|
|
13
|
+
if(!changed.length||changed.some(file=>!file.endsWith('.css'))||!manifest.devStyles?.bundles.length||manifest.app?.globalError?.css?.length)return;
|
|
14
|
+
if(!manifest.config.assetBase.startsWith('/'))return fallback('external-asset-base');
|
|
15
|
+
if(JSON.parse(await readFile(path.join(root,'package.json'),'utf8')).postcss!==undefined)return fallback('css-processor');
|
|
16
|
+
// Custom PostCSS/webpack generators can change JS or emit other files.
|
|
17
|
+
for(const name of ['postcss.config.js','postcss.config.cjs','postcss.config.mjs','postcss.config.json','.postcssrc','.postcssrc.json'])if(await stat(path.join(root,name)).catch(()=>null)){
|
|
18
|
+
if(name!=='postcss.config.json')return fallback('css-processor');
|
|
19
|
+
const value=JSON.parse(await readFile(path.join(root,name),'utf8'));
|
|
20
|
+
if(Object.keys(value).some(key=>key!=='plugins')||!value.plugins||Object.keys(value.plugins).length)return fallback('css-processor');
|
|
21
|
+
}
|
|
22
|
+
const plans=manifest.devStyles.bundles,dependencies=new Map(manifest.devStyles.dependencies||[]);
|
|
23
|
+
// Older manifests lack enough provenance to select bundles safely.
|
|
24
|
+
if(plans.some(([name])=>!dependencies.get(name)?.length))return fallback('missing-style-dependencies');
|
|
25
|
+
const changedPaths=new Set(changed.map(file=>path.resolve(root,file)));
|
|
26
|
+
const selected=plans.filter(([name])=>dependencies.get(name).some(file=>changedPaths.has(file)));
|
|
27
|
+
const known=new Set([...dependencies.values()].flat());
|
|
28
|
+
const allRoots=plans.flatMap(([,files])=>files);
|
|
29
|
+
if(selected.flatMap(([,files])=>files).some(file=>!file.endsWith('.css')||!file.startsWith('prnext-compiled-css:')&&!path.isAbsolute(file)))return fallback('stylesheet-language');
|
|
30
|
+
for(const file of allRoots)known.add(file.replace(/^prnext-compiled-css:/,''));
|
|
31
|
+
for(const dependency of manifest.devDependencies||[])for(const file of dependency.files)if(file.endsWith('.css'))known.add(path.join(root,file));
|
|
32
|
+
if(changed.some(file=>!known.has(path.join(root,file))))return fallback('unknown-style-dependency');
|
|
33
|
+
if(!selected.length)return fallback('unknown-style-dependency');
|
|
34
|
+
const stage=destination,modules=createCssModules({projectRoot:root,stage,assetBase:manifest.config.assetBase,cssEngine,dev:true});
|
|
35
|
+
try{
|
|
36
|
+
await snapshot(source,stage);
|
|
37
|
+
for(const previous of manifest.devStyles.modules.filter(module=>(module.watchFiles||[module.file]).some(file=>changedPaths.has(file)))){
|
|
38
|
+
if(!previous.file.endsWith('.css')){await rm(stage,{recursive:true,force:true});return fallback('stylesheet-language');}
|
|
39
|
+
if((await modules.compile(previous.file)).mapping!==previous.mapping){await rm(stage,{recursive:true,force:true});return fallback('css-module-exports');}
|
|
40
|
+
}
|
|
41
|
+
const state={dev:true,root,stage};
|
|
42
|
+
for(const [name,files]of selected){
|
|
43
|
+
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
|
+
const result=await bundle(options);recordDevDependencies(state,options,result);
|
|
45
|
+
for(const output of result.outputFiles){const temporary=output.path+'.'+randomUUID()+'.tmp';try{await writeFile(temporary,output.contents);await rename(temporary,output.path);}finally{await rm(temporary,{force:true});}}
|
|
46
|
+
}
|
|
47
|
+
const compiled=await modules.mappings();
|
|
48
|
+
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');}
|
|
49
|
+
const updated=manifest.devStyles.modules.map(previous=>compiled.find(value=>value.file===previous.file)||previous);
|
|
50
|
+
for(const [name,files]of developmentStyleDependencies(state)){
|
|
51
|
+
for(const module of updated)if(files.includes(module.file))for(const file of module.watchFiles)if(!files.includes(file))files.push(file);
|
|
52
|
+
dependencies.set(name,files);
|
|
53
|
+
}
|
|
54
|
+
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,bundles:plans,dependencies:[...dependencies],modules:updated}};
|
|
55
|
+
// Keep route invalidation conservative as CSS imports are added or removed.
|
|
56
|
+
// Unchanged routes retain their original dependency arrays.
|
|
57
|
+
next.devDependencies=(manifest.devDependencies||[]).map(route=>{
|
|
58
|
+
const relevant=selected.filter(([name])=>manifest.devStyles.dependencies.find(([key])=>key===name)[1].some(file=>route.files.includes(path.relative(root,file).replaceAll(path.sep,'/'))));
|
|
59
|
+
if(!relevant.length)return route;
|
|
60
|
+
const currentStyleFiles=new Set([...dependencies.values()].flat());
|
|
61
|
+
const removed=new Set(manifest.devStyles.dependencies.flatMap(([,files])=>files).filter(file=>!currentStyleFiles.has(file)).map(file=>path.relative(root,file).replaceAll(path.sep,'/')));
|
|
62
|
+
const files=new Set(route.files.filter(file=>!removed.has(file)));
|
|
63
|
+
for(const[name]of relevant)for(const file of dependencies.get(name)){
|
|
64
|
+
const relative=path.relative(root,file).replaceAll(path.sep,'/');
|
|
65
|
+
if(relative&&!relative.startsWith('../')&&!relative.includes(':'))files.add(relative);
|
|
66
|
+
}
|
|
67
|
+
return {...route,files:[...files]};
|
|
68
|
+
});
|
|
69
|
+
const old=JSON.parse(await readFile(path.join(source,'assets',path.basename(manifest.devClient)),'utf8'));
|
|
70
|
+
old.buildId=buildId;
|
|
71
|
+
const changedNames=new Set(selected.map(([name])=>name));
|
|
72
|
+
const revise=urls=>urls.map(url=>changedNames.has(path.posix.basename(url.split('?')[0]))?url.split('?')[0]+'?v='+buildId:url);
|
|
73
|
+
const reviseScopes=scopes=>scopes?.map(scope=>({...scope,css:revise(scope.css)}));
|
|
74
|
+
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);}}
|
|
75
|
+
next.routes=next.routes.map(route=>({...route,css:revise(route.css||[]),...(route.cssScopes?{cssScopes:reviseScopes(route.cssScopes)}:{})}));
|
|
76
|
+
await writeFile(path.join(stage,'assets',path.basename(next.devClient)),JSON.stringify(old));
|
|
77
|
+
const protectedNames=new Set([path.basename(baseClient),path.basename(next.devClient)]),history=[];
|
|
78
|
+
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});}
|
|
79
|
+
history.sort((a,b)=>b.time-a.time);let bytes=0;
|
|
80
|
+
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));}
|
|
81
|
+
const temporary=path.join(stage,'manifest.'+buildId+'.tmp');await writeFile(temporary,JSON.stringify(next));await rename(temporary,path.join(stage,'manifest.json'));
|
|
82
|
+
await rm(path.join(stage,'build-output.json'));await writeBuildOutput(stage,next);
|
|
83
|
+
return next;
|
|
84
|
+
}catch(error){await rm(stage,{recursive:true,force:true});throw error;}
|
|
85
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function recordDevDependencies(state,options,result){
|
|
3
|
+
if(!state?.dev||!result.metafile)return;
|
|
4
|
+
state.devDependencies||=new Map();state.devStyleBundles||=new Map();state.devStyleDependencies||=new Map();
|
|
5
|
+
const absolute=name=>path.resolve(options.absWorkingDir||state.root,name.replace(/^[\w-]+:(?=(?:[A-Za-z]:[\\/]|\/))/ ,''));
|
|
6
|
+
for(const [name,input]of Object.entries(result.metafile.inputs)){
|
|
7
|
+
const file=absolute(name),imports=state.devDependencies.get(file)||new Set();
|
|
8
|
+
for(const item of input.imports||[])if(!item.external)imports.add(absolute(item.path));
|
|
9
|
+
state.devDependencies.set(file,imports);
|
|
10
|
+
}
|
|
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
|
+
const inputs=Object.keys(output.inputs||{}).filter(file=>file!=='<stdin>'),imported=new Set();
|
|
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
|
+
const roots=inputs.filter(input=>!imported.has(input));
|
|
15
|
+
state.devStyleBundles.set(path.basename(name),roots.map(file=>file.startsWith('prnext-compiled-css:')?file:path.isAbsolute(file)?file:path.resolve(state.root,file)));
|
|
16
|
+
state.devStyleDependencies.set(path.basename(name),inputs.map(absolute));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function developmentDependencies(state,project,manifest){
|
|
20
|
+
const graph=state?.devDependencies||new Map(),routes=[];
|
|
21
|
+
let bytes=0;
|
|
22
|
+
for(const route of manifest.routes.filter(route=>!route.internal)){
|
|
23
|
+
const source=project.routes.find(item=>item.id===route.id);if(!source)continue;
|
|
24
|
+
const pending=[],seen=new Set(),visit=value=>{if(typeof value==='string'&&path.isAbsolute(value))pending.push(value);else if(Array.isArray(value))value.forEach(visit);else if(value&&typeof value==='object')Object.values(value).forEach(visit);};
|
|
25
|
+
visit(source);if(project.app)pending.push(project.app);if(project.document)pending.push(project.document);
|
|
26
|
+
while(pending.length){const file=pending.pop();if(seen.has(file))continue;seen.add(file);for(const dependency of graph.get(file)||[])pending.push(dependency);}
|
|
27
|
+
const record={route:route.pattern,files:[...seen].filter(file=>file.startsWith(project.root+path.sep)&&!path.relative(project.root,file).startsWith('.prnext-')).map(file=>path.relative(project.root,file).replaceAll(path.sep,'/')).filter(file=>!file.includes(':')&&!file.includes('?'))};
|
|
28
|
+
bytes+=Buffer.byteLength(JSON.stringify(record));if(bytes>16*1024*1024||routes.length>=50000)return Object.assign([],{incomplete:'Route dependency graph exceeds the 16 MiB / 50,000 record budget'});
|
|
29
|
+
routes.push(record);
|
|
30
|
+
}
|
|
31
|
+
return routes;
|
|
32
|
+
}
|
|
33
|
+
export const developmentStylePlans=state=>[...(state?.devStyleBundles||[])];
|
|
34
|
+
export const developmentStyleDependencies=state=>[...(state?.devStyleDependencies||[])];
|
|
35
|
+
export function developmentModules(state,root){
|
|
36
|
+
const records=[];let bytes=0;
|
|
37
|
+
const relative=file=>{const name=path.relative(root,file).replaceAll(path.sep,'/');return !name||name.startsWith('../')||name.includes(':')||name.includes('?')||name.startsWith('.prnext-')||name.startsWith('node_modules/')?undefined:name;};
|
|
38
|
+
for(const[file,dependencies]of state?.devDependencies||[]){
|
|
39
|
+
const name=relative(file);if(!name)continue;
|
|
40
|
+
const record={route:name,module:true,files:[name,...[...dependencies].map(relative).filter(Boolean)]};
|
|
41
|
+
bytes+=Buffer.byteLength(JSON.stringify(record));if(bytes>16*1024*1024||records.length>=50000)return Object.assign([],{incomplete:'Module dependency graph exceeds the 16 MiB / 50,000 record budget'});records.push(record);
|
|
42
|
+
}
|
|
43
|
+
return records;
|
|
44
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {loadProjectConfig} from './config.mjs';
|
|
2
|
+
import {scanProject} from './scan.mjs';
|
|
3
|
+
import {snapshotEnvConfig} from '../runtime/env.mjs';
|
|
4
|
+
|
|
5
|
+
export function routeFiles(project, patterns) {
|
|
6
|
+
const files=new Set();
|
|
7
|
+
const visit=value=>{
|
|
8
|
+
if(typeof value==='string' && /\.(?:[cm]?js|jsx|tsx?)$/.test(value))files.add(value);
|
|
9
|
+
else if(Array.isArray(value))for(const item of value)visit(item);
|
|
10
|
+
else if(value&&typeof value==='object')for(const item of Object.values(value))visit(item);
|
|
11
|
+
};
|
|
12
|
+
for(const route of project.routes)if(patterns.includes(route.pattern)||route.internal||route.errorStatus)visit(route);
|
|
13
|
+
visit(project.appNotFound);return files;
|
|
14
|
+
}
|
|
15
|
+
export async function discoverDevelopment(root) {
|
|
16
|
+
const restore=snapshotEnvConfig();
|
|
17
|
+
const envMode=process.env.NODE_ENV==='test'?'test':'development';
|
|
18
|
+
process.env.NODE_ENV='development';
|
|
19
|
+
try {
|
|
20
|
+
const config=await loadProjectConfig(root,{dev:true,envMode});
|
|
21
|
+
const project=await scanProject(root,{basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n,devAppFiles:new Set()});
|
|
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
|
+
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
|
+
onDemandEntries:config.onDemandEntries,typescript:config.typescript,typedRoutes:config.typedRoutes,cssEngine:config.cssEngine}};
|
|
27
|
+
} finally {restore();}
|
|
28
|
+
}
|
package/build/dev-worker.mjs
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import {build} from './index.mjs';
|
|
2
|
-
import {closeIncrementalCompiler} from './incremental.mjs';
|
|
2
|
+
import {closeIncrementalCompiler,incrementalCompilerStats} from './incremental.mjs';
|
|
3
3
|
import {realpath} from 'node:fs/promises';
|
|
4
4
|
import {createDevInputSnapshot} from '../runtime/dev-file-changes.mjs';
|
|
5
|
-
|
|
5
|
+
import {discoverDevelopment} from './dev-discovery.mjs';
|
|
6
|
+
import {transformCacheStats} from './transform-cache.mjs';
|
|
7
|
+
import {scheduleCompileCacheFlush} from '../runtime/compile-cache.mjs';
|
|
8
|
+
import {createCompilerHealth} from './dev-compiler-health.mjs';
|
|
9
|
+
const compilerHealth=createCompilerHealth();
|
|
10
|
+
let busy=false;
|
|
6
11
|
process.on('message',async message=>{
|
|
7
|
-
if(message?.type
|
|
12
|
+
if(!['build','discover'].includes(message?.type)||busy)return;
|
|
8
13
|
busy=true;
|
|
9
14
|
try{
|
|
15
|
+
if(message.type==='discover'){process.send({type:'result',ok:true,discovery:await discoverDevelopment(message.root)});return;}
|
|
10
16
|
const started=performance.now();
|
|
11
17
|
const inputs=createDevInputSnapshot([message.root,await realpath(message.root)]);
|
|
12
|
-
await build(message.root,{dev:true,incremental
|
|
18
|
+
const manifest=await build(message.root,{dev:true,incremental:!message.options?.devSafe,...message.options,devInputs:inputs});
|
|
13
19
|
console.log(`PRNext built ${message.root} in ${((performance.now()-started)/1000).toFixed(2)}s`);
|
|
14
|
-
|
|
20
|
+
scheduleCompileCacheFlush();
|
|
21
|
+
const health=compilerHealth(process.memoryUsage());
|
|
22
|
+
if(message.options?.devSafe)Object.assign(health,{recycle:true,reason:'safe-mode'});
|
|
23
|
+
process.send({type:'result',ok:true,manifest,inputs:inputs.entries(),compiler:{...incrementalCompilerStats(),health},transforms:transformCacheStats(),recycle:health.recycle});
|
|
15
24
|
}catch(error){process.send({type:'result',ok:false,error:error.stack||error.message,recycle:true});}
|
|
16
25
|
finally{busy=false;}
|
|
17
26
|
});
|
package/build/dev.mjs
CHANGED
|
@@ -5,10 +5,54 @@ 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');
|
|
13
|
+
const refreshIdentities = new Map();
|
|
14
|
+
function namedDefaultComponents({types:t}){
|
|
15
|
+
return {visitor:{Program:{enter(program){
|
|
16
|
+
if(program.node.directives.some(item=>item.value.value==='use server'))return;
|
|
17
|
+
for(const statement of program.get('body')){
|
|
18
|
+
if(!statement.isExportDefaultDeclaration())continue;
|
|
19
|
+
const value=statement.get('declaration');if(!value.isArrowFunctionExpression())continue;
|
|
20
|
+
let jsx=false;value.traverse({JSXElement(p){jsx=true;p.stop();},JSXFragment(p){jsx=true;p.stop();}});if(!jsx)continue;
|
|
21
|
+
let name='PRNextDefaultComponent',suffix=0;
|
|
22
|
+
while(program.scope.hasBinding(name)||program.scope.hasGlobal(name))name='PRNextDefaultComponent'+(++suffix);
|
|
23
|
+
const id=t.identifier(name);
|
|
24
|
+
statement.replaceWithMultiple([t.variableDeclaration('const',[t.variableDeclarator(id,value.node)]),t.exportDefaultDeclaration(t.cloneNode(id))]);
|
|
25
|
+
program.scope.crawl();
|
|
26
|
+
}
|
|
27
|
+
}}}};
|
|
28
|
+
}
|
|
29
|
+
function sourceElements(id,trace){return ({types:t})=>({visitor:{JSXOpeningElement(node){
|
|
30
|
+
const name=node.node.name;
|
|
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;
|
|
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;}
|
|
37
|
+
node.node.attributes.push(t.jsxAttribute(t.jsxIdentifier('data-prnext-source'),t.stringLiteral(`${id}:${start.line}:${start.column+1}`)));
|
|
38
|
+
}}});}
|
|
39
|
+
|
|
40
|
+
/** The same original JSX locations must be present in SSR and browser output. */
|
|
41
|
+
export function createSourceLocationTransform(projectRoot){
|
|
42
|
+
projectRoot=path.resolve(projectRoot);const canonicalRoot=realpath(projectRoot).catch(()=>projectRoot);
|
|
43
|
+
return async(source,filename)=>{
|
|
44
|
+
filename=path.resolve(filename);
|
|
45
|
+
if(!source.includes('<')||filename.includes(`${path.sep}node_modules${path.sep}`)||filename.includes(`${path.sep}.prnext-build-`))return source;
|
|
46
|
+
const root=filename.startsWith(projectRoot+path.sep)?projectRoot:await canonicalRoot;
|
|
47
|
+
if(!filename.startsWith(root+path.sep))return source;
|
|
48
|
+
const id=path.relative(root,filename).split(path.sep).join('/');
|
|
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)]});
|
|
52
|
+
return inlineSourceMap(result.code,result.map);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
}
|
|
12
56
|
|
|
13
57
|
/** Keep React renderers and framework contexts identical across rebuilt ESM chunks. */
|
|
14
58
|
export function devSingletonsPlugin() {
|
|
@@ -46,8 +90,9 @@ function componentExports(program) {
|
|
|
46
90
|
}
|
|
47
91
|
|
|
48
92
|
/** The official React transform records hook signatures and refresh families. */
|
|
49
|
-
export function createRefreshTransform(projectRoot) {
|
|
93
|
+
export function createRefreshTransform(projectRoot, { generation,forceGeneration=false,forceFiles } = {}) {
|
|
50
94
|
projectRoot = path.resolve(projectRoot);
|
|
95
|
+
const affected=forceFiles&&new Set(forceFiles.map(file=>path.resolve(projectRoot,file)));
|
|
51
96
|
const canonicalRoot = realpath(projectRoot).catch(() => projectRoot);
|
|
52
97
|
return async (source, filename) => {
|
|
53
98
|
filename = path.resolve(filename);
|
|
@@ -57,13 +102,14 @@ export function createRefreshTransform(projectRoot) {
|
|
|
57
102
|
const root = filename.startsWith(projectRoot + path.sep) ? projectRoot : await canonicalRoot;
|
|
58
103
|
if (!filename.startsWith(root + path.sep)) return source;
|
|
59
104
|
const hash = createHash('sha256').update(source).digest('hex');
|
|
60
|
-
|
|
105
|
+
const transformed = await cachedAsyncTransform('fast-refresh',source,[projectRoot,filename,'source-elements-v3-original-locations'],async () => {
|
|
106
|
+
const id = path.relative(root, filename).replaceAll(path.sep, '/');
|
|
107
|
+
const input=extractSourceMap(source),trace=input.map?new TraceMap(input.map):undefined;
|
|
61
108
|
const result = await transformAsync(source, { filename, babelrc: false, configFile: false,
|
|
62
109
|
ast: true, sourceMaps: true, sourceFileName:filename, sourceType: 'unambiguous',
|
|
63
|
-
parserOpts: { plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] },
|
|
64
|
-
plugins: [[refresh, { skipEnvCheck: true, refreshReg: '__prnextRefreshReg', refreshSig: '__prnextRefreshSig' }]],
|
|
110
|
+
parserOpts: { plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] },
|
|
111
|
+
plugins: [namedDefaultComponents,[refresh, { skipEnvCheck: true, refreshReg: '__prnextRefreshReg', refreshSig: '__prnextRefreshSig' }],sourceElements(id,trace)],
|
|
65
112
|
});
|
|
66
|
-
const id = path.relative(root, filename).replaceAll(path.sep, '/');
|
|
67
113
|
// An ESM import changes webpack's interpretation of module.exports.
|
|
68
114
|
// Keep script/CommonJS inputs in their original module format.
|
|
69
115
|
const runtime = result.ast.program.sourceType === 'script'
|
|
@@ -73,5 +119,17 @@ export function createRefreshTransform(projectRoot) {
|
|
|
73
119
|
const prefix = `${strict}${runtime}function __prnextRefreshReg(type,id){__prnextRegister(type,${JSON.stringify(id + ':')}+id)};\n`;
|
|
74
120
|
return inlineSourceMap(prefix + result.code + `\n__prnextRegisterModule(${JSON.stringify(id)},${JSON.stringify(hash)},${componentExports(result.ast.program)});\n`,{...result.map,mappings:';'+result.map.mappings});
|
|
75
121
|
});
|
|
122
|
+
// ESM URLs are cached forever by the browser. A -> B -> exact A otherwise
|
|
123
|
+
// imports an already evaluated content-hashed chunk and never registers A
|
|
124
|
+
// with React Refresh again. Mark project modules per generation, after the
|
|
125
|
+
// persistent transform cache. Keep the marker stable for unchanged modules:
|
|
126
|
+
// esbuild can retain their parsed graph rather than reparsing every route.
|
|
127
|
+
if(!generation)return transformed;
|
|
128
|
+
const identity=refreshIdentities.get(filename);
|
|
129
|
+
if((affected?affected.has(filename):forceGeneration)||!identity||identity.hash!==hash){
|
|
130
|
+
if(refreshIdentities.size>=50_000&&!identity)refreshIdentities.delete(refreshIdentities.keys().next().value);
|
|
131
|
+
refreshIdentities.set(filename,{hash,generation});
|
|
132
|
+
}
|
|
133
|
+
return transformed + `\nglobalThis.__PRNEXT_DEV__.evaluatedGeneration=${JSON.stringify(refreshIdentities.get(filename).generation)};\n`;
|
|
76
134
|
};
|
|
77
135
|
}
|
package/build/dynamic.mjs
CHANGED
|
@@ -67,7 +67,7 @@ export function transformDynamicImports(source, filename, { projectRoot, mode =
|
|
|
67
67
|
|
|
68
68
|
function transformDynamicImportsUncached(source, filename, {projectRoot,mode}) {
|
|
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
72
|
let changed = false;
|
|
73
73
|
traverse(ast, {
|
package/build/edge.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
|
|
2
|
-
import {frameworkImportName, frameworkSubpathPattern} from './framework-imports.mjs';
|
|
2
|
+
import {applicationEntry, frameworkImportName, frameworkSubpathPattern} from './framework-imports.mjs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { builtinModules, createRequire } from 'node:module';
|
|
6
|
-
import { build } from './compiler.mjs';
|
|
6
|
+
import { build, readCompilerFile } from './compiler.mjs';
|
|
7
7
|
import { parse } from '@babel/parser';
|
|
8
8
|
import traversePackage from '@babel/traverse';
|
|
9
9
|
import { cachedTransform } from './transform-cache.mjs';
|
|
@@ -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,6 +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,{transformTypedHelpers});
|
|
88
89
|
builder.onResolve({ filter: /.*/ }, args => {
|
|
89
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}).` }] };
|
|
90
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' };
|
|
@@ -111,7 +112,7 @@ export async function compileEdge({ file, outfile, projectRoot, dev = false, def
|
|
|
111
112
|
: names[args.path].map(name => `export const ${name}=globalThis.__PRNEXT_EDGE_HOST.${args.path}.${name};`).join('\n') }));
|
|
112
113
|
builder.onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: 'file' }, async args => {
|
|
113
114
|
let source = args.prnextSource ?? await readFile(args.path, 'utf8');
|
|
114
|
-
if (transformSource) source = await transformSource(source, args.path, builder);
|
|
115
|
+
if (transformSource) source = await transformSource(source, args.path, builder, {loaderOutput:args.prnextSource!==undefined});
|
|
115
116
|
if (!trusted.has(args.path)) source = validate(source, args.path, reactMode);
|
|
116
117
|
return { contents: source, loader: /\.[cm]?tsx?$/.test(args.path) ? 'tsx' : 'jsx', resolveDir: path.dirname(args.path) };
|
|
117
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 = [];
|