@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/runtime/development.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {createDevCompilerBroker} from './dev-compiler-broker.mjs';
|
|
2
2
|
import {spawn} from 'node:child_process';
|
|
3
3
|
import {mkdir,readdir,readFile,writeFile,rm,link,copyFile,realpath,lstat} from 'node:fs/promises';
|
|
4
|
-
import {randomUUID} from 'node:crypto';
|
|
4
|
+
import {randomUUID,randomBytes} from 'node:crypto';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import {fileURLToPath} from 'node:url';
|
|
7
7
|
import {rename} from './fs.mjs';
|
|
@@ -20,7 +20,13 @@ import {createDevRuntimePool} from './dev-runtime-pool.mjs';
|
|
|
20
20
|
import {tryDevCssUpdate} from '../build/dev-css-update.mjs';
|
|
21
21
|
import {devBuildReason} from './dev-build-reasons.mjs';
|
|
22
22
|
import {publishDependencyGraph,affectedDependencies,canSkipUnopenedRoutes} from './dev-dependency-graph.mjs';
|
|
23
|
+
import {selectiveDevelopmentRoutes} from './dev-route-selection.mjs';
|
|
23
24
|
import {prepareDevNotification} from './dev-notification.mjs';
|
|
25
|
+
import {writeAtomicFile} from './atomic-file.mjs';
|
|
26
|
+
import {lazyImportIdentifier,materializeDevImport} from './dev-lazy-imports.mjs';
|
|
27
|
+
import {discardLazyBuild} from './dev-lazy-staging.mjs';
|
|
28
|
+
import {pruneDevelopmentStyles} from './dev-style-retention.mjs';
|
|
29
|
+
import {createDevelopmentSessionOwner,pruneDevelopmentSessions,createRetiredGenerationCleanup} from './dev-session-retention.mjs';
|
|
24
30
|
|
|
25
31
|
const packageRoot=fileURLToPath(new URL('../',import.meta.url));
|
|
26
32
|
export function developmentRoute(routes,raw,config={}) {
|
|
@@ -79,23 +85,33 @@ function serverOptions(args) {
|
|
|
79
85
|
return {hostname,port,backend,hostAi,profile,safe};
|
|
80
86
|
}
|
|
81
87
|
export async function develop(projectRoot,args,binary) {
|
|
82
|
-
|
|
88
|
+
let entered=false;
|
|
89
|
+
try{return await withBuildLock(projectRoot,root=>{entered=true;return runDevelopment(root,args,binary);},{development:true});}
|
|
90
|
+
finally{if(entered)try{const {pruneCompilationStorage}=await import('../build/compilation-storage.mjs');await pruneCompilationStorage(projectRoot);}catch{/* Optional compilation-cache maintenance cannot change shutdown correctness. */}}
|
|
83
91
|
}
|
|
84
92
|
async function runDevelopment(projectRoot,args,binary) {
|
|
85
93
|
const root=await realpath(projectRoot),options=serverOptions(args),session=randomUUID();
|
|
94
|
+
// Private IPC state survives compiler recycling without exposing the key in
|
|
95
|
+
// the browser's session metadata or any development manifest/notification.
|
|
96
|
+
const devActionSecret=randomBytes(32).toString('base64');
|
|
86
97
|
const stateFile=path.join(root,'.prnext-dev.json'),home=path.join(root,'.prnext-dev',session);
|
|
87
98
|
const diagnosticsDirectory=process.env.PRNEXT_INSPECT_DIR||path.join(home,'diagnostics');
|
|
88
99
|
// The session is ours; never clean a path obtained from application config.
|
|
89
100
|
const container=path.join(root,'.prnext-dev'),info=await lstat(container).catch(error=>{if(error.code!=='ENOENT')throw error;});
|
|
90
101
|
if(info&&(!info.isDirectory()||info.isSymbolicLink()))throw Error('Development directory must be a real directory');
|
|
102
|
+
await pruneDevelopmentSessions(root);
|
|
91
103
|
await mkdir(home,{recursive:true});
|
|
104
|
+
const sessionOwner=createDevelopmentSessionOwner(home,{session});
|
|
105
|
+
if(options.safe)sessionOwner.disable();
|
|
92
106
|
await mkdir(diagnosticsDirectory,{recursive:true,mode:0o700});
|
|
93
107
|
let compiler,gateway,watcher,timer,tools,hostRelay,hostRelayTask,stopping=false,current,discovery,typeChecker,readyState={state:'building'},typeErrors=[],sequence=0;
|
|
94
108
|
let dependencyIndexValid=false;
|
|
95
109
|
const lifecycle=new AbortController();
|
|
96
110
|
let queue=Promise.resolve(),stateQueue=Promise.resolve(),output='.prnext',changes=new Set(),active=new Map(),failed=new Set();
|
|
97
111
|
const failedPaths=new Set();
|
|
98
|
-
const
|
|
112
|
+
const requestedImports=new Set();
|
|
113
|
+
const generations=[],styleGenerations=[],retiring=new Set(),pins=new Map(),routeCache=new Map(),filter=createDevChangeFilter(root,{directoryEventsAreRecursive:process.platform==='darwin',ignore:relative=>relative===output||relative.startsWith(output+'/')});
|
|
114
|
+
const retiredCleanup=createRetiredGenerationCleanup({root,home,gateway:{request:(...args)=>gateway.request(...args)},stop});
|
|
99
115
|
const compilerBroker=await createDevCompilerBroker({compile:(pattern,sources)=>serialized(async()=>{
|
|
100
116
|
if(stopping)throw Error('Development is stopping');
|
|
101
117
|
if(!discovery)await discover();
|
|
@@ -106,10 +122,10 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
106
122
|
if(!current||patterns.some(p=>!current.patterns.has(p)))await rebuild([...active.keys()]);
|
|
107
123
|
return current.directory;
|
|
108
124
|
})});
|
|
109
|
-
const runtimePool=createDevRuntimePool({binary,root,home,environment:{PRNEXT_DEV_COMPILER:compilerBroker.descriptor,PRNEXT_INSPECT_DIR:diagnosticsDirectory,...(options.profile?{PRNEXT_PROFILE:options.profile}:{})},onChild:child=>captureOutput({stderr:child.stderr,once:child.once.bind(child)},'runtime',{forwardStdout:false})});
|
|
125
|
+
const runtimePool=createDevRuntimePool({binary,root,home,environment:{PRNEXT_DEV_COMPILER:compilerBroker.descriptor,PRNEXT_INSPECT_DIR:diagnosticsDirectory,...(options.profile?{PRNEXT_PROFILE:options.profile}:{})},onBeforeChild:()=>sessionOwner.beforeSpawn(),onReady:(child,info,ticket)=>sessionOwner.spawned(ticket,child,[info.pid]),onChild:child=>captureOutput({stderr:child.stderr,once:child.once.bind(child)},'runtime',{forwardStdout:false})});
|
|
110
126
|
const toolSnapshot=()=>({...readyState,safeMode:options.safe,sequence:readyState.sequence??sequence,session,typeErrors,
|
|
111
127
|
routes:(discovery?.project.routes||[]).filter(route=>!route.internal).map(route=>({path:(discovery.config.basePath||'')+(route.pattern==='/'?'/':route.pattern),kind:route.kind,source:route.file?path.relative(root,route.file).replaceAll(path.sep,'/'):undefined,compiled:current?.patterns.has(route.pattern)||false,pinned:[...pins.values()].some(pin=>pin.pattern===route.pattern)})),
|
|
112
|
-
cache:{activeRoutes:active.size,pinnedRoutes:new Set([...pins.values()].map(pin=>pin.pattern)).size,retainedGenerations:generations.length,lookupEntries:routeCache.size,compiler:readyState.compiler,transforms:readyState.transforms,persistentTransforms:true,dependencyIndex:dependencyIndexValid?{valid:true,...current?.manifest.devDependencyIndex}:{valid:false}}});
|
|
128
|
+
cache:{activeRoutes:active.size,pinnedRoutes:new Set([...pins.values()].map(pin=>pin.pattern)).size,retainedGenerations:generations.length,retiredCleanupPending:retiredCleanup.pending,lookupEntries:routeCache.size,compiler:readyState.compiler,transforms:readyState.transforms,persistentTransforms:true,dependencyIndex:dependencyIndexValid?{valid:true,...current?.manifest.devDependencyIndex}:{valid:false}}});
|
|
113
129
|
function captureOutput(child,source,{forwardStdout=true}={}){
|
|
114
130
|
for(const [stream,level] of [[child.stdout,'info'],[child.stderr,'error']]){
|
|
115
131
|
stream?.setEncoding('utf8');
|
|
@@ -124,10 +140,23 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
124
140
|
const route=developmentRoute(discovery.project.routes,raw,discovery.config);
|
|
125
141
|
if(routeCache.size>=256)routeCache.delete(routeCache.keys().next().value);routeCache.set(key,route);return route;
|
|
126
142
|
}
|
|
143
|
+
let publicationFailures=0,lastPublicationFailure;
|
|
127
144
|
const writeState=value=>{
|
|
128
|
-
const task=stateQueue.then(async()=>{
|
|
129
|
-
const
|
|
130
|
-
|
|
145
|
+
const task=stateQueue.then(async()=>{
|
|
146
|
+
const next=value||readyState;
|
|
147
|
+
const {notification,text}=prepareDevNotification({...next,safeMode:options.safe,typeErrors,...(publicationFailures?{publication:{failures:publicationFailures,lastFailure:lastPublicationFailure}}:{})});
|
|
148
|
+
try{await writeAtomicFile(stateFile,text);}
|
|
149
|
+
catch(error){
|
|
150
|
+
publicationFailures++;lastPublicationFailure=error.atomicFile||{operation:'publish-development-state',code:error.code||'UNKNOWN'};
|
|
151
|
+
// A failed publication must leave the last complete state readable.
|
|
152
|
+
// This private bounded receipt contains no source path or payload.
|
|
153
|
+
await writeFile(path.join(diagnosticsDirectory,'state-publication.json'),JSON.stringify({failures:publicationFailures,lastFailure:lastPublicationFailure,lastSuccessfulSequence:readyState.sequence??null})+'\n',{mode:0o600}).catch(()=>{});
|
|
154
|
+
tools?.addServerLog('error','Development state publication failed.',{process:'compiler',...lastPublicationFailure});
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
readyState=next;
|
|
158
|
+
if(gateway&&!gateway.stopping)await gateway.request('notify',{value:notification});
|
|
159
|
+
});
|
|
131
160
|
stateQueue=task.catch(()=>{});return task;
|
|
132
161
|
};
|
|
133
162
|
const serialized=run=>{const task=queue.then(()=>{if(stopping)throw Error('Development is stopping');return run();});queue=task.catch(()=>{});return task;};
|
|
@@ -135,7 +164,9 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
135
164
|
if(!compiler||compiler.exitCode!==null||compiler.signalCode){
|
|
136
165
|
const cache=await privateCacheDirectory(root,'dev-compiler-v8');
|
|
137
166
|
await pruneCompilerCache(cache);
|
|
167
|
+
const ticket=sessionOwner.beforeSpawn();
|
|
138
168
|
compiler=spawn(process.execPath,[path.join(packageRoot,'build/dev-worker.mjs')],{stdio:['pipe','pipe','pipe','ipc'],windowsHide:true,env:{...process.env,NODE_ENV:process.env.NODE_ENV||'development',...(options.safe?{PRNEXT_BUILD_CACHE:'0'}:{}),...(cache?{NODE_COMPILE_CACHE:cache}:{})}});
|
|
169
|
+
sessionOwner.spawned(ticket,compiler);
|
|
139
170
|
captureOutput(compiler,'compiler');
|
|
140
171
|
}
|
|
141
172
|
const child=compiler;
|
|
@@ -153,14 +184,17 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
153
184
|
if(options.safe)discovery={...discovery,eager:true};
|
|
154
185
|
const {writeTypedRoutes}=await import('../build/typed-routes.mjs');
|
|
155
186
|
await writeTypedRoutes(root,discovery.project,discovery.config);
|
|
156
|
-
if(!typeChecker||typeChecker.inactive){const {startTypeChecker}=await import('../build/typescript-dev.mjs');typeChecker=await startTypeChecker(root,discovery.config,errors=>{typeErrors=errors;if(!stopping)void writeState().catch(console.error);});}
|
|
187
|
+
if(!typeChecker||typeChecker.inactive){const {startTypeChecker}=await import('../build/typescript-dev.mjs');typeChecker=await startTypeChecker(root,discovery.config,errors=>{typeErrors=errors;if(!stopping)void writeState().catch(console.error);},{onBeforeChild:()=>sessionOwner.beforeSpawn(),onChild:(child,ticket)=>sessionOwner.spawned(ticket,child)});}
|
|
157
188
|
}
|
|
158
189
|
async function launch(manifest,directory){
|
|
159
190
|
const runtime=options.safe?undefined:await runtimePool.take(directory).catch(error=>{tools?.addServerLog('warn','Worker warmup was unavailable; starting a fresh worker.',{process:'runtime'});return undefined;});
|
|
191
|
+
if(!runtime)sessionOwner.disable();
|
|
160
192
|
const env={...process.env,NODE_ENV:process.env.NODE_ENV||'development',PRNEXT_INSPECT_DIR:diagnosticsDirectory,PRNEXT_DEV_COMPILER:compilerBroker.descriptor};
|
|
161
193
|
delete env.PRNEXT_DEV_BROKER;
|
|
162
194
|
if(runtime)env.PRNEXT_DEV_BROKER=runtime.descriptor;
|
|
195
|
+
const ticket=sessionOwner.beforeSpawn();
|
|
163
196
|
const child=spawn(binary,['start',root,'--build-dir',directory,'--worker',path.join(root,directory,'runtime/worker.mjs'),'--node',process.execPath,'--hostname','127.0.0.1','--port','0','--shutdown-on-stdin-eof',...options.backend],{stdio:['pipe','pipe','pipe'],windowsHide:true,env});
|
|
197
|
+
sessionOwner.spawned(ticket,child);
|
|
164
198
|
captureOutput(child,'runtime',{forwardStdout:false});
|
|
165
199
|
let output='';
|
|
166
200
|
const port=await new Promise((resolve,reject)=>{
|
|
@@ -172,6 +206,8 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
172
206
|
return {id:manifest.buildId,child,runtime,port,directory};
|
|
173
207
|
}
|
|
174
208
|
async function rebuild(patterns,changedFiles=[],affectedRoutes,affectedModules,cause){
|
|
209
|
+
void retiredCleanup.retry();
|
|
210
|
+
if(retiredCleanup.pending>=64)throw Error('Development snapshots could not be removed. Close applications locking these files, then retry.');
|
|
175
211
|
const startedAt=Date.now(),started=performance.now(),phases={};let mark=started;
|
|
176
212
|
let reason=devBuildReason({initial:!current,safe:options.safe,changed:changedFiles,cause});
|
|
177
213
|
await writeState({state:'building',reason});
|
|
@@ -180,21 +216,21 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
180
216
|
if(!options.safe)void runtimePool.warm().catch(()=>{});
|
|
181
217
|
if(current&&!discovery.eager&&discovery.cssFast){
|
|
182
218
|
const directory=path.relative(root,path.join(home,'styles-'+randomUUID())).replaceAll(path.sep,'/');
|
|
183
|
-
const styles=await tryDevCssUpdate({root,source:path.join(root,current.cssDirectory||current.directory),destination:path.join(root,directory),manifest:current.manifest,changed:changedFiles,sequence:sequence+1,cssEngine:discovery.config.cssEngine,
|
|
219
|
+
const styles=await tryDevCssUpdate({root,source:path.join(root,current.cssDirectory||current.directory),baseSource:path.join(root,current.directory),destination:path.join(root,directory),manifest:current.manifest,changed:changedFiles,sequence:sequence+1,cssEngine:discovery.config.cssEngine,onFallback:code=>{reason=devBuildReason({changed:changedFiles,cause:code});}});
|
|
184
220
|
if(styles){
|
|
185
221
|
await gateway.request('assets',{generation:styles.buildId,directory});
|
|
186
222
|
try{styles.devDependencyIndex=await publishDependencyGraph(gateway,styles);dependencyIndexValid=true;}catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
|
|
187
|
-
sequence++;current.manifest=styles;current.cssDirectory=directory;current.cssOnly=true;current.affectedRoutes=undefined;current.changed=changed;
|
|
188
223
|
styleGenerations.push({id:styles.buildId,directory});
|
|
224
|
+
await pruneDevelopmentStyles(styleGenerations,gateway,root);
|
|
225
|
+
sequence++;current.manifest=styles;current.cssDirectory=directory;current.cssOnly=true;current.affectedRoutes=undefined;current.changed=changed;
|
|
189
226
|
reason=devBuildReason({cause:'css-only'});
|
|
190
227
|
await writeState({state:'ready',reason,buildId:styles.buildId,clientManifest:styles.devClient,sequence,session,changed,cssOnly:true,listenerPid:gateway.child.pid,compiler:current.compiler,transforms:current.transforms});
|
|
191
228
|
tools?.recordBuild({buildId:styles.buildId,reason:reason.summary,reasonCode:reason.code,sequence,time:Date.now(),startedAt,duration:performance.now()-started,phases:{styles:performance.now()-started},mode:'css',state:'ready'});
|
|
192
229
|
console.log('Styles updated. '+reason.summary);
|
|
193
|
-
while(styleGenerations.length>3){const old=styleGenerations.shift();await gateway.request('retire',{generation:old.id});await gateway.request('forget',{generation:old.id});await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});}
|
|
194
230
|
await watcher?.refresh();return styles;
|
|
195
231
|
}
|
|
196
232
|
}
|
|
197
|
-
const result=await compilerRequest('build',{options:{...(discovery.eager?{}:{devRoutes:patterns}),devSafe:options.safe,devSequence:++sequence,devSession:session,devTools:tools?.bootstrap,devAffectedModules:affectedModules,devForceRefresh:options.safe||changedFiles.some(file=>/\.(?:css|scss|sass)$/.test(file)||/(?:postcss|browserslist|\.env|config)/.test(file))}});
|
|
233
|
+
const result=await compilerRequest('build',{options:{...(discovery.eager?{}:{devRoutes:patterns}),devSafe:options.safe,devSequence:++sequence,devSession:session,devActionSecret,devTools:tools?.bootstrap,devAffectedModules:affectedModules,devLazyActivated:[...requestedImports],devForceRefresh:options.safe||changedFiles.some(file=>/\.(?:css|scss|sass)$/.test(file)||/(?:postcss|browserslist|\.env|config)/.test(file))}});
|
|
198
234
|
phases.compile=performance.now()-mark;mark=performance.now();
|
|
199
235
|
if(stopping)throw Error('Development is stopping');
|
|
200
236
|
const consumed=filter.acceptInputs(result.inputs||[]);
|
|
@@ -211,18 +247,30 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
211
247
|
catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});affectedRoutes=undefined;affectedModules=undefined;tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
|
|
212
248
|
const previous=current;current=next;current.compiler=result.compiler;current.transforms=result.transforms;generations.push(next);failed.clear();failedPaths.clear();
|
|
213
249
|
current.manifest=manifest;current.changed=changed;current.patterns=new Set(discovery.eager?discovery.project.routes.map(route=>route.pattern):patterns);
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if(previous){
|
|
220
|
-
const retiringTask=(async()=>{await gateway.request('retire',{generation:previous.id}).catch(()=>{});await previous.runtime?.close();await stop(previous.child);})().finally(()=>retiring.delete(retiringTask));retiring.add(retiringTask);
|
|
250
|
+
for(const id of requestedImports)if(!manifest.devLazy?.modules.some(item=>item.id===id))requestedImports.delete(id);
|
|
251
|
+
let affected;
|
|
252
|
+
if(dependencyIndexValid&&affectedRoutes) {
|
|
253
|
+
const nextAffected=await affectedDependencies(gateway,changedFiles).catch(()=>undefined);
|
|
254
|
+
affected=selectiveDevelopmentRoutes(previous?.manifest,manifest,{changed:changedFiles,affectedRoutes,affectedModules,nextAffected});
|
|
221
255
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
256
|
+
current.affectedRoutes=affected;
|
|
257
|
+
try{
|
|
258
|
+
await writeState({state:'ready',reason,buildId:manifest.buildId,clientManifest:manifest.devClient,sequence:manifest.devSequence,session,changed,affectedRoutes:affected,listenerPid:gateway.child.pid,compiler:result.compiler,transforms:result.transforms,routes:manifest.routes.filter(route=>!route.internal&&!route.devDeferred).map(route=>route.pattern)});
|
|
259
|
+
tools?.recordBuild({buildId:manifest.buildId,reason:reason.summary,reasonCode:reason.code,sequence:manifest.devSequence,time:Date.now(),startedAt,duration:performance.now()-started,phases,routes:patterns.length,state:'ready'});
|
|
260
|
+
console.log('Source compiled. '+reason.summary);
|
|
261
|
+
}finally{
|
|
262
|
+
// Activation already succeeded. A locked readiness receipt must not
|
|
263
|
+
// keep obsolete runtimes alive or bypass the generation bound. Retire
|
|
264
|
+
// through the gateway so existing requests drain before their owner
|
|
265
|
+
// closes; the newly activated current runtime remains available.
|
|
266
|
+
if(previous){
|
|
267
|
+
const retiringTask=(async()=>{await gateway.request('retire',{generation:previous.id}).catch(()=>{});await previous.runtime?.close();await stop(previous.child);})().finally(()=>retiring.delete(retiringTask));retiring.add(retiringTask);
|
|
268
|
+
}
|
|
269
|
+
while(generations.length>3){const old=generations.shift();
|
|
270
|
+
// A deferred request may itself be awaiting this build. Never wait for
|
|
271
|
+
// its generation lease while holding the serialized compiler queue.
|
|
272
|
+
const cleanup=retiredCleanup.enqueue(old).finally(()=>retiring.delete(cleanup));retiring.add(cleanup);
|
|
273
|
+
}
|
|
226
274
|
}
|
|
227
275
|
await watcher?.refresh();
|
|
228
276
|
return manifest;
|
|
@@ -230,6 +278,18 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
230
278
|
}
|
|
231
279
|
const updateInfo=()=>({buildId:current.manifest.buildId,clientManifest:current.manifest.devClient,pagesManifest:current.manifest.pagesManifest,sequence:current.manifest.devSequence,session,changed:current.changed,cssOnly:current.cssOnly,affectedRoutes:current.affectedRoutes});
|
|
232
280
|
async function ensure(raw,client,exact=false){
|
|
281
|
+
if(new URL(raw,'http://local').pathname.startsWith('/_prnext/lazy/'))return serialized(async()=>{
|
|
282
|
+
const id=lazyImportIdentifier(raw);
|
|
283
|
+
if(!id||!current)throw Error('Unknown deferred import identifier');
|
|
284
|
+
let built,failure,lazy;
|
|
285
|
+
try{lazy=await materializeDevImport({root,current,id,compile:async request=>{
|
|
286
|
+
const result=await compilerRequest('build',{options:{devRoutes:[...current.patterns],devSafe:options.safe,devSequence:sequence,devSession:session,devActionSecret,devTools:tools?.bootstrap,devLazyRequest:request,devLazyBase:{cacheId:current.manifest.cacheId}}});
|
|
287
|
+
built=result.manifest;return built;
|
|
288
|
+
}});}catch(error){failure=error;throw error;}
|
|
289
|
+
finally{try{await discardLazyBuild(root,built);}catch(error){if(failure){failure.cleanupError=error;failure.message+='; deferred compiler staging cleanup also failed';}else throw error;}}
|
|
290
|
+
requestedImports.add(id);
|
|
291
|
+
return {lazy};
|
|
292
|
+
});
|
|
233
293
|
const resolve=()=>exact?discovery.project.routes.find(route=>route.pattern===raw):findRoute(raw);
|
|
234
294
|
const warm=discovery&&resolve();
|
|
235
295
|
if(!stopping&¤t&&warm&&active.has(warm.pattern)&¤t.patterns.has(warm.pattern)){
|
|
@@ -288,7 +348,7 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
288
348
|
// Keep each native data-cache bridge alive until its externally owned
|
|
289
349
|
// Node process has drained after() work and cache fills on stdin EOF.
|
|
290
350
|
await runtimePool.close();
|
|
291
|
-
await Promise.all([tools?.close(),...generations.map(g=>stop(g.child)),...retiring]);
|
|
351
|
+
await Promise.all([tools?.close(),...generations.map(g=>stop(g.child)),...retiring,retiredCleanup.close()]);
|
|
292
352
|
await queue.catch(()=>{});await compilerBroker.close();await stateQueue;await rm(home,{recursive:true,force:true,maxRetries:10,retryDelay:100});await rm(stateFile,{force:true});
|
|
293
353
|
})();return closing;
|
|
294
354
|
};
|
|
@@ -303,7 +363,7 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
303
363
|
return {...updateInfo(),...result};
|
|
304
364
|
})});
|
|
305
365
|
await writeState({state:'building',session});
|
|
306
|
-
gateway=new DevGateway(binary,root,{...options,onEnsure:ensure,onFatal:fatal,
|
|
366
|
+
gateway=new DevGateway(binary,root,{...options,onEnsure:ensure,onFatal:fatal,onBeforeChild:()=>sessionOwner.beforeSpawn(),onChild:(child,ticket)=>sessionOwner.spawned(ticket,child),
|
|
307
367
|
onOpen:message=>{const route=discovery&&findRoute(message.path);if(route)pins.set(message.client,{pattern:route.pattern,connection:message.connection});},
|
|
308
368
|
onClose:message=>{if(pins.get(message.client)?.connection===message.connection)pins.delete(message.client);}});
|
|
309
369
|
const listener=await gateway.ready;
|
package/runtime/env.mjs
CHANGED
|
@@ -57,6 +57,15 @@ export function loadEnvConfig(projectRoot, { dev = false, env = process.env, mod
|
|
|
57
57
|
return { combinedEnv: env, parsedEnv, loadedEnvFiles };
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Native runtime settings need the same project env as its Node workers.
|
|
61
|
+
* Return a fresh object: persistent apps must never share loaded credentials.
|
|
62
|
+
*/
|
|
63
|
+
export function productionEnvironment(projectRoot, {env=process.env}={}) {
|
|
64
|
+
const combined={...env,NODE_ENV:env.NODE_ENV||'production'};
|
|
65
|
+
loadEnvConfig(projectRoot,{dev:false,env:combined});
|
|
66
|
+
return combined;
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
export function shouldWatchProjectFile(filename) {
|
|
61
70
|
if (!filename) return false;
|
|
62
71
|
const parts = String(filename).split(/[\\/]/);
|
package/runtime/flight-delta.mjs
CHANGED
|
@@ -1,21 +1,72 @@
|
|
|
1
1
|
import { randomBytes, createHmac } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { deflateRaw } from 'node:zlib';
|
|
3
3
|
import { TYPE, MAX_VALUE, collectStream, deltaStream } from '../compat/flight-delta.cjs';
|
|
4
4
|
|
|
5
|
-
export function createFlightStore({maxBytes=32*1024*1024,maxEntries=64,ttl=30000,maxCaptures=8,maxEncoders=2,now=Date.now}={}) {
|
|
6
|
-
|
|
5
|
+
export function createFlightStore({maxBytes=32*1024*1024,maxEntries=64,ttl=30000,maxCaptures=8,maxEncoders=2,now=Date.now,setTimer=setTimeout,clearTimer=clearTimeout}={}) {
|
|
6
|
+
if(!Number.isFinite(ttl)||ttl<0||typeof now!=='function'||typeof setTimer!=='function'||typeof clearTimer!=='function')throw new TypeError('Invalid Flight store expiration policy');
|
|
7
|
+
const entries=new Map();let size=0,captures=0,encoders=0,timer,timerExpires;
|
|
7
8
|
function reserve(kind){if(kind==='capture'?captures>=maxCaptures:encoders>=maxEncoders)return;kind==='capture'?captures++:encoders++;let released=false;return()=>{if(released)return;released=true;kind==='capture'?captures--:encoders--;};}
|
|
8
9
|
function remove(token){const entry=entries.get(token);if(entry){size-=entry.bytes.length;entries.delete(token);}}
|
|
9
|
-
function
|
|
10
|
+
function schedule(at){
|
|
11
|
+
let expires=Infinity;
|
|
12
|
+
for(const entry of entries.values())expires=Math.min(expires,entry.expires);
|
|
13
|
+
if(timer!==undefined&&timerExpires===expires)return;
|
|
14
|
+
if(timer!==undefined)clearTimer(timer);
|
|
15
|
+
timer=timerExpires=undefined;
|
|
16
|
+
if(!entries.size)return;
|
|
17
|
+
timerExpires=expires;
|
|
18
|
+
// One timer for the bounded store, not one per response. A cancelled cache
|
|
19
|
+
// entry must not leave its former deadline armed. Clamp long TTLs to Node's
|
|
20
|
+
// timer range; an early wake simply checks the authoritative clock again.
|
|
21
|
+
const handle=setTimer(()=>{
|
|
22
|
+
if(timer!==handle)return;
|
|
23
|
+
timer=timerExpires=undefined;prune(true);
|
|
24
|
+
},Math.min(2147483647,Math.max(1,expires-at)));
|
|
25
|
+
timer=handle;timer?.unref?.();
|
|
26
|
+
}
|
|
27
|
+
function prune(changed=false){
|
|
28
|
+
const at=now();
|
|
29
|
+
for(const[token,entry]of entries)if(entry.expires<=at){remove(token);changed=true;}
|
|
30
|
+
while(size>maxBytes||entries.size>maxEntries){remove(entries.keys().next().value);changed=true;}
|
|
31
|
+
// Ordinary hits keep the same timer without another full entry scan.
|
|
32
|
+
if(changed)schedule(at);
|
|
33
|
+
}
|
|
10
34
|
return {
|
|
11
35
|
get(token,identity){prune();const entry=entries.get(token);if(entry?.identity!==identity)return;return entry?.bytes;},
|
|
12
|
-
set(token,identity,bytes){if(bytes.length>MAX_VALUE)return;remove(token);entries.set(token,{identity,bytes,expires:now()+ttl});size+=bytes.length;prune();},
|
|
36
|
+
set(token,identity,bytes){if(bytes.length>MAX_VALUE)return;remove(token);entries.set(token,{identity,bytes,expires:now()+ttl});size+=bytes.length;prune(true);},
|
|
13
37
|
capture:()=>reserve('capture'),encoder:()=>reserve('encoder'),get inFlight(){return {captures,encoders};},
|
|
14
38
|
get bytes(){return size;},get size(){return entries.size;},
|
|
15
39
|
};
|
|
16
40
|
}
|
|
17
41
|
const store=createFlightStore();
|
|
18
|
-
|
|
42
|
+
const compress=value=>new Promise((resolve,reject)=>deflateRaw(value,{level:1},(error,result)=>error?reject(error):resolve(result)));
|
|
43
|
+
|
|
44
|
+
// At most two probes (four small zlib jobs) across all stores and requests.
|
|
45
|
+
// Admission never queues: a saturated probe keeps ordinary Flight. An aborted
|
|
46
|
+
// caller stops waiting, but its slot remains occupied until native work ends.
|
|
47
|
+
export function createCompressionProbe({maxConcurrent=2,compress:run=compress}={}) {
|
|
48
|
+
if(!Number.isInteger(maxConcurrent)||maxConcurrent<1||maxConcurrent>8||typeof run!=='function')throw new TypeError('Invalid Flight compression probe policy');
|
|
49
|
+
let active=0;
|
|
50
|
+
const probe=(raw,encoded,signal)=>{
|
|
51
|
+
signal?.throwIfAborted();
|
|
52
|
+
if(raw.length>64*1024||encoded.length+32>=raw.length||active>=maxConcurrent)return false;
|
|
53
|
+
active++;
|
|
54
|
+
const work=Promise.allSettled([Promise.resolve().then(()=>run(encoded)),Promise.resolve().then(()=>run(raw))])
|
|
55
|
+
.then(results=>results.every(result=>result.status==='fulfilled')&&results[0].value.length+32<results[1].value.length*0.9)
|
|
56
|
+
.finally(()=>{active--;});
|
|
57
|
+
if(!signal)return work;
|
|
58
|
+
return new Promise((resolve,reject)=>{
|
|
59
|
+
const abort=()=>{signal.removeEventListener('abort',abort);reject(signal.reason);};
|
|
60
|
+
signal.addEventListener('abort',abort,{once:true});
|
|
61
|
+
if(signal.aborted)abort();
|
|
62
|
+
work.then(resolve,reject).finally(()=>signal.removeEventListener('abort',abort));
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
Object.defineProperty(probe,'inFlight',{get:()=>active});
|
|
66
|
+
return probe;
|
|
67
|
+
}
|
|
68
|
+
const compressionProbe=createCompressionProbe();
|
|
69
|
+
export function compressedDeltaPolicy(acceptEncoding='',{signal}={}) {
|
|
19
70
|
const compressed=acceptEncoding.split(',').some(value=>{
|
|
20
71
|
const [name,...parameters]=value.trim().toLowerCase().split(';');
|
|
21
72
|
return /^(gzip|deflate|br|zstd|\*)$/.test(name)&&!parameters.some(p=>/^\s*q\s*=\s*0(?:\.0*)?\s*$/.test(p));
|
|
@@ -24,8 +75,7 @@ export function compressedDeltaPolicy(acceptEncoding='') {
|
|
|
24
75
|
// This conservative, bounded probe is not the response compressor. It avoids
|
|
25
76
|
// choosing thousands of references to JSON that gzip already compresses well.
|
|
26
77
|
// A large/expensive chunk keeps ordinary Flight instead of delaying a flush.
|
|
27
|
-
return (raw,encoded)=>raw
|
|
28
|
-
deflateRawSync(encoded,{level:1}).length+32<deflateRawSync(raw,{level:1}).length*0.9;
|
|
78
|
+
return (raw,encoded)=>compressionProbe(raw,encoded,signal);
|
|
29
79
|
}
|
|
30
80
|
export async function differentialFlight(response,options,cache=store) {
|
|
31
81
|
const headers=new Headers(options.headers), outgoing=new Headers(response.headers);
|
|
@@ -40,7 +90,7 @@ export async function differentialFlight(response,options,cache=store) {
|
|
|
40
90
|
controller.enqueue(raw instanceof Uint8Array?raw:raw instanceof ArrayBuffer?new Uint8Array(raw):new TextEncoder().encode(String(raw??'')));controller.close();
|
|
41
91
|
}});
|
|
42
92
|
const collected=collectStream(body,{capture:!!releaseCapture,onComplete:bytes=>cache.set(token,identity,bytes),onFinally:releaseCapture});
|
|
43
|
-
encoded=base?await deltaStream(collected,base,{assess:compressedDeltaPolicy(headers.get('accept-encoding')||'')}):{delta:false,body:collected};
|
|
93
|
+
encoded=base?await deltaStream(collected,base,{assess:compressedDeltaPolicy(headers.get('accept-encoding')||'',{signal:options.signal})}):{delta:false,body:collected};
|
|
44
94
|
if(!encoded.delta)releaseEncoder?.();
|
|
45
95
|
else encoded.body=collectStream(encoded.body,{capture:false,onFinally:releaseEncoder});
|
|
46
96
|
}catch(error){releaseEncoder?.();releaseCapture?.();throw error;}
|
package/runtime/fs.mjs
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { rename as move } from 'node:fs/promises';
|
|
2
2
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
|
|
4
5
|
// Windows scanners and recently closed file handles can briefly deny rename.
|
|
5
6
|
// Keep the atomic swap: never delete the destination to work around a lock.
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
export function createAtomicRenamer({moveFile=move,wait=delay,platform=process.platform,now=()=>performance.now(),retryMs=2000}={}) {
|
|
8
|
+
return async function rename(source,destination) {
|
|
9
|
+
const started=now(),deadline=started+retryMs;
|
|
10
|
+
for(let attempt=1;;attempt++){
|
|
11
|
+
try{return await moveFile(source,destination);}
|
|
12
|
+
catch(error){
|
|
13
|
+
if(platform!=='win32'||!['EPERM','EACCES','EBUSY'].includes(error.code)||now()>=deadline){
|
|
14
|
+
error.atomicRename={operation:'rename',code:error.code||'UNKNOWN',attempts:attempt,elapsedMs:Math.max(0,now()-started),destination:path.basename(destination)};
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
await wait(Math.min(10*attempt,100,Math.max(0,deadline-now())));
|
|
18
|
+
}
|
|
13
19
|
}
|
|
14
|
-
}
|
|
20
|
+
};
|
|
15
21
|
}
|
|
22
|
+
export const rename=createAtomicRenamer();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Only emitted by the opt-in development compiler. Successful imports retain
|
|
2
|
+
// native ESM caching; failed compilation is retryable after a source correction.
|
|
3
|
+
const pending=new Map(),styles=new Map();
|
|
4
|
+
function stylesheet(url){
|
|
5
|
+
const existing=[...document.querySelectorAll('link[rel="stylesheet"]')].find(link=>link.href===new URL(url,location.href).href);
|
|
6
|
+
if(existing?.sheet)return Promise.resolve();
|
|
7
|
+
if(existing&&styles.has(url))return styles.get(url);
|
|
8
|
+
const promise=new Promise((resolve,reject)=>{
|
|
9
|
+
const link=existing||document.createElement('link');
|
|
10
|
+
const timer=setTimeout(()=>finish(Error('Deferred stylesheet timed out')),30000);
|
|
11
|
+
const finish=error=>{clearTimeout(timer);link.removeEventListener('load',loaded);link.removeEventListener('error',failed);if(error){styles.delete(url);if(!existing)link.remove();reject(error);}else resolve();};
|
|
12
|
+
const loaded=()=>finish(),failed=()=>finish(Error('Deferred stylesheet failed to load'));
|
|
13
|
+
link.addEventListener('load',loaded,{once:true});link.addEventListener('error',failed,{once:true});
|
|
14
|
+
if(!existing){link.rel='stylesheet';link.href=url;document.head.appendChild(link);}
|
|
15
|
+
});
|
|
16
|
+
styles.set(url,promise);
|
|
17
|
+
void promise.then(()=>styles.delete(url),()=>styles.delete(url));
|
|
18
|
+
return promise;
|
|
19
|
+
}
|
|
20
|
+
export function lazyImport(id,basePath=''){
|
|
21
|
+
const key=basePath+':'+id;
|
|
22
|
+
if(pending.has(key))return pending.get(key);
|
|
23
|
+
const task=(async()=>{
|
|
24
|
+
const path='/_prnext/lazy/'+id;
|
|
25
|
+
const response=await fetch(`${basePath}/_prnext/ensure?path=${encodeURIComponent(path)}`,{cache:'no-store',credentials:'same-origin',signal:AbortSignal.timeout(120000)});
|
|
26
|
+
if(!response.ok)throw Error(await response.text());
|
|
27
|
+
const value=await response.json(),target=value.lazy;
|
|
28
|
+
if(!target||typeof target.url!=='string'||!Array.isArray(target.css))throw Error('Invalid deferred import response');
|
|
29
|
+
for(const url of [target.url,...target.css]){
|
|
30
|
+
const parsed=new URL(url,location.href);
|
|
31
|
+
if(parsed.origin!==location.origin||!parsed.pathname.includes('/_prnext/assets/'))throw Error('Invalid deferred import asset URL');
|
|
32
|
+
}
|
|
33
|
+
await Promise.all(target.css.map(stylesheet));
|
|
34
|
+
const module=await import(target.url);
|
|
35
|
+
return target.native?module:module.__prnextLazyModule;
|
|
36
|
+
})().finally(()=>pending.delete(key));
|
|
37
|
+
pending.set(key,task);return task;
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {readFile, stat} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {escapeHtml} from './http.mjs';
|
|
5
|
+
|
|
6
|
+
export const NATIVE_PPR_ARTIFACT_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
export const NATIVE_PPR_PREFIX_BYTES = 128 * 1024;
|
|
8
|
+
export const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
9
|
+
|
|
10
|
+
export function partialShell(artifact, route) {
|
|
11
|
+
const bootstrap = '<script>self.__PRNEXT_FLIGHT_STREAM__=self.__PRNEXT_FLIGHT_STREAM__||[];</script>' +
|
|
12
|
+
(route.client ? `<script type="module" async src="${escapeHtml(route.client)}"></script>` : '');
|
|
13
|
+
const end = artifact.shell.lastIndexOf('</body>');
|
|
14
|
+
if (end < 0 || !/<\/body><\/html>$/i.test(artifact.shell)) throw new Error('Invalid partial prerender document');
|
|
15
|
+
return artifact.shell.slice(0, end) + bootstrap + artifact.shell.slice(end);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// This is a private transport field, never an HTTP header. Rust has already
|
|
19
|
+
// checked cache generation and committed these exact build bytes. Pinning the
|
|
20
|
+
// same artifact gives the request one snapshot even if invalidation races Node.
|
|
21
|
+
export async function nativePprArtifact({renderMode, manifest, route, url, distDir}) {
|
|
22
|
+
if (!renderMode?.startsWith('native-ppr:')) return null;
|
|
23
|
+
if (!manifest.config?.nativePprShell || manifest.dev || !/^[a-f\d]{64}$/.test(renderMode.slice(11))) throw new Error('Invalid native PPR continuation');
|
|
24
|
+
const pathname = new URL(url).pathname;
|
|
25
|
+
const descriptor = manifest.config.nativePprArtifacts?.find(item => item.routeId === route.id && item.path === pathname && item.artifactHash === renderMode.slice(11));
|
|
26
|
+
if (!descriptor || new URL(url).search || descriptor.artifact !== route.ppr?.[pathname]) throw new Error('Native PPR artifact does not match the route');
|
|
27
|
+
const filename = path.resolve(distDir, descriptor.artifact);
|
|
28
|
+
if (!descriptor.artifact.startsWith('static/') || !filename.startsWith(path.resolve(distDir) + path.sep) || (await stat(filename)).size > NATIVE_PPR_ARTIFACT_BYTES) throw new Error('Invalid native PPR artifact path or size');
|
|
29
|
+
const bytes = await readFile(filename);
|
|
30
|
+
if (bytes.length > NATIVE_PPR_ARTIFACT_BYTES || sha256(bytes) !== descriptor.artifactHash) throw new Error('Native PPR artifact changed after publication');
|
|
31
|
+
const artifact = JSON.parse(bytes);
|
|
32
|
+
const prefix = Buffer.from(partialShell(artifact, route).replace(/<\/body><\/html>$/i, ''));
|
|
33
|
+
if (artifact.version !== 1 || !artifact.postponed || artifact.complete || artifact.generic || sha256(prefix) !== descriptor.prefixHash || sha256(artifact.flight) !== descriptor.identity) throw new Error('Invalid native PPR snapshot');
|
|
34
|
+
return {artifact, descriptor};
|
|
35
|
+
}
|
package/runtime/og/NOTICE.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Image renderer: @vercel/og 1.0.3 (https://github.com/vercel/og), with bundled Satori 0.33.5 (https://github.com/vercel/satori). PRNext replaces embedded fflate 0.7.3 with fflate 0.7.5, bundles it, adds Node CommonJS globals and includes adjacent WASM/font assets. Optional sharp backends require librsvg 2.63.2 or newer; otherwise rendering uses resvg. Upstream bundled licenses are retained. Rebuild: node scripts/prepare-og.mjs.
|
|
1
|
+
Image renderer: @vercel/og 1.0.3 (https://github.com/vercel/og), with bundled Satori 0.33.5 (https://github.com/vercel/satori). PRNext replaces embedded fflate 0.7.3 with fflate 0.7.5, bundles and minifies it with function/class names preserved, adds Node CommonJS globals and includes adjacent WASM/font assets. Optional sharp backends require librsvg 2.63.2 or newer; otherwise rendering uses resvg. Upstream bundled licenses are retained. Rebuild: node scripts/prepare-og.mjs.
|
|
2
2
|
Geist Regular 1.800: Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git), SIL OFL 1.1; LICENSE.Geist is the upstream OFL.txt.
|
|
3
3
|
HarfBuzzJS 0.10.0: https://github.com/harfbuzz/harfbuzzjs; see LICENSE.harfbuzzjs.
|
|
4
4
|
resvg-js 2.4.1: https://github.com/yisibl/resvg-js/tree/v2.4.1, MPL-2.0 (LICENSE).
|