@thomas.f/prnext 0.1.18-alpha → 0.1.20-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 +2 -0
- package/build/actions.mjs +26 -14
- package/build/app.mjs +90 -101
- package/build/build-lock.mjs +2 -71
- package/build/build-retention.mjs +119 -0
- package/build/bundle-runtime.mjs +9 -8
- package/build/cache-command.mjs +31 -0
- package/build/check.mjs +41 -3
- package/build/compilation-cache.mjs +32 -12
- package/build/compilation-storage.mjs +190 -0
- package/build/compiler.mjs +8 -7
- package/build/config.mjs +25 -2
- package/build/create.mjs +1 -1
- package/build/css-modules.mjs +4 -0
- package/build/dev-action-graph.mjs +21 -0
- package/build/dev-css-assets.mjs +173 -0
- package/build/dev-css-update.mjs +14 -12
- package/build/dev-lazy-build.mjs +77 -0
- package/build/dynamic.mjs +13 -6
- package/build/font-cache.mjs +9 -6
- package/build/font-files.mjs +1 -1
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +61 -15
- package/build/lazy-dynamic-imports.mjs +137 -0
- 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-ppr.mjs +32 -0
- package/build/npm-packages.mjs +1 -0
- package/build/og.mjs +121 -1
- package/build/parallel-plan.mjs +55 -0
- package/build/persistent-cache.mjs +29 -24
- package/build/production-builder.mjs +1 -1
- package/build/production-check.mjs +34 -0
- package/build/react-compiler-report.mjs +31 -8
- package/build/react-compiler.mjs +101 -10
- package/build/server-config.mjs +35 -0
- package/build/source-analysis.mjs +54 -4
- package/build/standalone.mjs +21 -3
- package/build/static-adapter.mjs +7 -3
- package/build/transform.mjs +38 -6
- package/build/typescript-dev.mjs +3 -1
- package/build/webpack-graph.mjs +13 -3
- package/cli.mjs +24 -6
- package/compat/app-dynamic.cjs +12 -1
- package/compat/app-layout-context.cjs +7 -3
- package/compat/cache-handlers.cjs +3 -1
- package/compat/flight-delta.cjs +50 -13
- package/compat/index.d.ts +24 -1
- package/compat/link.cjs +2 -2
- package/compat/navigation-authorization.cjs +8 -1
- package/docs/ai/compatibility.md +187 -7
- package/docs/ai/deployment.md +141 -0
- package/docs/ai/quickstart.md +5 -0
- package/docs/ai/routing.md +11 -0
- package/docs/ai/troubleshooting.md +79 -0
- package/package.json +26 -11
- package/release/fontkit.mjs +31 -0
- package/release/verify.mjs +3 -0
- package/runtime/api.mjs +5 -2
- package/runtime/app-client.mjs +254 -53
- package/runtime/app-partial.mjs +2 -10
- package/runtime/app-prefetch.mjs +2 -2
- package/runtime/app-render.mjs +11 -7
- package/runtime/app-stream-channel.mjs +3 -2
- package/runtime/app-styles.mjs +22 -4
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +3 -1
- package/runtime/compile-cache-retention.mjs +53 -0
- package/runtime/compile-cache.mjs +28 -1
- package/runtime/dev-client.mjs +13 -5
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-gateway.mjs +3 -2
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-route-selection.mjs +64 -0
- package/runtime/dev-runtime-pool.mjs +3 -2
- package/runtime/dev-runtime-spare.mjs +1 -1
- package/runtime/dev-session-retention.mjs +154 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +86 -26
- package/runtime/env.mjs +9 -0
- package/runtime/flight-delta.mjs +59 -9
- package/runtime/fs.mjs +15 -8
- package/runtime/lazy-import-client.mjs +38 -0
- package/runtime/native-ppr.mjs +35 -0
- package/runtime/og/NOTICE.txt +1 -1
- package/runtime/og/index.node.mjs +76 -31216
- package/runtime/og/upstream.json +115 -0
- package/runtime/persistent/client.mjs +4 -2
- package/runtime/persistent/daemon.mjs +3 -11
- package/runtime/persistent/health.mjs +58 -0
- package/runtime/project-lock.mjs +71 -0
- package/runtime/request-transport.mjs +112 -32
- package/runtime/storage-path.mjs +36 -0
- package/runtime/trace.mjs +44 -4
- package/runtime/worker.mjs +9 -4
- package/vendor/fontkit/NOTICE.txt +5 -0
- package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
- package/vendor/fontkit/index.mjs +3 -0
- package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
- package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
- package/vendor/fontkit/licenses/brotli/readme.md +61 -0
- package/vendor/fontkit/licenses/clone/LICENSE +18 -0
- package/vendor/fontkit/licenses/dfa/README.md +83 -0
- package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
- package/vendor/fontkit/licenses/fontkit/README.md +266 -0
- package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
- package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
- package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
- package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
- package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
- package/vendor/fontkit/upstream.json +215 -0
- 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/compiler.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import {withSourceMaps,configureSourceMaps,inlineSourceMap} from './source-maps.
|
|
|
16
16
|
import { optimizeImportsPlugins } from './optimize-imports.mjs';
|
|
17
17
|
import {recordBuildAnalysis} from './analyze.mjs';
|
|
18
18
|
import {recordDevDependencies,developmentDependencies,developmentStylePlans,developmentStyleDependencies,developmentModules} from './dev-dependencies.mjs';
|
|
19
|
-
import {normalizeReactCompilerConfig,
|
|
19
|
+
import {normalizeReactCompilerConfig,reactCompilerVersion} from './react-compiler.mjs';
|
|
20
20
|
import {withReactCompilerReporting,configureReactCompilerReporting,withReactCompilerGraph,recordReactCompilerGraph} from './react-compiler-report.mjs';
|
|
21
21
|
import {withCompilationGraphCacheReporting,cachedGraphBuild} from './compilation-cache.mjs';
|
|
22
22
|
export {reactCompilerReport} from './react-compiler-report.mjs';
|
|
@@ -27,7 +27,7 @@ const compilation = new AsyncLocalStorage();
|
|
|
27
27
|
export const withCompilation = callback => withCompilationGraphCacheReporting(()=>withReactCompilerReporting(()=>withSourceMaps(()=>compilation.run({ targets: new Map(), loaded: new Map() }, callback))));
|
|
28
28
|
export function configureCompiler(options) {
|
|
29
29
|
Object.assign(compilation.getStore() || {}, options);configureSourceMaps(options.dev || options.config?.productionBrowserSourceMaps);
|
|
30
|
-
configureReactCompilerReporting({...options,config:{reactCompiler:options.config?.reactCompiler ? normalizeReactCompilerConfig(options.config.reactCompiler) : false},compilerVersion:
|
|
30
|
+
configureReactCompilerReporting({...options,config:{reactCompiler:options.config?.reactCompiler ? normalizeReactCompilerConfig(options.config.reactCompiler) : false},compilerVersion:reactCompilerVersion(options.config?.reactCompiler,options.root)});
|
|
31
31
|
}
|
|
32
32
|
export const buildAnalysisRecords=()=>compilation.getStore()?.analysis;
|
|
33
33
|
export const buildAnalysisRoutes=()=>compilation.getStore()?.analysisRoutes;
|
|
@@ -145,18 +145,19 @@ export async function validateCompilerConfig(config, root, {dev=false,edge=false
|
|
|
145
145
|
const state={config,root,dev,buildId:'prnext-check',targets:new Map()};
|
|
146
146
|
for (const target of ['node','browser',...(edge?['edge']:[])]) await targetConfig(state,target);
|
|
147
147
|
}
|
|
148
|
-
export async function build(options) {
|
|
148
|
+
export async function build(options, { role = 'main' } = {}) {
|
|
149
|
+
if (role !== 'main' && role !== 'styles') throw new TypeError('Invalid compiler graph role');
|
|
149
150
|
const state=compilation.getStore();
|
|
150
151
|
const target=options.define?.['process.env.NEXT_RUNTIME']==='"edge"'?'edge':options.platform==='browser'?'browser':'node';
|
|
151
152
|
return withReactCompilerGraph(target,async()=>{
|
|
152
|
-
const result=await compileGraph(options);
|
|
153
|
+
const result=await compileGraph(options,role);
|
|
153
154
|
recordReactCompilerGraph(result);
|
|
154
155
|
recordBuildAnalysis(state,options,result);
|
|
155
156
|
recordDevDependencies(state,options,result);
|
|
156
157
|
return result;
|
|
157
158
|
});
|
|
158
159
|
}
|
|
159
|
-
async function compileGraph(options) {
|
|
160
|
+
async function compileGraph(options,role) {
|
|
160
161
|
const state = compilation.getStore();
|
|
161
162
|
// The transform is captured by the framework's per-build callbacks. Give both
|
|
162
163
|
// esbuild and the webpack bridge a new graph identity when its options change,
|
|
@@ -164,7 +165,7 @@ async function compileGraph(options) {
|
|
|
164
165
|
// no hooks or runtime output and leaves the opt-out graph exactly as before.
|
|
165
166
|
if ((options.plugins || []).some(plugin => framework.test(plugin.name)) && state?.config?.reactCompiler) {
|
|
166
167
|
const settings=normalizeReactCompilerConfig(state.config.reactCompiler);
|
|
167
|
-
const fingerprint=createHash('sha256').update(JSON.stringify([
|
|
168
|
+
const fingerprint=createHash('sha256').update(JSON.stringify([reactCompilerVersion(settings,state.root),settings])).digest('hex').slice(0,24);
|
|
168
169
|
options={...options,plugins:[...options.plugins,{name:'prnext-react-compiler-'+fingerprint,setup(){}}]};
|
|
169
170
|
}
|
|
170
171
|
if ((options.plugins || []).some(plugin => framework.test(plugin.name))) options = { ...options, plugins: optimizeImportsPlugins(options.plugins, state?.config?.experimental?.optimizePackageImports) };
|
|
@@ -183,7 +184,7 @@ async function compileGraph(options) {
|
|
|
183
184
|
// Runtime vendor bundles are framework-owned, not project webpack targets.
|
|
184
185
|
const projectGraph = (options.plugins || []).some(plugin => framework.test(plugin.name));
|
|
185
186
|
if (projectGraph && state?.tsconfig) options = { ...options, tsconfig: state.tsconfig };
|
|
186
|
-
const nativeCompile = input => state?.incremental && (projectGraph || !input.plugins?.length) ? incrementalBuild(input,{root:state.root,stage:state.stage,group:projectGraph?'project':'auxiliary',run:callback=>compilation.run(state,callback)}) : esbuild(input);
|
|
187
|
+
const nativeCompile = input => state?.incremental && (projectGraph || !input.plugins?.length) ? incrementalBuild(input,{root:state.root,stage:state.stage,group:projectGraph?'project':'auxiliary',role:projectGraph?role:'auxiliary',run:callback=>compilation.run(state,callback)}) : esbuild(input);
|
|
187
188
|
const compile = input => state?.root && state?.stage ? cachedGraphBuild(input,state,nativeCompile) : nativeCompile(input);
|
|
188
189
|
if (!(state?.config?.webpack || Object.keys(state?.config?.turbopack?.rules || {}).length) || !projectGraph) return compile(options);
|
|
189
190
|
const target = options.define?.['process.env.NEXT_RUNTIME'] === '"edge"' ? 'edge' : options.platform === 'browser' ? 'browser' : 'node';
|
package/build/config.mjs
CHANGED
|
@@ -25,6 +25,8 @@ import {validatePrerenderConfig} from './prerender-config.mjs';
|
|
|
25
25
|
import {validateHtmlOnly} from './html-only-config.mjs';
|
|
26
26
|
import { validateProxyBodySize } from './proxy-body-config.mjs';
|
|
27
27
|
import { normalizeReactCompilerConfig } from './react-compiler.mjs';
|
|
28
|
+
import {validateCompilationCache} from './compilation-storage.mjs';
|
|
29
|
+
import {validateServerConfig} from './server-config.mjs';
|
|
28
30
|
|
|
29
31
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
32
|
const configName = /^(?:next|prnext)\.config\.(?:js|mjs|cjs|ts)$/;
|
|
@@ -37,6 +39,8 @@ supported.add('workerMemory');
|
|
|
37
39
|
supported.add('workerRetirement');
|
|
38
40
|
supported.add('workerPreload');
|
|
39
41
|
supported.add('deploymentRetention');
|
|
42
|
+
supported.add('compilationCache');
|
|
43
|
+
supported.add('server');
|
|
40
44
|
supported.add('prerender');
|
|
41
45
|
supported.add('htmlOnly');
|
|
42
46
|
supported.add('cssEngine');
|
|
@@ -107,6 +111,7 @@ export function validateProjectConfig(input, filename = 'project config') {
|
|
|
107
111
|
config.workerRetirement = validateWorkerRetirement(config.workerRetirement);
|
|
108
112
|
config.workerPreload=validateWorkerPreload(config.workerPreload);
|
|
109
113
|
config.deploymentRetention=validateDeploymentRetention(config.deploymentRetention);
|
|
114
|
+
config.compilationCache=validateCompilationCache(config.compilationCache);
|
|
110
115
|
config.prerender=validatePrerenderConfig(config.prerender);
|
|
111
116
|
config.htmlOnly=validateHtmlOnly(config.htmlOnly);
|
|
112
117
|
config.cssEngine ??= 'auto';
|
|
@@ -127,6 +132,7 @@ export function validateProjectConfig(input, filename = 'project config') {
|
|
|
127
132
|
}
|
|
128
133
|
if (config.webpack !== undefined && typeof config.webpack !== 'function') throw new TypeError('webpack must be a configuration function');
|
|
129
134
|
config.turbopack = validateTurbopack(config.turbopack);
|
|
135
|
+
config.server = validateServerConfig(config.server);
|
|
130
136
|
config.i18n = validateI18n(config.i18n);
|
|
131
137
|
if (config.i18n && config.output === 'export') throw new Error("i18n and output: 'export' cannot be combined; use explicit locale routes for static exports");
|
|
132
138
|
validateStandaloneConfig(config);
|
|
@@ -164,11 +170,28 @@ export function validateProjectConfig(input, filename = 'project config') {
|
|
|
164
170
|
if (config.sassOptions !== undefined && (!config.sassOptions || typeof config.sassOptions !== 'object' || Array.isArray(config.sassOptions))) throw new Error(`${filename}: sassOptions must be an object.`);
|
|
165
171
|
if (!config.experimental || typeof config.experimental !== 'object' || Array.isArray(config.experimental)) throw new Error(`${filename}: experimental must be an object.`);
|
|
166
172
|
for (const key of Object.keys(config.experimental)) {
|
|
167
|
-
if (!['nextScriptWorkers', 'serverActions', 'optimizePackageImports', 'proxyClientMaxBodySize'].includes(key)) throw new Error(`${filename}: experimental.${key} is not supported by PRNext. It cannot be silently ignored.`);
|
|
173
|
+
if (!['nextScriptWorkers', 'serverActions', 'optimizePackageImports', 'proxyClientMaxBodySize', 'lazyDynamicImports', 'streamingRequestBodies', 'nativePprShell', 'parallelBuilds'].includes(key)) throw new Error(`${filename}: experimental.${key} is not supported by PRNext. It cannot be silently ignored.`);
|
|
168
174
|
}
|
|
169
175
|
const nextScriptWorkers = config.experimental.nextScriptWorkers === undefined ? false : config.experimental.nextScriptWorkers;
|
|
170
176
|
if (typeof nextScriptWorkers !== 'boolean') throw new Error(`${filename}: experimental.nextScriptWorkers must be a boolean.`);
|
|
171
|
-
|
|
177
|
+
const lazyDynamicImports=config.experimental.lazyDynamicImports===undefined?false:config.experimental.lazyDynamicImports;
|
|
178
|
+
if(typeof lazyDynamicImports!=='boolean')throw new Error(`${filename}: experimental.lazyDynamicImports must be a boolean.`);
|
|
179
|
+
const streamingRequestBodies = config.experimental.streamingRequestBodies === undefined ? false : config.experimental.streamingRequestBodies;
|
|
180
|
+
const nativePprShell = config.experimental.nativePprShell === undefined ? false : config.experimental.nativePprShell;
|
|
181
|
+
for (const [key, value] of Object.entries({streamingRequestBodies, nativePprShell})) {
|
|
182
|
+
if (typeof value !== 'boolean') throw new Error(`${filename}: experimental.${key} must be a boolean.`);
|
|
183
|
+
}
|
|
184
|
+
if (nativePprShell && !config.cacheComponents) throw new Error(`${filename}: experimental.nativePprShell requires cacheComponents: true.`);
|
|
185
|
+
let parallelBuilds = config.experimental.parallelBuilds === undefined ? false : config.experimental.parallelBuilds;
|
|
186
|
+
if (parallelBuilds !== false) {
|
|
187
|
+
if (parallelBuilds === true) parallelBuilds = {};
|
|
188
|
+
if (!parallelBuilds || typeof parallelBuilds !== 'object' || Array.isArray(parallelBuilds)) throw new Error(`${filename}: experimental.parallelBuilds must be a boolean or an object.`);
|
|
189
|
+
for (const key of Object.keys(parallelBuilds)) if (!['maxWorkers', 'maxMemoryMb'].includes(key)) throw new Error(`${filename}: unknown parallelBuilds option ${key}.`);
|
|
190
|
+
parallelBuilds = {maxWorkers: parallelBuilds.maxWorkers === undefined ? 4 : parallelBuilds.maxWorkers, maxMemoryMb: parallelBuilds.maxMemoryMb === undefined ? 1024 : parallelBuilds.maxMemoryMb};
|
|
191
|
+
if (!Number.isInteger(parallelBuilds.maxWorkers) || parallelBuilds.maxWorkers < 1 || parallelBuilds.maxWorkers > 8) throw new Error(`${filename}: parallelBuilds.maxWorkers must be between 1 and 8.`);
|
|
192
|
+
if (!Number.isInteger(parallelBuilds.maxMemoryMb) || parallelBuilds.maxMemoryMb < 256 || parallelBuilds.maxMemoryMb > 8192) throw new Error(`${filename}: parallelBuilds.maxMemoryMb must be between 256 and 8192.`);
|
|
193
|
+
}
|
|
194
|
+
config.experimental = { nextScriptWorkers, lazyDynamicImports, streamingRequestBodies, nativePprShell, parallelBuilds, serverActions: validateServerActions(config.experimental.serverActions), optimizePackageImports: validateOptimizePackageImports(config.experimental.optimizePackageImports), proxyClientMaxBodySize: validateProxyBodySize(config.experimental.proxyClientMaxBodySize) };
|
|
172
195
|
for (const key of ['basePath', 'assetPrefix']) {
|
|
173
196
|
if (typeof config[key] !== 'string' || Buffer.byteLength(config[key]) > 4096) throw new Error(`${filename}: ${key} must be a string of at most 4096 bytes.`);
|
|
174
197
|
}
|
package/build/create.mjs
CHANGED
|
@@ -129,7 +129,7 @@ h1 { font-size: clamp(2.5rem, 8vw, 4rem); letter-spacing: -.04em; }
|
|
|
129
129
|
p { line-height: 1.6; }
|
|
130
130
|
code { font-size: .95em; }
|
|
131
131
|
`],
|
|
132
|
-
['.gitignore', 'node_modules/\n.prnext/\n.prnext-types/\n.prnext-dev/\n.prnext-cache/\n.env*\n!.env.example\n*.tsbuildinfo\n'],
|
|
132
|
+
['.gitignore', 'node_modules/\n.prnext/\n.prnext-types/\n.prnext-dev/\n.prnext-cache/\n.prnext-gc/\n.env*\n!.env.example\n*.tsbuildinfo\n'],
|
|
133
133
|
['README.md', `# ${name}\n\nA minimal PRNext application.\n\nRun \`npm install\` if dependencies have not been installed, then:\n\n\`\`\`sh\nnpm run dev\n\`\`\`\n\nEdit \`app/page.${extension}\`. Use the local URL printed by the server.\n\n\`\`\`sh\nnpm run check\n${typescript ? 'npm run typecheck\n' : ''}npm run build\nnpm start\n\`\`\`\n\nUse another port with \`npm run dev -- --port 3001\` when needed.\n\n[PRNext documentation](https://prnext.dev) · Installed AI guide: \`npx --no-install prnext docs --ai\`.\nPRNext is experimental; Next.js compatibility is partial.\n`],
|
|
134
134
|
['AGENTS.md', `# PRNext project\n\nThis application uses \`@thomas.f/prnext\`. Before changing routing, configuration, data fetching or deployment, read the installed guide at \`node_modules/@thomas.f/prnext/AI.md\` or run:\n\n\`\`\`sh\nnpx --no-install prnext docs --ai\nnpx --no-install prnext docs --ai --list\n\`\`\`\n\nRead only the relevant topic with \`npx --no-install prnext docs --ai <topic>\`. The installed documentation is version-specific; do not assume full Next.js compatibility. Preserve the project's package manager and lockfile.\n`],
|
|
135
135
|
]);
|
package/build/css-modules.mjs
CHANGED
|
@@ -138,6 +138,10 @@ export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/ass
|
|
|
138
138
|
|
|
139
139
|
return {
|
|
140
140
|
compile:get,
|
|
141
|
+
async parallelSafe() {
|
|
142
|
+
eligibility ??= cacheEligible();
|
|
143
|
+
return typeof sassOptions?.additionalData !== 'function' && await eligibility;
|
|
144
|
+
},
|
|
141
145
|
async mappings(){return Promise.all([...cache].map(async([file,result])=>({file,mapping:(await result).mapping,watchFiles:(await result).watchFiles})));},
|
|
142
146
|
plugin(browser, { emitStyles = () => true } = {}) {
|
|
143
147
|
return {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
// Build-private provenance, never Action arguments, source text or encryption
|
|
4
|
+
// material. Unknown/external owners deliberately disable selective refresh.
|
|
5
|
+
export function developmentActionMetadata(root, actions) {
|
|
6
|
+
const records = [], identities = new Set(); let bytes = 0;
|
|
7
|
+
for (const action of actions) {
|
|
8
|
+
const file = path.relative(root, action.file).replaceAll(path.sep, '/');
|
|
9
|
+
const runtimes = [...(action.runtimes?.size ? action.runtimes : ['nodejs'])].sort();
|
|
10
|
+
const record = { id: action.id, file, binding: action.binding, runtimes };
|
|
11
|
+
const identity = file + '\0' + action.binding;
|
|
12
|
+
if (!/^[a-f0-9]{64}$/.test(action.id) || !file || file.startsWith('../') || path.isAbsolute(file) || /[:?\0]/.test(file) || file.startsWith('node_modules/') ||
|
|
13
|
+
typeof action.binding !== 'string' || !action.binding || action.binding.length > 1024 || identities.has(identity) ||
|
|
14
|
+
runtimes.some(runtime => !['nodejs', 'edge'].includes(runtime)) || records.length >= 10000 ||
|
|
15
|
+
(bytes += Buffer.byteLength(JSON.stringify(record))) > 1024 * 1024)
|
|
16
|
+
return { version: 1, complete: false, actions: [] };
|
|
17
|
+
identities.add(identity); records.push(record);
|
|
18
|
+
}
|
|
19
|
+
records.sort((a, b) => a.file.localeCompare(b.file) || a.binding.localeCompare(b.binding));
|
|
20
|
+
return { version: 1, complete: true, actions: records };
|
|
21
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { copyFile, link, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { rename } from '../runtime/fs.mjs';
|
|
5
|
+
|
|
6
|
+
export const STYLE_OVERLAY_LIMITS = Object.freeze({ files: 4096, bytes: 64 * 1024 * 1024, catalogueBytes: 4 * 1024 * 1024, references: 16384 });
|
|
7
|
+
const digest = data => createHash('sha256').update(data).digest('hex');
|
|
8
|
+
const filename = name => typeof name === 'string' && name.length > 0 && name.length <= 255 && name !== '.' && name !== '..'
|
|
9
|
+
&& !/[\\/\x00-\x1f<>:"|?*#]/.test(name) && path.basename(name) === name;
|
|
10
|
+
const inside = (root, file) => { const relative = path.relative(root, file); return relative !== '' && !path.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..' + path.sep); };
|
|
11
|
+
|
|
12
|
+
export class StyleOverlayFallback extends Error {
|
|
13
|
+
constructor(code) { super(code); this.code = code; }
|
|
14
|
+
}
|
|
15
|
+
const fallback = code => { throw new StyleOverlayFallback(code); };
|
|
16
|
+
|
|
17
|
+
function assetsFor(bundles) {
|
|
18
|
+
const assets = new Map();
|
|
19
|
+
let references = 0, bytes = 0;
|
|
20
|
+
for (const files of bundles.values()) for (const file of files) {
|
|
21
|
+
if (++references > STYLE_OVERLAY_LIMITS.references) fallback('style-overlay-budget');
|
|
22
|
+
if (!file || !filename(file.name) || !Number.isSafeInteger(file.bytes) || file.bytes < 0 || file.bytes > STYLE_OVERLAY_LIMITS.bytes || !/^[a-f0-9]{64}$/.test(file.sha256)) fallback('invalid-style-overlay');
|
|
23
|
+
const previous = assets.get(file.name);
|
|
24
|
+
if (previous && (previous.bytes !== file.bytes || previous.sha256 !== file.sha256)) fallback('conflicting-style-overlay');
|
|
25
|
+
if (!previous) {
|
|
26
|
+
assets.set(file.name, file);
|
|
27
|
+
bytes += file.bytes;
|
|
28
|
+
if (assets.size > STYLE_OVERLAY_LIMITS.files || bytes > STYLE_OVERLAY_LIMITS.bytes) fallback('style-overlay-budget');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return assets;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function catalogue(previous, plans) {
|
|
35
|
+
const bundles = new Map();
|
|
36
|
+
if (!previous) return bundles;
|
|
37
|
+
if (previous.version !== 1 || typeof previous.baseDirectory !== 'string' || previous.baseDirectory.length > 4096
|
|
38
|
+
|| !Array.isArray(previous.bundles) || previous.bundles.length > STYLE_OVERLAY_LIMITS.files) fallback('invalid-style-overlay');
|
|
39
|
+
let references = 0;
|
|
40
|
+
for (const entry of previous.bundles) {
|
|
41
|
+
if (!Array.isArray(entry) || entry.length !== 2 || !plans.has(entry[0]) || bundles.has(entry[0]) || !Array.isArray(entry[1]) || entry[1].length > STYLE_OVERLAY_LIMITS.files) fallback('invalid-style-overlay');
|
|
42
|
+
references += entry[1].length;
|
|
43
|
+
if (references > STYLE_OVERLAY_LIMITS.references) fallback('style-overlay-budget');
|
|
44
|
+
if (!entry[1].some(file => file?.name === entry[0])) fallback('invalid-style-overlay');
|
|
45
|
+
bundles.set(entry[0], entry[1]);
|
|
46
|
+
}
|
|
47
|
+
assetsFor(bundles);
|
|
48
|
+
if (Buffer.byteLength(JSON.stringify(previous)) > STYLE_OVERLAY_LIMITS.catalogueBytes) fallback('style-overlay-budget');
|
|
49
|
+
return bundles;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function inspect(file) {
|
|
53
|
+
const info = await lstat(file);
|
|
54
|
+
if (!info.isFile() || info.size > STYLE_OVERLAY_LIMITS.bytes) fallback('invalid-style-overlay');
|
|
55
|
+
const data = await readFile(file);
|
|
56
|
+
return { name: path.basename(file), bytes: data.byteLength, sha256: digest(data) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A CSS generation owns only explicitly catalogued stylesheet assets. */
|
|
60
|
+
export async function createStyleOverlay({ root, source, destination, baseSource = source, manifest, plans, selected }) {
|
|
61
|
+
const previous = manifest.devStyles.overlay;
|
|
62
|
+
if (!previous && manifest.devStyles.baseClient) fallback('missing-style-overlay');
|
|
63
|
+
if (previous && !manifest.devStyles.baseClient) fallback('invalid-style-overlay');
|
|
64
|
+
if (!Array.isArray(plans) || plans.length > STYLE_OVERLAY_LIMITS.files) fallback('style-overlay-budget');
|
|
65
|
+
const names = new Set(plans.map(([name]) => name));
|
|
66
|
+
if (names.size !== plans.length || ![...names].every(filename)) fallback('invalid-style-overlay');
|
|
67
|
+
const bundles = catalogue(previous, names), previousAssets = assetsFor(bundles);
|
|
68
|
+
const selectedNames = new Set(selected.map(([name]) => name));
|
|
69
|
+
if ([...selectedNames].some(name => !names.has(name))) fallback('invalid-style-overlay');
|
|
70
|
+
for (const route of manifest.routes || []) for (const url of route.css || []) {
|
|
71
|
+
if (url.includes('?v=') && !bundles.has(path.posix.basename(url.split('?')[0]))) fallback('missing-style-overlay');
|
|
72
|
+
}
|
|
73
|
+
const [canonicalRoot, canonicalSource, canonicalBase, parent, sourceAssets] = await Promise.all([realpath(root), realpath(source), realpath(baseSource), realpath(path.dirname(destination)), realpath(path.join(source, 'assets'))]).catch(error => {
|
|
74
|
+
if (error.code === 'ENOENT') fallback('missing-style-overlay');
|
|
75
|
+
throw error;
|
|
76
|
+
});
|
|
77
|
+
// The destination does not exist yet. Its existing parent supplies the real
|
|
78
|
+
// identity, including macOS /var aliases and Windows short-name prefixes.
|
|
79
|
+
const canonicalDestination = path.join(parent, path.basename(destination));
|
|
80
|
+
if (!inside(canonicalRoot, canonicalSource) || !inside(canonicalRoot, canonicalBase)
|
|
81
|
+
|| parent !== canonicalRoot && !inside(canonicalRoot, parent) || !inside(canonicalRoot, canonicalDestination)
|
|
82
|
+
|| canonicalDestination === canonicalSource || canonicalDestination === canonicalBase
|
|
83
|
+
|| inside(canonicalDestination, canonicalSource) || inside(canonicalSource, canonicalDestination)
|
|
84
|
+
|| inside(canonicalDestination, canonicalBase) || inside(canonicalBase, canonicalDestination)
|
|
85
|
+
|| sourceAssets !== path.join(canonicalSource, 'assets')) fallback('invalid-style-overlay-directory');
|
|
86
|
+
const baseDirectory = path.relative(canonicalRoot, canonicalBase).replaceAll(path.sep, '/');
|
|
87
|
+
if (previous ? previous.baseDirectory !== baseDirectory : canonicalSource !== canonicalBase) fallback('invalid-style-overlay-base');
|
|
88
|
+
const assetsDirectory = path.join(destination, 'assets');
|
|
89
|
+
let owned = false;
|
|
90
|
+
async function inherit(file) {
|
|
91
|
+
const from = path.join(source, 'assets', file.name), target = path.join(assetsDirectory, file.name);
|
|
92
|
+
let actual;
|
|
93
|
+
try { actual = await inspect(from); } catch (error) { if (error.code === 'ENOENT') fallback('missing-style-overlay-asset'); throw error; }
|
|
94
|
+
if (actual.bytes !== file.bytes || actual.sha256 !== file.sha256) fallback('invalid-style-overlay-asset');
|
|
95
|
+
try { await link(from, target); }
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!['EXDEV', 'EPERM', 'ENOTSUP', 'EACCES'].includes(error.code)) throw error;
|
|
98
|
+
await copyFile(from, target);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await mkdir(destination);
|
|
103
|
+
owned = true;
|
|
104
|
+
await mkdir(assetsDirectory);
|
|
105
|
+
for (const name of selected.map(([name]) => name)) bundles.delete(name);
|
|
106
|
+
for (const file of assetsFor(bundles).values()) await inherit(file);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (owned) await rm(destination, { recursive: true, force: true });
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
async recordBundle(name, result) {
|
|
114
|
+
if (!selectedNames.has(name)) fallback('invalid-style-overlay-output');
|
|
115
|
+
const files = new Map();
|
|
116
|
+
for (const output of result.outputFiles) {
|
|
117
|
+
if (path.dirname(path.resolve(output.path)) !== path.resolve(assetsDirectory) || !filename(path.basename(output.path))) fallback('invalid-style-overlay-output');
|
|
118
|
+
const file = { name: path.basename(output.path), bytes: output.contents.byteLength, sha256: digest(output.contents) };
|
|
119
|
+
files.set(file.name, file);
|
|
120
|
+
}
|
|
121
|
+
if (!files.has(name)) fallback('invalid-style-overlay-output');
|
|
122
|
+
// CSS Module compilation emits its URL assets before the outer CSS
|
|
123
|
+
// bundle. Its absolute, external URL-token imports identify exactly the
|
|
124
|
+
// emitted images/fonts to retain, without scanning the JavaScript build.
|
|
125
|
+
const prefix = manifest.config.assetBase.replace(/\/$/, '') + '/';
|
|
126
|
+
for (const output of Object.values(result.metafile.outputs)) for (const item of output.imports || []) {
|
|
127
|
+
if (item.kind !== 'url-token' || !item.external || !item.path.startsWith(prefix)) continue;
|
|
128
|
+
let name;
|
|
129
|
+
try { name = decodeURIComponent(item.path.slice(prefix.length).split(/[?#]/)[0]); }
|
|
130
|
+
catch { fallback('invalid-style-overlay-output'); }
|
|
131
|
+
if (!filename(name) || files.has(name)) continue;
|
|
132
|
+
let file;
|
|
133
|
+
try { file = await inspect(path.join(assetsDirectory, name)); }
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error.code !== 'ENOENT') throw error;
|
|
136
|
+
const previous = previousAssets.get(name);
|
|
137
|
+
if (!previous) continue; // An unchanged base or public asset stays in its original generation.
|
|
138
|
+
await inherit(previous);
|
|
139
|
+
file = previous;
|
|
140
|
+
}
|
|
141
|
+
files.set(name, file);
|
|
142
|
+
}
|
|
143
|
+
bundles.set(name, [...files.values()]);
|
|
144
|
+
assetsFor(bundles); // Validate the aggregate budget before publishing bytes.
|
|
145
|
+
for (const output of result.outputFiles) {
|
|
146
|
+
const file = files.get(path.basename(output.path));
|
|
147
|
+
let existing;
|
|
148
|
+
try { existing = await inspect(output.path); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
149
|
+
if (existing) {
|
|
150
|
+
if (existing.bytes !== file.bytes || existing.sha256 !== file.sha256) fallback('conflicting-style-overlay');
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const temporary = output.path + '.' + randomUUID() + '.tmp';
|
|
154
|
+
try { await writeFile(temporary, output.contents); await rename(temporary, output.path); }
|
|
155
|
+
finally { await rm(temporary, { force: true }); }
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
async finish() {
|
|
159
|
+
const assets = assetsFor(bundles);
|
|
160
|
+
const value = { version: 1, baseDirectory, bundles: [...bundles] };
|
|
161
|
+
if (Buffer.byteLength(JSON.stringify(value)) > STYLE_OVERLAY_LIMITS.catalogueBytes) fallback('style-overlay-budget');
|
|
162
|
+
// This directory was created above for CSS only. Remove module assets
|
|
163
|
+
// that no current bundle references, rather than accumulating old URLs.
|
|
164
|
+
const entries = await readdir(assetsDirectory, { withFileTypes: true });
|
|
165
|
+
if (entries.length > STYLE_OVERLAY_LIMITS.references) fallback('style-overlay-budget');
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (!entry.isFile() || !filename(entry.name)) fallback('invalid-style-overlay-output');
|
|
168
|
+
if (!assets.has(entry.name)) await rm(path.join(assetsDirectory, entry.name));
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
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');
|
|
@@ -32,8 +33,9 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
|
|
|
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
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,7 +69,10 @@ 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);
|
|
@@ -74,12 +80,8 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
|
|
|
74
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);}}
|
|
75
81
|
next.routes=next.routes.map(route=>({...route,css:revise(route.css||[]),...(route.cssScopes?{cssScopes:reviseScopes(route.cssScopes)}:{})}));
|
|
76
82
|
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
83
|
const temporary=path.join(stage,'manifest.'+buildId+'.tmp');await writeFile(temporary,JSON.stringify(next));await rename(temporary,path.join(stage,'manifest.json'));
|
|
82
|
-
await
|
|
84
|
+
await writeBuildOutput(stage,next);
|
|
83
85
|
return next;
|
|
84
|
-
}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;}
|
|
85
87
|
}
|
|
@@ -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/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
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/font-cache.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { open, writeFile, rename, rm, readdir, lstat } from 'node:fs/promises';
|
|
3
|
+
import {constants} from 'node:fs';
|
|
4
|
+
import {privateCacheDirectory} from './persistent-cache.mjs';
|
|
3
5
|
import path from 'node:path';
|
|
4
6
|
|
|
5
7
|
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
@@ -11,12 +13,14 @@ let writes = Promise.resolve();
|
|
|
11
13
|
*/
|
|
12
14
|
export async function cachedFontResource(root, url, maximum, accept, download) {
|
|
13
15
|
if (!root) return download();
|
|
14
|
-
const directory =
|
|
16
|
+
const directory = await privateCacheDirectory(root,'fonts-v1');
|
|
17
|
+
if(!directory)return download();
|
|
15
18
|
const file = path.join(directory, hash(JSON.stringify([url, accept])) + '.bin');
|
|
16
19
|
try {
|
|
17
20
|
const metadata = await lstat(file);
|
|
18
|
-
if (metadata.isFile() && metadata.size > 65 && metadata.size <= maximum + 65) {
|
|
19
|
-
const
|
|
21
|
+
if (metadata.isFile() && !metadata.isSymbolicLink() && metadata.size > 65 && metadata.size <= maximum + 65) {
|
|
22
|
+
const handle=await open(file,constants.O_RDONLY|(constants.O_NOFOLLOW||0));
|
|
23
|
+
let stored;try{const info=await handle.stat();if(!info.isFile()||info.size>maximum+65)throw Object.assign(Error('Font cache changed'),{code:'ENOENT'});stored=await handle.readFile();}finally{await handle.close();}
|
|
20
24
|
const bytes = stored.subarray(65);
|
|
21
25
|
if (stored[64] === 10 && hash(bytes) === stored.toString('ascii', 0, 64)) return bytes;
|
|
22
26
|
}
|
|
@@ -24,10 +28,9 @@ export async function cachedFontResource(root, url, maximum, accept, download) {
|
|
|
24
28
|
const bytes = await download();
|
|
25
29
|
if (!bytes.length || bytes.length > maximum) throw new Error('Font resource exceeds its cache input limit');
|
|
26
30
|
const task = writes.catch(() => {}).then(async () => {
|
|
27
|
-
await mkdir(directory, { recursive: true });
|
|
28
31
|
const temporary = file + '.' + randomUUID() + '.tmp';
|
|
29
32
|
try {
|
|
30
|
-
await writeFile(temporary, Buffer.concat([Buffer.from(hash(bytes) + '\n'), bytes]), { flag: 'wx' });
|
|
33
|
+
await writeFile(temporary, Buffer.concat([Buffer.from(hash(bytes) + '\n'), bytes]), { flag: 'wx',mode:0o600 });
|
|
31
34
|
await rename(temporary, file);
|
|
32
35
|
} finally { await rm(temporary, { force: true }); }
|
|
33
36
|
const entries = [];
|
package/build/font-files.mjs
CHANGED
|
@@ -183,7 +183,7 @@ async function google(name, options, context) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
async function fallbackMetrics(faces, fallbackName) {
|
|
186
|
-
const { create } = await import('fontkit');
|
|
186
|
+
const { create } = await import('../vendor/fontkit/index.mjs');
|
|
187
187
|
// Prefer an upright face near the normal text weight for a shared fallback.
|
|
188
188
|
const candidates = [...faces].sort((a, b) => Number(a.style === 'italic') - Number(b.style === 'italic') || Math.abs((parseFloat(a.weight) || 400) - 400) - Math.abs((parseFloat(b.weight) || 400) - 400));
|
|
189
189
|
for (const face of candidates) {
|