@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/cli.mjs
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { rename } from './runtime/fs.mjs';
|
|
3
2
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import { readFile,
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { readFile, rm, mkdir, readdir } from 'node:fs/promises';
|
|
6
4
|
import path from 'node:path';
|
|
7
5
|
import { resolveNativeBinary, packageManifest } from './native/resolve.mjs';
|
|
8
6
|
import { replaceWithNative } from './native/launch.mjs';
|
|
9
7
|
import { readBuildDirectory } from './runtime/build-directory.mjs';
|
|
10
|
-
import { shouldWatchProjectFile } from './runtime/env.mjs';
|
|
11
8
|
|
|
12
|
-
const self = fileURLToPath(import.meta.url);
|
|
13
9
|
const [command = 'help', ...argv] = process.argv.slice(2);
|
|
14
10
|
const help = `PRNext ${packageManifest.version}
|
|
15
11
|
|
|
16
12
|
Use prnext for portable scripts; prn is a shortcut on compatible shells.
|
|
17
13
|
|
|
14
|
+
prnext create <directory> Create a project and install dependencies
|
|
15
|
+
prnext create <directory> --no-install Prepare project files without installing
|
|
16
|
+
prnext create --help TypeScript, JavaScript and creation options
|
|
18
17
|
prnext build [directory] Build React/TypeScript and static HTML
|
|
18
|
+
prnext build [directory] --watch Incremental production rebuilds on changes
|
|
19
19
|
prnext build [directory] --adapter static [--from dist] [--spa]
|
|
20
20
|
Import an existing public build directory
|
|
21
|
+
prnext build [directory] --adapter auto
|
|
22
|
+
Detect a completed browser build
|
|
23
|
+
prnext build [directory] --adapter static --from .
|
|
24
|
+
Build a plain HTML/CSS/JS site
|
|
21
25
|
prnext output [directory] [--json] Read the versioned build output contract
|
|
22
26
|
prnext start [directory] [options] Start native Rust production server
|
|
23
27
|
prnext pstart [directory] [options] Start production persistently in the background
|
|
@@ -28,9 +32,18 @@ Use prnext for portable scripts; prn is a shortcut on compatible shells.
|
|
|
28
32
|
prnext pstartup [--remove] Configure restoration at user login
|
|
29
33
|
prnext pstart --help All persistent commands and options
|
|
30
34
|
prnext dev [directory] [options] Rebuild and restart on source changes
|
|
35
|
+
prnext dev [directory] --host-ai Share local diagnostics with your connected Host AI
|
|
36
|
+
prnext dev [directory] --safe Use full builds and fresh workers for troubleshooting
|
|
37
|
+
prnext mcp [directory] Connect an AI to the running local dev server
|
|
38
|
+
prnext docs --ai [topic] Read the AI guide bundled with this version
|
|
39
|
+
prnext docs --ai --init [directory] Add a guide pointer to the project's AGENTS.md
|
|
31
40
|
prnext routes [directory] Display compiled routes
|
|
32
|
-
prnext check [directory] [--json]
|
|
41
|
+
prnext check [directory] [--json] Diagnose config, routes, Node, native and dependencies
|
|
42
|
+
prnext typegen [directory] [--json] Generate route types without building the app
|
|
43
|
+
prnext typecheck [directory] [--json] Generate types and check TypeScript without building
|
|
33
44
|
prnext inspect [directory] [--json|--otlp] Explain routes, health and sampled timings
|
|
45
|
+
prnext analyze [directory] [--json] [--max-js 150] Browser bundles and budget (kB)
|
|
46
|
+
prnext compiler [directory] [--json] React Compiler results from the last successful build
|
|
34
47
|
prnext host [prnext.host.json] Host built apps by hostname; wake on demand
|
|
35
48
|
prnext host [config] --check Validate hosting configuration and builds
|
|
36
49
|
prnext host [config] --status [--json] Show memory, process and idle status
|
|
@@ -47,9 +60,25 @@ Migration options: --dry-run (preview), --no-install (prepare only), --json
|
|
|
47
60
|
Compare GET responses before switching
|
|
48
61
|
Migration keeps Next.js and backs up package.json and existing lockfiles.
|
|
49
62
|
Simple next dev/build/start scripts become prnext commands; custom shell scripts need a manual edit.
|
|
63
|
+
Installation honors npm, pnpm, Yarn Classic/Berry and the workspace root.
|
|
64
|
+
The preview reports the manager, root and selected workspace. Failed installs
|
|
65
|
+
restore the selected manifest/lockfiles; dependency caches may need a retry.
|
|
66
|
+
|
|
67
|
+
Type commands require an application tsconfig and TypeScript installation.
|
|
68
|
+
They generate route types unless typedRoutes: false, preserve JSONC/extends,
|
|
69
|
+
and report updates to tsconfig/prnext-env.d.ts. typecheck always checks errors.
|
|
70
|
+
|
|
71
|
+
Analysis options: --metric raw|gzip|brotli (default raw), --baseline analysis.json
|
|
72
|
+
--max-js 150 Maximum initial JS per route, in kB
|
|
73
|
+
--max-increase-bytes 1024 --max-increase-percent 5
|
|
74
|
+
--budgets budgets.json Exact route keys with maxJs (kB),
|
|
75
|
+
maxIncreaseBytes and/or maxIncreasePercent
|
|
76
|
+
Analysis exit codes: 0 passed, 1 budget exceeded or initial total unverifiable,
|
|
77
|
+
2 invalid input or missing build. Request-dependent totals are lower bounds.
|
|
50
78
|
|
|
51
79
|
Server options: --port 3000 --hostname 127.0.0.1 --workers 1 --inspect
|
|
52
80
|
Production profiles: --profile balanced|speed|memory|classic
|
|
81
|
+
Render readiness: --prewarm --readiness-file /private/path/ready.json
|
|
53
82
|
Worker retirement: --worker-idle-seconds 300 --lane-idle-seconds 30 --keep-warm [true|false]
|
|
54
83
|
Memory favors lower RAM under concurrency, accepting more waiting and CPU per response.
|
|
55
84
|
Without an option: PRNEXT_PROFILE, legacy PRNEXT_MEMORY_PROFILE=compact, then balanced.
|
|
@@ -78,6 +107,42 @@ function stop(child) {
|
|
|
78
107
|
async function main() {
|
|
79
108
|
if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
|
|
80
109
|
if (['--version', '-v'].includes(command)) { console.log(packageManifest.version); return; }
|
|
110
|
+
if (command === 'create') {
|
|
111
|
+
const { createHelp, parseCreateArgs, createProject } = await import('./build/create.mjs');
|
|
112
|
+
if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) { console.log(createHelp); return; }
|
|
113
|
+
const { directory, options } = parseCreateArgs(argv);
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
let interrupted;
|
|
116
|
+
const interrupt = signal => { interrupted = signal; controller.abort(); };
|
|
117
|
+
const onInt = () => interrupt('SIGINT'), onTerm = () => interrupt('SIGTERM');
|
|
118
|
+
process.on('SIGINT', onInt); process.on('SIGTERM', onTerm);
|
|
119
|
+
try {
|
|
120
|
+
const report = await createProject(directory, { ...options, signal: controller.signal,
|
|
121
|
+
onOutput: options.json ? () => {} : (chunk, stream) => process[stream].write(chunk) });
|
|
122
|
+
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
123
|
+
else {
|
|
124
|
+
console.log(`PRNext project ${report.status}: ${report.root}`);
|
|
125
|
+
if (report.error) console.error(report.error);
|
|
126
|
+
if (!report.ok) console.log('Project files were preserved. Retry installation from this directory.');
|
|
127
|
+
if (path.resolve(process.cwd()) !== directory) console.log(`\nOpen ${report.root} in your terminal, then run:`);
|
|
128
|
+
for (const next of report.next) console.log(` ${next}`);
|
|
129
|
+
}
|
|
130
|
+
if (!report.ok) process.exitCode = interrupted === 'SIGINT' ? 130 : interrupted === 'SIGTERM' ? 143 : 1;
|
|
131
|
+
} finally { process.removeListener('SIGINT', onInt); process.removeListener('SIGTERM', onTerm); }
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (command === 'docs') {
|
|
135
|
+
const { executeAiDocs } = await import('./runtime/ai-docs.mjs');
|
|
136
|
+
const result = await executeAiDocs(argv);
|
|
137
|
+
process.stdout.write(result.output);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (command === 'mcp') {
|
|
141
|
+
if (argv.includes('--help') || argv.includes('-h')) { console.log('prnext mcp [directory]\nLocal development MCP over stdio. Start prnext dev first; no shell or production access is exposed.'); return; }
|
|
142
|
+
if (argv.length > 1 || argv.some(arg => arg.startsWith('-'))) throw Error('mcp accepts only [directory]');
|
|
143
|
+
const {runDevMcp}=await import('./runtime/dev-tools-mcp.mjs');
|
|
144
|
+
await runDevMcp(path.resolve(argv[0] || '.'));return;
|
|
145
|
+
}
|
|
81
146
|
if (['login', 'logout', 'whoami', 'deploy', 'update'].includes(command)) {
|
|
82
147
|
const { cloudCommand } = await import('./runtime/cloud/client.mjs');
|
|
83
148
|
await cloudCommand(command, argv);
|
|
@@ -150,9 +215,59 @@ async function main() {
|
|
|
150
215
|
if (!report.ok) process.exitCode = 1;
|
|
151
216
|
return;
|
|
152
217
|
}
|
|
153
|
-
if (!['build', 'start', 'dev', 'routes', 'check', 'inspect', 'output'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
|
|
218
|
+
if (!['build', 'start', 'dev', 'routes', 'check', 'typegen', 'typecheck', 'inspect', 'analyze', 'compiler', 'output'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
|
|
154
219
|
const args = [...argv];
|
|
155
220
|
const root = path.resolve(args[0] && !args[0].startsWith('-') ? args.shift() : '.');
|
|
221
|
+
if (['check','typegen','typecheck','analyze','compiler'].includes(command) && args.some(arg => ['--help','-h'].includes(arg))) { console.log(help); return; }
|
|
222
|
+
if(command==='compiler'){
|
|
223
|
+
try {
|
|
224
|
+
if(args.some(arg=>arg!=='--json') || args.filter(arg=>arg==='--json').length>1)throw new Error('compiler accepts only [directory] and --json');
|
|
225
|
+
const {readReactCompilerReport,printReactCompilerReport}=await import('./build/react-compiler-report.mjs');
|
|
226
|
+
const {readBuildOutput}=await import('./build/output-contract.mjs');
|
|
227
|
+
const directory=path.join(root,await readBuildDirectory(root));
|
|
228
|
+
const output=await readBuildOutput(directory);
|
|
229
|
+
if(output.adapter!=='react')throw new Error('React Compiler reports apply to React builds. This application uses the static adapter.');
|
|
230
|
+
const report=await readReactCompilerReport(directory,{buildId:output.buildId});
|
|
231
|
+
if(!report)throw new Error('No React Compiler report is available. Rebuild this application with the installed PRNext version.');
|
|
232
|
+
if(args.includes('--json'))console.log(JSON.stringify(report,null,2));else printReactCompilerReport(report,{details:true});
|
|
233
|
+
} catch(error) {
|
|
234
|
+
if(args.includes('--json'))console.log(JSON.stringify({ok:false,error:error.message}));
|
|
235
|
+
else console.error(`PRNext compiler: ${error.message}`);
|
|
236
|
+
process.exitCode=2;
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if(command==='analyze'){
|
|
241
|
+
try {
|
|
242
|
+
const {analyzeBuild,printAnalysis,parseAnalysisArgs}=await import('./build/analyze.mjs');
|
|
243
|
+
const options=parseAnalysisArgs(args),report=await analyzeBuild(root,options);
|
|
244
|
+
if(options.json)console.log(JSON.stringify(report,null,2));else printAnalysis(report);
|
|
245
|
+
if(!report.ok)process.exitCode=1;
|
|
246
|
+
} catch(error) {
|
|
247
|
+
if(args.includes('--json'))console.log(JSON.stringify({ok:false,error:error.message}));
|
|
248
|
+
else console.error(`PRNext analysis: ${error.message}`);
|
|
249
|
+
process.exitCode=2;
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (command === 'typegen' || command === 'typecheck') {
|
|
254
|
+
if (args.some(arg => arg !== '--json') || args.filter(arg => arg === '--json').length > 1) throw new Error(`${command} accepts only [directory] and --json`);
|
|
255
|
+
const { runTypeCommand } = await import('./build/type-command.mjs');
|
|
256
|
+
process.chdir(root);
|
|
257
|
+
process.env.INIT_CWD = root;
|
|
258
|
+
process.env.PWD = root;
|
|
259
|
+
const report = await runTypeCommand(root, { check: command === 'typecheck' });
|
|
260
|
+
if (args.includes('--json')) console.log(JSON.stringify(report, null, 2));
|
|
261
|
+
else {
|
|
262
|
+
console.log(`PRNext ${command} ${report.ok ? 'passed' : 'failed'}: ${root}`);
|
|
263
|
+
console.log(`${report.routes} page routes; tsconfig: ${report.tsconfig || 'unavailable'}; route types: ${report.typedRoutes ? 'enabled' : 'disabled'}.`);
|
|
264
|
+
for (const file of report.updated) console.log(`Updated ${file}`);
|
|
265
|
+
for (const note of report.notes) console.log(note);
|
|
266
|
+
for (const error of report.errors) console.error(error);
|
|
267
|
+
}
|
|
268
|
+
if (!report.ok) process.exitCode = 1;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
156
271
|
if (command === 'output') {
|
|
157
272
|
if (args.some(arg => arg !== '--json')) throw new Error('output accepts only [directory] and --json');
|
|
158
273
|
const { readBuildOutput } = await import('./build/output-contract.mjs');
|
|
@@ -196,7 +311,10 @@ async function main() {
|
|
|
196
311
|
else {
|
|
197
312
|
console.log(`PRNext preflight ${report.ok ? 'passed' : 'failed'}: ${root}`);
|
|
198
313
|
console.log(`${report.routes.length} routes. Installed versions: ${Object.entries(report.versions).map(([name,version])=>`${name}@${version||'missing'}`).join(', ')}`);
|
|
199
|
-
for (const
|
|
314
|
+
for (const item of report.diagnostics) {
|
|
315
|
+
const output = item.severity === 'error' ? console.error : console.log;
|
|
316
|
+
output(`${item.severity}: ${item.message}${item.action ? ` ${item.action}` : ''}`);
|
|
317
|
+
}
|
|
200
318
|
for (const note of report.notes) console.log(note);
|
|
201
319
|
}
|
|
202
320
|
if (!report.ok) process.exitCode = 1;
|
|
@@ -207,6 +325,7 @@ async function main() {
|
|
|
207
325
|
for (let i = 0; i < args.length; i++) {
|
|
208
326
|
const flag = args[i];
|
|
209
327
|
if (flag === '--dev') options.dev = true;
|
|
328
|
+
else if (flag === '--watch') options.watch = true;
|
|
210
329
|
else if (flag === '--spa') options.spa = true;
|
|
211
330
|
else if (flag === '--adapter' || flag === '--from') {
|
|
212
331
|
const value = args[++i];
|
|
@@ -220,131 +339,52 @@ async function main() {
|
|
|
220
339
|
const envMode = process.env.NODE_ENV === 'test' ? 'test' : undefined;
|
|
221
340
|
process.env.NODE_ENV = dev ? 'development' : 'production';
|
|
222
341
|
const started = performance.now();
|
|
342
|
+
if(options.watch){
|
|
343
|
+
if(dev)throw new Error('--watch builds production; use prnext dev for development.');
|
|
344
|
+
const {watchProduction}=await import('./build/production-builder.mjs');
|
|
345
|
+
delete options.watch;await watchProduction(root,options);return;
|
|
346
|
+
}
|
|
347
|
+
if (options.adapter === undefined || options.adapter === 'auto') {
|
|
348
|
+
const { detectBuildAdapter } = await import('./build/static-input.mjs');
|
|
349
|
+
const selected = await detectBuildAdapter(root, { prepared: options.adapter === 'auto' });
|
|
350
|
+
if (selected.adapter === 'static') Object.assign(options, selected);
|
|
351
|
+
else if (options.adapter === 'auto') options.adapter = 'react';
|
|
352
|
+
}
|
|
223
353
|
if (options.adapter === 'static') {
|
|
224
354
|
const { buildStatic } = await import('./build/static-adapter.mjs');
|
|
225
355
|
await buildStatic(root, options);
|
|
226
356
|
} else {
|
|
227
357
|
const { build } = await import('./build/index.mjs');
|
|
228
|
-
await build(root, { ...options, dev, envMode });
|
|
358
|
+
const result=await build(root, { ...options, dev, envMode });
|
|
359
|
+
if(!dev&&result.compilationCache?.hits){
|
|
360
|
+
const {hits,reusedBytes}=result.compilationCache;
|
|
361
|
+
console.log(`Compilation cache: ${hits} ${hits===1?'graph':'graphs'} reused (${Math.ceil(reusedBytes/1024)} KiB).`);
|
|
362
|
+
}
|
|
363
|
+
if(result.reactCompilerReport?.enabled){
|
|
364
|
+
const {printReactCompilerReport}=await import('./build/react-compiler-report.mjs');
|
|
365
|
+
printReactCompilerReport(result.reactCompilerReport,{details:false});
|
|
366
|
+
}
|
|
229
367
|
}
|
|
230
368
|
console.log(`PRNext built ${root} in ${((performance.now() - started) / 1000).toFixed(2)}s`);
|
|
231
369
|
return;
|
|
232
370
|
}
|
|
233
371
|
const binary = await resolveNativeBinary();
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const launch = () => spawn(binary, [command === 'routes' ? 'routes' : 'start', root,
|
|
241
|
-
...(command === 'routes' ? [] : ['--worker', path.join(root, outputDirectory, 'runtime/worker.mjs'), '--node', process.execPath,
|
|
242
|
-
'--shutdown-on-stdin-eof']), ...args], { stdio: ['pipe', 'inherit', 'inherit'], windowsHide: true, env: { ...process.env, NODE_ENV: process.env.NODE_ENV || (command === 'dev' ? 'development' : 'production') } });
|
|
243
|
-
let server;
|
|
244
|
-
let stopping = false;
|
|
245
|
-
let watcher;
|
|
246
|
-
let rebuildChild;
|
|
247
|
-
let timer;
|
|
248
|
-
const devStateFile = path.join(root, '.prnext-dev.json');
|
|
249
|
-
const changedFiles = new Set();
|
|
250
|
-
async function notifyDev(value) {
|
|
251
|
-
const temporary = `${devStateFile}.${process.pid}.tmp`;
|
|
252
|
-
await writeFile(temporary, JSON.stringify(value));
|
|
253
|
-
await rename(temporary, devStateFile);
|
|
372
|
+
if(command==='dev') {const {develop}=await import('./runtime/development.mjs');await develop(root,args,binary);return;}
|
|
373
|
+
const outputDirectory = await readBuildDirectory(root);
|
|
374
|
+
const serverEnvironment = {...process.env,NODE_ENV:process.env.NODE_ENV||'production'};
|
|
375
|
+
if(command==='start'&&serverEnvironment.NODE_ENV!=='development'){
|
|
376
|
+
if(replaceWithNative(binary,['start',root,'--worker',path.join(root,outputDirectory,'runtime/worker.mjs'),
|
|
377
|
+
'--node',process.execPath,...args],serverEnvironment))return;
|
|
254
378
|
}
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
process.once('SIGTERM', shutdown);
|
|
265
|
-
if (command !== 'dev') {
|
|
266
|
-
server = launch();
|
|
267
|
-
await done(server).catch(error => { if (!stopping) throw error; });
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
let building = false;
|
|
271
|
-
let queued = false;
|
|
272
|
-
let attempted = false;
|
|
273
|
-
const {createDevChangeFilter} = await import('./runtime/dev-file-changes.mjs');
|
|
274
|
-
const filterChanges = createDevChangeFilter(root);
|
|
275
|
-
async function rebuild() {
|
|
276
|
-
if (stopping) return;
|
|
277
|
-
if (building) { queued = true; return; }
|
|
278
|
-
building = true;
|
|
279
|
-
const pending = [...changedFiles];
|
|
280
|
-
changedFiles.clear();
|
|
281
|
-
let buildOutput = '';
|
|
282
|
-
try {
|
|
283
|
-
const actualChanges = await filterChanges(pending);
|
|
284
|
-
if (stopping) return;
|
|
285
|
-
if (attempted && !actualChanges.length) return;
|
|
286
|
-
attempted = true;
|
|
287
|
-
const changed = actualChanges.slice(0, 32).map(file => file.slice(0, 256));
|
|
288
|
-
await notifyDev({ state: 'building' });
|
|
289
|
-
if (actualChanges.some(file => /(?:^|\/)(?:(?:next|prnext|contentlayer)\.config\.|package(?:-lock)?\.json$|(?:pnpm-lock\.yaml|yarn\.lock)$)/.test(file))) { await stop(rebuildChild); rebuildChild = undefined; }
|
|
290
|
-
if (stopping) return;
|
|
291
|
-
if (!rebuildChild || rebuildChild.exitCode !== null || rebuildChild.signalCode) {
|
|
292
|
-
rebuildChild = spawn(process.execPath, [path.join(path.dirname(self), 'build/dev-worker.mjs')], { stdio: ['inherit', 'pipe', 'pipe', 'ipc'], env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'development' } });
|
|
293
|
-
}
|
|
294
|
-
const stdout = chunk => { process.stdout.write(chunk); buildOutput = (buildOutput + chunk).slice(-12_000); };
|
|
295
|
-
const stderr = chunk => { process.stderr.write(chunk); buildOutput = (buildOutput + chunk).slice(-12_000); };
|
|
296
|
-
rebuildChild.stdout.on('data', stdout); rebuildChild.stderr.on('data', stderr);
|
|
297
|
-
let result;
|
|
298
|
-
try {
|
|
299
|
-
result = await new Promise((resolve, reject) => {
|
|
300
|
-
const worker = rebuildChild;
|
|
301
|
-
const cleanup = () => { worker.off('message', message); worker.off('exit', exited); worker.off('error', failed); };
|
|
302
|
-
const message = value => { if (value?.type === 'result') { cleanup(); resolve(value); } };
|
|
303
|
-
const exited = (code, signal) => { cleanup(); reject(new Error(`Compiler exited ${signal || code}`)); };
|
|
304
|
-
const failed = error => { cleanup(); reject(error); };
|
|
305
|
-
worker.on('message', message); worker.once('exit', exited); worker.once('error', failed);
|
|
306
|
-
worker.send({type:'build',root});
|
|
307
|
-
});
|
|
308
|
-
} finally { rebuildChild.stdout.off('data', stdout); rebuildChild.stderr.off('data', stderr); }
|
|
309
|
-
if (result.recycle) { await stop(rebuildChild); rebuildChild = undefined; }
|
|
310
|
-
if (!result.ok) throw new Error(result.error);
|
|
311
|
-
filterChanges.acceptInputs(result.inputs || []);
|
|
312
|
-
if (!stopping) {
|
|
313
|
-
await stop(server);
|
|
314
|
-
outputDirectory = await readBuildDirectory(root);
|
|
315
|
-
await watcher.refresh();
|
|
316
|
-
// Shutdown can arrive while the previous server is draining. Never
|
|
317
|
-
// leave a replacement running after shutdown already stopped its peers.
|
|
318
|
-
if (stopping) return;
|
|
319
|
-
for (const file of changedFiles) {
|
|
320
|
-
if (file === outputDirectory || file.startsWith(outputDirectory + '/') || outputDirectory.startsWith(file + '/')) changedFiles.delete(file);
|
|
321
|
-
}
|
|
322
|
-
if (!changedFiles.size) { clearTimeout(timer); queued = false; }
|
|
323
|
-
server = launch();
|
|
324
|
-
server.on('error', error => console.error(`PRNext server: ${error.message}`));
|
|
325
|
-
const manifest = JSON.parse(await readFile(path.join(root, outputDirectory, 'manifest.json'), 'utf8'));
|
|
326
|
-
if (stopping) return;
|
|
327
|
-
await notifyDev({ state: 'ready', buildId: manifest.buildId, clientManifest: manifest.devClient, changed });
|
|
328
|
-
console.log('Source compiled. React Fast Refresh applied in connected browsers.');
|
|
329
|
-
}
|
|
330
|
-
} catch (error) { if (!stopping) { console.error(`Build failed: ${error.message}`); await notifyDev({ state: 'error', error: buildOutput || error.message }); } }
|
|
331
|
-
finally { building = false; if (queued && !stopping) { queued = false; void rebuild(); } }
|
|
332
|
-
}
|
|
333
|
-
const { watchProject } = await import('./runtime/watch-project.mjs');
|
|
334
|
-
watcher = await watchProject(root, {
|
|
335
|
-
ignore: relative => relative === outputDirectory || relative.startsWith(outputDirectory + '/'),
|
|
336
|
-
onError: error => { console.error(`PRNext development watcher: ${error.message}`); process.exitCode = 1; void shutdown(); },
|
|
337
|
-
onChange: filename => {
|
|
338
|
-
if (stopping) return;
|
|
339
|
-
const relative = String(filename || '').replaceAll(path.sep, '/');
|
|
340
|
-
if (relative === outputDirectory || relative.startsWith(outputDirectory + '/') || !shouldWatchProjectFile(filename)) return;
|
|
341
|
-
changedFiles.add(String(filename).replaceAll(path.sep, '/'));
|
|
342
|
-
clearTimeout(timer);
|
|
343
|
-
timer = setTimeout(() => { if (changedFiles.size) void rebuild(); }, 150);
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
if (stopping) { watcher.close(); return; }
|
|
347
|
-
await rebuild();
|
|
379
|
+
const server=spawn(binary,[command==='routes'?'routes':'start',root,
|
|
380
|
+
...(command==='routes'?[]:['--worker',path.join(root,outputDirectory,'runtime/worker.mjs'),'--node',process.execPath,'--shutdown-on-stdin-eof']),...args],
|
|
381
|
+
{stdio:['pipe','inherit','inherit'],windowsHide:true,env:serverEnvironment});
|
|
382
|
+
let stopping=false;
|
|
383
|
+
const shutdown=async()=>{if(stopping)return;stopping=true;await stop(server);};
|
|
384
|
+
process.once('SIGINT',shutdown);process.once('SIGTERM',shutdown);
|
|
385
|
+
try{await done(server).catch(error=>{if(!stopping)throw error;});}
|
|
386
|
+
finally{process.off('SIGINT',shutdown);process.off('SIGTERM',shutdown);await stop(server);}
|
|
387
|
+
|
|
348
388
|
}
|
|
349
389
|
|
|
350
390
|
main().catch(error => { console.error(`PRNext: ${error.message}`); process.exitCode = 1; });
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A shared budget across layout slots, rather than a separate page budget at
|
|
4
|
+
// every nesting level. Only inactive branches enter this registry.
|
|
5
|
+
function createActivityCache({ limit = 4, ttl = 120000, now = Date.now, schedule = setTimeout, cancel = clearTimeout } = {}) {
|
|
6
|
+
const entries = new Map(); let timer;
|
|
7
|
+
function arm() {
|
|
8
|
+
cancel(timer); timer = undefined;
|
|
9
|
+
if (entries.size) {
|
|
10
|
+
timer = schedule(prune, Math.max(1, Math.min(...[...entries.values()].map(entry => entry.expires)) - now()));
|
|
11
|
+
timer?.unref?.();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function prune() {
|
|
15
|
+
for (const [key, entry] of entries) if (entry.expires <= now()) { entries.delete(key); entry.evict(); }
|
|
16
|
+
while (entries.size > limit) { const [key, entry] = entries.entries().next().value; entries.delete(key); entry.evict(); }
|
|
17
|
+
arm();
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
retain(key, evict) { if (!entries.has(key)) entries.set(key, { evict, expires: now() + ttl }); prune(); },
|
|
21
|
+
release(key) { entries.delete(key); arm(); },
|
|
22
|
+
clear() { const previous = [...entries.values()]; entries.clear(); cancel(timer); timer = undefined; for (const entry of previous) entry.evict(); },
|
|
23
|
+
get size() { return entries.size; },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
module.exports = { createActivityCache };
|
package/compat/after.cjs
CHANGED
|
@@ -4,11 +4,11 @@ const { AsyncLocalStorage } = require('node:async_hooks');
|
|
|
4
4
|
function after(task) {
|
|
5
5
|
const context = require('./headers.cjs').currentRequest();
|
|
6
6
|
if (require('./data-cache.cjs').inCacheScope()) throw new Error('after() cannot be registered inside a cache scope');
|
|
7
|
-
if (!context.afterScope || !['route', 'action', 'after'].includes(context.phase)) throw new Error('after()
|
|
7
|
+
if (!context.afterScope || !['route', 'action', 'render', 'after'].includes(context.phase)) throw new Error('after() requires a Node Server Component, Route Handler or Server Action. Edge after() is not implemented.');
|
|
8
8
|
context.afterScope.add(task, context, AsyncLocalStorage.snapshot());
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onComplete, report = error => console.error('[prnext] after() failed:', error?.stack || error) } = {}) {
|
|
11
|
+
function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onComplete, onRegistered, report = error => console.error('[prnext] after() failed:', error?.stack || error) } = {}) {
|
|
12
12
|
const queue = []; let count = 0, closed = false, finishing;
|
|
13
13
|
const deadline = Date.now() + timeoutMs;
|
|
14
14
|
const track = promise => signal?.[Symbol.for('prnext.request-work')]?.(promise, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
@@ -18,6 +18,7 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
|
|
|
18
18
|
if (closed) throw new Error('after() request has already completed');
|
|
19
19
|
if (typeof task !== 'function' && (!task || typeof task.then !== 'function')) throw new TypeError('after() expects a callback or a Promise');
|
|
20
20
|
if (++count > 128) throw new Error('after() exceeds 128 tasks per request');
|
|
21
|
+
if (count === 1) onRegistered?.();
|
|
21
22
|
const pending = typeof task === 'function' ? null : observe(task);
|
|
22
23
|
if (pending) track(pending);
|
|
23
24
|
queue.push({ task: pending ? null : task, pending, context, run });
|
|
@@ -27,8 +28,9 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
|
|
|
27
28
|
try {
|
|
28
29
|
while (queue.length) {
|
|
29
30
|
const item = queue.shift(), remaining = deadline - Date.now();
|
|
30
|
-
if (remaining <= 0) { const error = new Error('after() exceeded the request maxDuration'); report(error); onTimeout(error); break; }
|
|
31
|
+
if (remaining <= 0) { const error = new Error('after() exceeded the request maxDuration'); await item.run(()=>report(error)); onTimeout(error); break; }
|
|
31
32
|
const abort = new AbortController();
|
|
33
|
+
item.context.afterRestricted ||= item.context.phase === 'render';
|
|
32
34
|
item.context.cookies._mutable = false;
|
|
33
35
|
item.context.phase = 'after';
|
|
34
36
|
item.context.signal = abort.signal;
|
|
@@ -38,7 +40,7 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
|
|
|
38
40
|
let timer;
|
|
39
41
|
try {
|
|
40
42
|
const outcome = await Promise.race([work, new Promise(resolve => { timer = setTimeout(() => { const error = new Error('after() exceeded the request maxDuration'); abort.abort(error); resolve({ error }); }, remaining); })]);
|
|
41
|
-
if ('error' in outcome) report(outcome.error);
|
|
43
|
+
if ('error' in outcome) await item.run(()=>report(outcome.error));
|
|
42
44
|
if (abort.signal.aborted) { onTimeout(outcome.error); break; }
|
|
43
45
|
} finally { clearTimeout(timer); item.context.cacheState.closed = true; await onComplete?.(item.context); }
|
|
44
46
|
}
|
|
@@ -1,8 +1,53 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
'use strict';
|
|
3
3
|
const React = require('react');
|
|
4
|
+
const { AppRouterContext } = require('./app-context.cjs');
|
|
4
5
|
const LayoutContext = React.createContext({ children: [] });
|
|
5
6
|
const LayoutCacheContext = React.createContext(null);
|
|
7
|
+
const ActivityCacheContext = React.createContext(null);
|
|
8
|
+
|
|
9
|
+
function ActivitySlot({ identity, children, authorizationKey }) {
|
|
10
|
+
const retention = React.useContext(ActivityCacheContext);
|
|
11
|
+
authorizationKey ??= retention?.authorizationKey;
|
|
12
|
+
const router = React.useContext(AppRouterContext);
|
|
13
|
+
const epoch = retention?.epoch ?? 0;
|
|
14
|
+
const authorizationEpoch = retention?.authorizationEpoch ?? 0;
|
|
15
|
+
const registrations = React.useRef(new Set());
|
|
16
|
+
const serial = React.useRef(0);
|
|
17
|
+
const [saved, setSaved] = React.useState(() => ({ epoch, authorizationEpoch, active: identity, entries: new Map() }));
|
|
18
|
+
let entries = saved.entries;
|
|
19
|
+
const current = entries.get(identity);
|
|
20
|
+
if (saved.epoch !== epoch || saved.authorizationEpoch !== authorizationEpoch || current?.children !== children || current?.router !== router || current?.authorizationKey !== authorizationKey) {
|
|
21
|
+
const sameAuthorization = saved.authorizationEpoch === authorizationEpoch && !(current?.authorizationKey && authorizationKey && current.authorizationKey !== authorizationKey);
|
|
22
|
+
entries = saved.epoch === epoch && sameAuthorization ? new Map(entries) : new Map();
|
|
23
|
+
const preserve = current && saved.authorizationEpoch === authorizationEpoch && (sameAuthorization && (saved.epoch === epoch || saved.active === identity)
|
|
24
|
+
|| !retention?.strictAuthorization && saved.active === identity);
|
|
25
|
+
entries.set(identity, { children, router, authorizationKey, token: preserve ? current.token : {}, reactKey: preserve ? current.reactKey : ++serial.current });
|
|
26
|
+
setSaved({ epoch, authorizationEpoch, active: identity, entries });
|
|
27
|
+
}
|
|
28
|
+
React.useLayoutEffect(() => {
|
|
29
|
+
const cache = retention?.cache;
|
|
30
|
+
if (!cache) return;
|
|
31
|
+
const inactive = new Set([...entries].filter(([key]) => key !== identity).map(([, entry]) => entry.token));
|
|
32
|
+
for (const token of registrations.current) if (!inactive.has(token)) cache.release(token);
|
|
33
|
+
for (const [key, entry] of entries) {
|
|
34
|
+
if (key === identity) cache.release(entry.token);
|
|
35
|
+
else cache.retain(entry.token, () => setSaved(previous => {
|
|
36
|
+
if (!previous.entries.has(key)) return previous;
|
|
37
|
+
const next = new Map(previous.entries); next.delete(key); return { ...previous, entries: next };
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
registrations.current = inactive;
|
|
41
|
+
}, [entries, identity, retention?.cache]);
|
|
42
|
+
React.useLayoutEffect(() => () => {
|
|
43
|
+
for (const token of registrations.current) retention?.cache.release(token);
|
|
44
|
+
registrations.current.clear();
|
|
45
|
+
}, [retention?.cache]);
|
|
46
|
+
// Identical boundary structure during SSR and hydration. Hidden branches get
|
|
47
|
+
// their original router context and their effects are disconnected by React.
|
|
48
|
+
return [...entries].map(([key, entry]) => React.createElement(React.Activity, { key: entry.reactKey, mode: key === identity ? 'visible' : 'hidden' },
|
|
49
|
+
React.createElement(AppRouterContext.Provider, { value: key === identity ? router : entry.router }, entry.children)));
|
|
50
|
+
}
|
|
6
51
|
|
|
7
52
|
function LayoutProvider({ id, segments, children }) {
|
|
8
53
|
const cache = React.useContext(LayoutCacheContext);
|
|
@@ -13,15 +58,20 @@ function LayoutProvider({ id, segments, children }) {
|
|
|
13
58
|
return React.createElement(LayoutContext.Provider, { value: current }, children);
|
|
14
59
|
}
|
|
15
60
|
|
|
16
|
-
function LayoutSlot({ id, preserve, children }) {
|
|
61
|
+
function LayoutSlot({ id, preserve, children, activityKey, activityPreserve, authorizationKey }) {
|
|
17
62
|
const cache = React.useContext(LayoutCacheContext);
|
|
18
63
|
const previous = React.useRef(children);
|
|
19
64
|
const current = preserve && cache && Object.hasOwn(cache.slots, id) ? cache.slots[id] : preserve ? previous.current : children;
|
|
20
65
|
React.useLayoutEffect(() => { previous.current = current; if (cache && id) cache.slots[id] = current; }, [cache, id, current]);
|
|
21
|
-
|
|
66
|
+
const previousKey = React.useRef(activityKey);
|
|
67
|
+
const identity = preserve || activityPreserve ? previousKey.current : activityKey;
|
|
68
|
+
React.useLayoutEffect(() => { previousKey.current = identity; }, [identity]);
|
|
69
|
+
return activityKey === undefined ? current ?? null : React.createElement(ActivitySlot, { identity, authorizationKey }, current);
|
|
22
70
|
}
|
|
23
71
|
|
|
24
72
|
module.exports.LayoutContext = LayoutContext;
|
|
25
73
|
module.exports.LayoutCacheContext = LayoutCacheContext;
|
|
26
74
|
module.exports.LayoutProvider = LayoutProvider;
|
|
27
75
|
module.exports.LayoutSlot = LayoutSlot;
|
|
76
|
+
module.exports.ActivitySlot = ActivitySlot;
|
|
77
|
+
module.exports.ActivityCacheContext = ActivityCacheContext;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Native decoding avoids a JavaScript callback for each Flight byte. The loop
|
|
3
|
+
// remains available on browsers that do not implement Uint8Array.fromBase64.
|
|
4
|
+
function decodeBase64(value) {
|
|
5
|
+
if(typeof Uint8Array.fromBase64==='function')return Uint8Array.fromBase64(value);
|
|
6
|
+
const binary=atob(value),bytes=new Uint8Array(binary.length);
|
|
7
|
+
for(let n=0;n<binary.length;n++)bytes[n]=binary.charCodeAt(n);
|
|
8
|
+
return bytes;
|
|
9
|
+
}
|
|
10
|
+
module.exports={decodeBase64};
|
|
@@ -139,10 +139,19 @@ async function customCachedValue(key, kind, producer, options) {
|
|
|
139
139
|
const timestamp = Date.now();
|
|
140
140
|
const record = { revision, timestamp, work: undefined, metadata: undefined };
|
|
141
141
|
work = (async () => {
|
|
142
|
+
let lease,canStore=true;
|
|
143
|
+
try {
|
|
144
|
+
if(typeof handler.acquire==='function'){
|
|
145
|
+
try{
|
|
146
|
+
lease=await call(handler,'acquire',[key,{signal:context.signal}],context);
|
|
147
|
+
const shared=await call(handler,'get',[key,tags],context);
|
|
148
|
+
if(shared){const sharedInfo=metadata(shared);if(Date.now()-sharedInfo.timestamp<Math.min(sharedInfo.expire,sharedInfo.revalidate)*1000){record.metadata=sharedInfo;options.onReadMetadata?.(sharedInfo);return await read(shared,context);}void shared.value.cancel().catch(()=>{});}
|
|
149
|
+
}catch{context.signal?.throwIfAborted();canStore=false;}
|
|
150
|
+
}
|
|
142
151
|
const result = await producer();
|
|
143
152
|
const info = resolveMetadata();
|
|
144
153
|
record.metadata = info;
|
|
145
|
-
if (result.byteLength <= MAX_BYTES && revision === (revisions.get(handler) || 0)) {
|
|
154
|
+
if (canStore && result.byteLength <= MAX_BYTES && revision === (revisions.get(handler) || 0)) {
|
|
146
155
|
try {
|
|
147
156
|
const changed = await expiration(handler, [...new Set([...tags, ...info.tags])], context);
|
|
148
157
|
// Many backends record Date.now() millisecond timestamps. Treat the
|
|
@@ -150,12 +159,13 @@ async function customCachedValue(key, kind, producer, options) {
|
|
|
150
159
|
// hide an invalidation which actually happened after the read started.
|
|
151
160
|
if ((changed === Infinity || changed < Math.floor(timestamp)) && revision === (revisions.get(handler) || 0)) {
|
|
152
161
|
const value = stream(result);
|
|
153
|
-
try { await call(handler, 'set', [key, Promise.resolve({ value, tags: info.tags, stale: info.stale, revalidate: info.revalidate, expire: info.expire, timestamp })], context); }
|
|
162
|
+
try { await call(handler, 'set', [key, Promise.resolve({ value, tags: info.tags, stale: info.stale, revalidate: info.revalidate, expire: info.expire, timestamp }),{lease,softTags:tags}], context); }
|
|
154
163
|
catch { if (!value.locked) void value.cancel().catch(() => {}); }
|
|
155
164
|
}
|
|
156
165
|
} catch { context.signal?.throwIfAborted(); }
|
|
157
166
|
}
|
|
158
167
|
return result;
|
|
168
|
+
}finally{try{await lease?.release();}catch{/* A failed cache cleanup does not replay application work. */}}
|
|
159
169
|
})();
|
|
160
170
|
record.work = work;
|
|
161
171
|
if (pendingCount < 64) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Versioned private framing: bounded JSON metadata followed by uninterpreted bytes.
|
|
3
|
+
const TYPE = 'application/vnd.prnext.cache-v2';
|
|
4
|
+
const MAX_VALUE = 2 * 1024 * 1024;
|
|
5
|
+
const MAX_META = 2 * 1024 * 1024;
|
|
6
|
+
function encode(operation) {
|
|
7
|
+
const { value, ...metadata } = operation;
|
|
8
|
+
if (value !== undefined && !Buffer.isBuffer(value) && !(value instanceof Uint8Array)) throw new TypeError('Cache wire values must be bytes');
|
|
9
|
+
const bytes = value === undefined ? undefined : Buffer.isBuffer(value) ? value : Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
10
|
+
if (bytes && (metadata.op !== 'commit' || bytes.length > MAX_VALUE)) throw new Error('Invalid cache wire value');
|
|
11
|
+
if (metadata.op === 'commit' && !bytes) throw new Error('Cache commit requires bytes');
|
|
12
|
+
const json = Buffer.from(JSON.stringify(metadata));
|
|
13
|
+
if (json.length > MAX_META) throw new Error('Cache wire metadata exceeds limit');
|
|
14
|
+
const prefix = Buffer.allocUnsafe(4); prefix.writeUInt32BE(json.length);
|
|
15
|
+
return bytes ? [prefix, json, bytes] : [prefix, json];
|
|
16
|
+
}
|
|
17
|
+
function decode(bytes) {
|
|
18
|
+
if (bytes.length < 4) throw new Error('Truncated cache wire frame');
|
|
19
|
+
const length = bytes.readUInt32BE(0);
|
|
20
|
+
if (length > MAX_META || length > bytes.length - 4) throw new Error('Invalid cache wire metadata length');
|
|
21
|
+
const value = JSON.parse(bytes.subarray(4, 4 + length).toString('utf8'));
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid cache wire metadata');
|
|
23
|
+
const raw = bytes.subarray(4 + length);
|
|
24
|
+
if (raw.length > MAX_VALUE || (value.value !== true && raw.length) || (value.value !== undefined && value.value !== true)) throw new Error('Invalid cache wire payload');
|
|
25
|
+
if (value.value === true) value.value = raw;
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
module.exports = { TYPE, MAX_VALUE, MAX_META, encode, decode };
|
package/compat/cache.cjs
CHANGED
|
@@ -96,4 +96,5 @@ function unstable_noStore() {
|
|
|
96
96
|
}
|
|
97
97
|
function refresh() { cacheState(mutationContext('refresh', true)).refresh = true; }
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
const { navigation, prefetch } = require('./navigation-stages.cjs');
|
|
100
|
+
module.exports = { unstable_cache, revalidateTag, updateTag, revalidatePath, unstable_noStore, refresh, cacheTag, cacheLife, navigation, prefetch };
|
package/compat/cache.d.cts
CHANGED
|
@@ -34,3 +34,7 @@ export function updateTag(tag: string): void;
|
|
|
34
34
|
export function revalidatePath(path: string, type?: 'page' | 'layout'): void;
|
|
35
35
|
export function unstable_noStore(): void;
|
|
36
36
|
export function refresh(): void;
|
|
37
|
+
/** Defer runtime work until navigation, while allowing static prerenders. */
|
|
38
|
+
export function navigation(): Promise<void>;
|
|
39
|
+
/** Defer runtime work until a per-link prefetch or navigation. */
|
|
40
|
+
export function prefetch(): Promise<void>;
|