@thomas.f/prnext 0.1.17-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/README.md +26 -869
- package/build/actions.mjs +1 -1
- package/build/analyze.mjs +180 -24
- package/build/app-config.mjs +13 -0
- package/build/app-static.mjs +34 -12
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +81 -26
- package/build/bundle-runtime.mjs +22 -9
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +31 -8
- package/build/config.mjs +30 -1
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +56 -12
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-css-update.mjs +4 -2
- package/build/dev-dependencies.mjs +1 -1
- package/build/dev-discovery.mjs +3 -3
- package/build/dev.mjs +15 -8
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +3 -3
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +15 -5
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/index.mjs +59 -26
- package/build/instrumentation.mjs +30 -0
- package/build/metadata-routes.mjs +1 -1
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/persistent-cache.mjs +18 -3
- package/build/precompress.mjs +1 -1
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +19 -9
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +5 -5
- package/build/production-builder.mjs +58 -9
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/static-export.mjs +4 -4
- package/build/styles.mjs +6 -4
- package/build/transform.mjs +1 -1
- package/build/typed-routes.mjs +4 -0
- package/build/typescript-dev.mjs +35 -8
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript.mjs +6 -4
- package/build/webpack-graph.mjs +6 -3
- package/build/worker-memory.mjs +9 -0
- package/cli.mjs +60 -4
- 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 +68 -3
- package/compat/link.cjs +12 -9
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +171 -2
- package/docs/ai/data.md +165 -5
- package/docs/ai/deployment.md +158 -0
- package/docs/ai/quickstart.md +29 -4
- package/docs/ai/routing.md +82 -0
- package/docs/ai/troubleshooting.md +37 -2
- package/package.json +26 -7
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/app-client.mjs +212 -65
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +88 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-client.mjs +7 -2
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-indicator.mjs +8 -3
- package/runtime/dev-lazy-tool.mjs +16 -0
- package/runtime/development.mjs +29 -12
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- 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 +14 -7
- package/runtime/pages-navigation.mjs +48 -63
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +34 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/watch-project.mjs +19 -15
- package/runtime/worker.mjs +70 -26
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {createHash,randomBytes}=require('node:crypto');
|
|
3
|
+
const MAX_VALUE=2*1024*1024,MAX_META=65536;
|
|
4
|
+
const hash=value=>createHash('sha256').update(value).digest('hex');
|
|
5
|
+
const ACQUIRE=`
|
|
6
|
+
if not redis.call('HGET',KEYS[2],'_incarnation') then redis.call('HSET',KEYS[2],'_incarnation',ARGV[3],'_epoch',ARGV[3]) end
|
|
7
|
+
redis.call('PEXPIRE',KEYS[2],ARGV[4])
|
|
8
|
+
local epoch=redis.call('HGET',KEYS[2],'_epoch');local incarnation=redis.call('HGET',KEYS[2],'_incarnation')
|
|
9
|
+
if redis.call('SET',KEYS[1],ARGV[1],'NX','PX',ARGV[2]) then return {1,epoch,incarnation} end
|
|
10
|
+
return {0}`;
|
|
11
|
+
const RELEASE=`if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) end return 0`;
|
|
12
|
+
const READ=`
|
|
13
|
+
local fields=redis.call('HMGET',KEYS[1],'meta','value')
|
|
14
|
+
if not fields[1] or not fields[2] or string.len(fields[1])>65536 or string.len(fields[2])>tonumber(ARGV[2]) then return nil end
|
|
15
|
+
local meta=cjson.decode(fields[1]); local extra=cjson.decode(ARGV[1]); local stale=0
|
|
16
|
+
if redis.call('HGET',KEYS[2],'_incarnation')~=meta.incarnation then return nil end
|
|
17
|
+
local t=redis.call('TIME'); local now=tonumber(t[1])*1000+math.floor(tonumber(t[2])/1000)
|
|
18
|
+
local seen={}
|
|
19
|
+
for _,id in ipairs(meta.tagIds) do seen[id]=true end
|
|
20
|
+
for _,id in ipairs(extra) do seen[id]=true end
|
|
21
|
+
for id,_ in pairs(seen) do
|
|
22
|
+
local value=redis.call('HGET',KEYS[2],id)
|
|
23
|
+
if value then
|
|
24
|
+
local tag=cjson.decode(value)
|
|
25
|
+
if tag.generation~=(meta.versions[id] or '0') then
|
|
26
|
+
if tag.deadline>=0 and tag.deadline<=now then return nil end
|
|
27
|
+
stale=1
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
return {fields[1],fields[2],math.max(0,now-meta.storedAt),stale}`;
|
|
32
|
+
const WRITE=`
|
|
33
|
+
if redis.call('GET',KEYS[3])~=ARGV[1] or redis.call('HGET',KEYS[2],'_epoch')~=ARGV[2] then return 0 end
|
|
34
|
+
local meta=cjson.decode(ARGV[3]); meta.versions={};meta.incarnation=ARGV[5];meta.writer=ARGV[1]
|
|
35
|
+
local t=redis.call('TIME'); local now=tonumber(t[1])*1000+math.floor(tonumber(t[2])/1000)
|
|
36
|
+
meta.storedAt=now-tonumber(ARGV[6])
|
|
37
|
+
for _,id in ipairs(meta.tagIds) do
|
|
38
|
+
local value=redis.call('HGET',KEYS[2],id)
|
|
39
|
+
meta.versions[id]=value and cjson.decode(value).generation or '0'
|
|
40
|
+
end
|
|
41
|
+
local encoded=cjson.encode(meta)
|
|
42
|
+
if string.len(encoded)>65536 then return 0 end
|
|
43
|
+
redis.call('HSET',KEYS[1],'meta',encoded,'value',ARGV[4])
|
|
44
|
+
redis.call('PEXPIRE',KEYS[1],ARGV[7]);redis.call('PEXPIRE',KEYS[2],ARGV[8]);return 1`;
|
|
45
|
+
const INVALIDATE=`
|
|
46
|
+
local generation=ARGV[3];local ids=cjson.decode(ARGV[4]);local added=0
|
|
47
|
+
for _,id in ipairs(ids) do if redis.call('HEXISTS',KEYS[1],id)==0 then added=added+1 end end
|
|
48
|
+
if redis.call('HLEN',KEYS[1])+added>4098 then redis.call('DEL',KEYS[1]) end
|
|
49
|
+
if not redis.call('HGET',KEYS[1],'_incarnation') then redis.call('HSET',KEYS[1],'_incarnation',generation) end
|
|
50
|
+
redis.call('HSET',KEYS[1],'_epoch',generation)
|
|
51
|
+
local t=redis.call('TIME');local now=tonumber(t[1])*1000+math.floor(tonumber(t[2])/1000)
|
|
52
|
+
local expiry=tonumber(ARGV[1]);local deadline=expiry<0 and -1 or now+expiry
|
|
53
|
+
for _,id in ipairs(ids) do redis.call('HSET',KEYS[1],id,cjson.encode({generation=generation,deadline=deadline})) end
|
|
54
|
+
redis.call('PEXPIRE',KEYS[1],ARGV[2])
|
|
55
|
+
return 1`;
|
|
56
|
+
const REMOVE=`local meta=redis.call('HGET',KEYS[1],'meta');if meta and cjson.decode(meta).writer==ARGV[1] then return redis.call('DEL',KEYS[1]) end return 0`;
|
|
57
|
+
|
|
58
|
+
function tags(values=[]){
|
|
59
|
+
if(!Array.isArray(values)||values.length>256||values.some(value=>typeof value!=='string'||!value||Buffer.byteLength(value)>8192))throw new TypeError('Redis cache tags must be bounded nonempty strings.');
|
|
60
|
+
return [...new Set(values)];
|
|
61
|
+
}
|
|
62
|
+
function positive(value,fallback,min,max,name){value??=fallback;if(!Number.isSafeInteger(value)||value<min||value>max)throw new TypeError(`Redis cache ${name} must be between ${min} and ${max}.`);return value;}
|
|
63
|
+
function pause(ms,signal){return new Promise((resolve,reject)=>{const done=error=>{clearTimeout(timer);signal?.removeEventListener('abort',abort);error?reject(error):resolve();};const abort=()=>done(signal.reason);const timer=setTimeout(done,ms);signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted)abort();});}
|
|
64
|
+
function boundedRead(reader,ms){return new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error('Redis cache value read timed out.')),ms);reader.read().then(value=>{clearTimeout(timer);resolve(value);},error=>{clearTimeout(timer);reject(error);});});}
|
|
65
|
+
|
|
66
|
+
/** First-party Cache Components handler; Redis is loaded only when configured. */
|
|
67
|
+
function createRedisCache(options={},maxValueBytes=MAX_VALUE){
|
|
68
|
+
if(!options||typeof options!=='object'||Array.isArray(options))throw new TypeError('Redis cache options must be an object.');
|
|
69
|
+
const known=new Set(['namespace','urlEnv','client','leaseMs','waitMs','commandTimeoutMs','maxAgeSeconds']);
|
|
70
|
+
for(const key of Object.keys(options))if(!known.has(key))throw new TypeError(`Unknown Redis cache option: ${key}`);
|
|
71
|
+
if(options.client!==undefined&&typeof options.client?.withTypeMapping!=='function')throw new TypeError('Redis cache client must be a node-redis client.');
|
|
72
|
+
if(typeof options.namespace!=='string'||!options.namespace||Buffer.byteLength(options.namespace)>256)throw new TypeError('Redis cache requires an application-specific namespace.');
|
|
73
|
+
const urlEnv=options.urlEnv??'REDIS_URL';if(typeof urlEnv!=='string'||!/^[_A-Za-z][_A-Za-z0-9]*$/.test(urlEnv))throw new TypeError('Redis cache urlEnv must name an environment variable.');
|
|
74
|
+
const leaseMs=positive(options.leaseMs,30000,1000,120000,'leaseMs'),waitMs=positive(options.waitMs,2500,100,3500,'waitMs');
|
|
75
|
+
const commandTimeoutMs=positive(options.commandTimeoutMs,1000,100,2000,'commandTimeoutMs'),maxAgeSeconds=positive(options.maxAgeSeconds,604800,1,2592000,'maxAgeSeconds');
|
|
76
|
+
if(waitMs+2*commandTimeoutMs>4500)throw new TypeError('Redis cache waitMs plus twice commandTimeoutMs must not exceed 4500 ms.');
|
|
77
|
+
const prefix='prnext:v1:{'+hash(options.namespace)+'}:',epochKey=prefix+'tags',retention=String(maxAgeSeconds*1000+leaseMs+60000);
|
|
78
|
+
let owned,connecting,closed=false;
|
|
79
|
+
async function client(){
|
|
80
|
+
if(closed)throw new Error('Redis cache is closed.');
|
|
81
|
+
if(options.client){
|
|
82
|
+
if(options.client.isReady===false)throw new Error('Redis cache client is not connected.');
|
|
83
|
+
return options.client;
|
|
84
|
+
}
|
|
85
|
+
if(owned?.isReady)return owned;
|
|
86
|
+
if(!connecting){
|
|
87
|
+
connecting=(async()=>{
|
|
88
|
+
const url=process.env[urlEnv];
|
|
89
|
+
if(!url||!/^rediss?:\/\//.test(url))throw new Error(`Set ${urlEnv} to a Redis connection URL.`);
|
|
90
|
+
// Use node-redis's base client; Redis Stack command packages are not
|
|
91
|
+
// needed by this provider's bounded EVAL/HASH protocol.
|
|
92
|
+
const {createClient}=require('@redis/client');
|
|
93
|
+
owned?.destroy();owned=createClient({url,disableOfflineQueue:true,commandsQueueMaxLength:128,socket:{connectTimeout:commandTimeoutMs,reconnectStrategy:false}});
|
|
94
|
+
owned.on('error',()=>{});await owned.connect();return owned;
|
|
95
|
+
})().finally(()=>{connecting=undefined;});
|
|
96
|
+
}
|
|
97
|
+
return connecting;
|
|
98
|
+
}
|
|
99
|
+
async function command(args,signal){
|
|
100
|
+
const connection=await client();
|
|
101
|
+
// '$' (36) is RESP's binary-safe bulk-string type, as exposed by node-redis.
|
|
102
|
+
const binary=connection.withTypeMapping({36:Buffer});
|
|
103
|
+
const deadline=AbortSignal.timeout(commandTimeoutMs);
|
|
104
|
+
return binary.sendCommand(args,{abortSignal:signal?AbortSignal.any([signal,deadline]):deadline});
|
|
105
|
+
}
|
|
106
|
+
const keys=key=>{if(typeof key!=='string'||!key||Buffer.byteLength(key)>8192)throw new TypeError('Invalid Redis cache key.');const id=hash(key);return {entry:prefix+'entry:'+id,lock:prefix+'lease:'+id};};
|
|
107
|
+
async function acquire(key,{signal}={}){
|
|
108
|
+
const {lock}=keys(key),token=randomBytes(24).toString('hex'),until=performance.now()+waitMs;
|
|
109
|
+
for(;;){
|
|
110
|
+
signal?.throwIfAborted();
|
|
111
|
+
const started=performance.now();
|
|
112
|
+
const result=await command(['EVAL',ACQUIRE,'2',lock,epochKey,token,String(leaseMs),randomBytes(24).toString('hex'),retention],signal);
|
|
113
|
+
if(Number(result[0])===1){let released=false;return {token,started,epoch:String(result[1]),incarnation:String(result[2]),async release(){if(released)return;released=true;await command(['EVAL',RELEASE,'1',lock,token]);}};}
|
|
114
|
+
if(performance.now()>=until)throw Object.assign(new Error('Redis cache fill wait exceeded its budget.'),{code:'PRNEXT_REDIS_BUSY'});
|
|
115
|
+
await pause(Math.min(50,Math.max(1,until-performance.now())),signal);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function get(key,softTags=[]){
|
|
119
|
+
const result=await command(['EVAL',READ,'2',keys(key).entry,epochKey,JSON.stringify(tags(softTags).map(hash)),String(maxValueBytes)]);
|
|
120
|
+
if(!result)return undefined;
|
|
121
|
+
const meta=JSON.parse(result[0].toString('utf8')),bytes=result[1];
|
|
122
|
+
if(meta.version!==1||!Buffer.isBuffer(bytes)||bytes.length>maxValueBytes)throw new Error('Invalid Redis cache entry.');
|
|
123
|
+
const age=Number(result[2]);if(!Number.isFinite(age)||age<0)throw new Error('Invalid Redis cache age.');
|
|
124
|
+
let offset=0;
|
|
125
|
+
return {tags:meta.tags,stale:meta.stale,revalidate:Number(result[3])?0:meta.revalidate,expire:meta.expire,timestamp:Math.max(0,Date.now()-age),
|
|
126
|
+
value:new ReadableStream({pull(controller){if(offset===bytes.length){controller.close();return;}const end=Math.min(bytes.length,offset+65536);controller.enqueue(new Uint8Array(bytes.subarray(offset,end)));offset=end;}},{highWaterMark:0})};
|
|
127
|
+
}
|
|
128
|
+
async function set(key,pending,{lease,softTags=[]}={}){
|
|
129
|
+
if(!lease||!Number.isFinite(lease.started)){
|
|
130
|
+
// Observe and release a caller's value even when its lease is missing.
|
|
131
|
+
// An unfenced producer must never publish after an invalidation.
|
|
132
|
+
void Promise.resolve(pending).then(entry=>entry.value.cancel()).catch(()=>{});
|
|
133
|
+
throw new Error('Redis cache writes require the lease returned by acquire().');
|
|
134
|
+
}
|
|
135
|
+
const entry=await pending,entryTags=tags(entry.tags);
|
|
136
|
+
for(const field of ['timestamp','stale','revalidate','expire'])if(!Number.isFinite(entry[field])||entry[field]<0)throw new TypeError('Invalid Redis cache '+field);
|
|
137
|
+
const reader=entry.value.getReader(),chunks=[];let size=0,complete=false;
|
|
138
|
+
const until=performance.now()+commandTimeoutMs;
|
|
139
|
+
let reads=0;
|
|
140
|
+
try{for(;;){if(performance.now()>=until||++reads>10000)throw new Error('Redis cache value read timed out.');const part=await boundedRead(reader,Math.max(1,until-performance.now()));if(part.done){complete=true;break;}if(!(part.value instanceof Uint8Array)||(size+=part.value.byteLength)>maxValueBytes)throw new Error(`Redis cache value exceeds ${maxValueBytes/1024/1024} MiB.`);chunks.push(Buffer.from(part.value));}}
|
|
141
|
+
finally{if(!complete)void reader.cancel().catch(()=>{});reader.releaseLock();}
|
|
142
|
+
const meta={version:1,tags:entryTags,tagIds:tags([...entryTags,...softTags]).map(hash),stale:entry.stale,revalidate:entry.revalidate,expire:entry.expire};
|
|
143
|
+
const metadata=JSON.stringify(meta);if(Buffer.byteLength(metadata)>MAX_META)throw new Error('Redis cache metadata exceeds 64 KiB.');
|
|
144
|
+
const ttl=Math.ceil(Math.min(maxAgeSeconds,entry.expire)*1000);if(ttl<=0)return;
|
|
145
|
+
const {entry:entryKey,lock}=keys(key);
|
|
146
|
+
return Number(await command(['EVAL',WRITE,'3',entryKey,epochKey,lock,lease.token,lease.epoch,metadata,Buffer.concat(chunks,size),lease.incarnation,String(Math.max(0,performance.now()-lease.started)),String(ttl),retention]))===1;
|
|
147
|
+
}
|
|
148
|
+
async function updateTags(values,durations){
|
|
149
|
+
const list=tags(values);if(!list.length)return;
|
|
150
|
+
const expire=durations?.expire;if(expire!==undefined&&(!Number.isFinite(expire)||expire<0))throw new TypeError('Invalid Redis tag expiration.');
|
|
151
|
+
await command(['EVAL',INVALIDATE,'1',epochKey,String(expire===undefined?-1:Math.ceil(expire*1000)),retention,randomBytes(24).toString('hex'),JSON.stringify(list.map(hash))]);
|
|
152
|
+
}
|
|
153
|
+
return {get,set,acquire,updateTags,
|
|
154
|
+
async release(key,lease){if(lease?.token)await command(['EVAL',RELEASE,'1',keys(key).lock,lease.token]);},
|
|
155
|
+
async remove(key,lease){if(lease?.token)await command(['EVAL',REMOVE,'1',keys(key).entry,lease.token]);},
|
|
156
|
+
async refreshTags(){},async getExpiration(){return Infinity;},async close(){closed=true;await connecting?.catch(()=>{});if(owned?.isOpen)owned.destroy();}};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Legacy incremental handler for HTML/ISR, fetch and unstable_cache. */
|
|
160
|
+
function createRedisIncrementalCache(options){
|
|
161
|
+
const {serialize,deserialize}=require('node:v8');
|
|
162
|
+
const backend=createRedisCache({leaseMs:60000,...options},16*1024*1024);
|
|
163
|
+
const keyFor=key=>'incremental:v8-'+process.versions.v8.split('.')[0]+':'+key;
|
|
164
|
+
function leaseFor(lease){
|
|
165
|
+
if(!lease||!['token','epoch','incarnation'].every(field=>typeof lease[field]==='string'&&/^[a-f0-9]{48}$/.test(lease[field]))||!Number.isFinite(lease.startedAt))throw new Error('Incremental Redis writes require a valid generation lease.');
|
|
166
|
+
return {...lease,started:performance.now()-Math.max(0,Date.now()-lease.startedAt)};
|
|
167
|
+
}
|
|
168
|
+
return class RedisIncrementalCache {
|
|
169
|
+
get prnextLeaseVersion(){return 1;}
|
|
170
|
+
async acquire(key,{signal}={}){
|
|
171
|
+
const lease=await backend.acquire(keyFor(key),{signal});
|
|
172
|
+
return {token:lease.token,epoch:lease.epoch,incarnation:lease.incarnation,startedAt:Date.now()};
|
|
173
|
+
}
|
|
174
|
+
async release(key,lease){await backend.release(keyFor(key),lease);}
|
|
175
|
+
async get(key,context={}){
|
|
176
|
+
const entry=await backend.get(keyFor(key),tags([...new Set([...(context.tags||[]),...(context.softTags||[])])]));
|
|
177
|
+
if(!entry)return null;
|
|
178
|
+
const bytes=Buffer.from(await new Response(entry.value).arrayBuffer());
|
|
179
|
+
const payload=deserialize(bytes);
|
|
180
|
+
if(payload?.version!==1||!Number.isFinite(payload.lastModified)||!payload.value||typeof payload.value.kind!=='string')throw new Error('Invalid incremental Redis payload.');
|
|
181
|
+
return {value:payload.value,lastModified:payload.lastModified,isStale:entry.revalidate===0};
|
|
182
|
+
}
|
|
183
|
+
async set(key,value,context={}){
|
|
184
|
+
const lease=leaseFor(context.prnextLease);
|
|
185
|
+
if(value===null){await backend.remove(keyFor(key),lease);return true;}
|
|
186
|
+
const raw=context.cacheControl?.revalidate??value.revalidate??value._prnext?.revalidate??false;
|
|
187
|
+
if(raw!==false&&(!Number.isFinite(raw)||raw<0))throw new TypeError('Invalid incremental Redis revalidate value.');
|
|
188
|
+
if(raw===0)return true;
|
|
189
|
+
const revalidate=raw===false?31536000:raw;
|
|
190
|
+
const entryTags=tags([...new Set([...(context.tags||[]),...(context.softTags||[])])]);
|
|
191
|
+
const bytes=serialize({version:1,lastModified:Date.now(),value});
|
|
192
|
+
if(bytes.length>16*1024*1024)throw new Error('Incremental Redis entry exceeds 16 MiB.');
|
|
193
|
+
return backend.set(keyFor(key),Promise.resolve({timestamp:Date.now(),stale:0,revalidate,expire:Math.max(revalidate,options?.maxAgeSeconds??604800),tags:entryTags,
|
|
194
|
+
value:new ReadableStream({start(controller){controller.enqueue(bytes);controller.close();}})}),{lease});
|
|
195
|
+
}
|
|
196
|
+
async revalidateTag(values,durations){await backend.updateTags(Array.isArray(values)?values:[values],durations);}
|
|
197
|
+
resetRequestCache(){}
|
|
198
|
+
async close(){await backend.close();}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
module.exports={createRedisCache,createRedisIncrementalCache};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
export interface RedisCacheOptions {
|
|
3
|
+
/** Unique application namespace, shared by its replicas. */
|
|
4
|
+
namespace: string;
|
|
5
|
+
/** Runtime environment variable; the URL is never embedded in a build. Default REDIS_URL. */
|
|
6
|
+
urlEnv?: string;
|
|
7
|
+
/** An already-connected node-redis client. Its lifecycle remains yours. */
|
|
8
|
+
client?: {readonly isReady?: boolean; withTypeMapping(mapping: {36: BufferConstructor}): {sendCommand(args: (string | Buffer)[], options?: {abortSignal?: AbortSignal}): Promise<unknown>}};
|
|
9
|
+
leaseMs?: number;
|
|
10
|
+
waitMs?: number;
|
|
11
|
+
commandTimeoutMs?: number;
|
|
12
|
+
maxAgeSeconds?: number;
|
|
13
|
+
}
|
|
14
|
+
interface CacheEntry {value: ReadableStream<Uint8Array>; tags: string[]; timestamp: number; stale: number; revalidate: number; expire: number}
|
|
15
|
+
interface Lease {token: string; started: number; epoch: string; incarnation: string; release(): Promise<void>}
|
|
16
|
+
export declare function createRedisCache(options: RedisCacheOptions): {
|
|
17
|
+
get(key: string, softTags?: string[]): Promise<CacheEntry | undefined>;
|
|
18
|
+
set(key: string, entry: Promise<CacheEntry>, options?: {lease?: Lease; softTags?: string[]}): Promise<void>;
|
|
19
|
+
acquire(key: string, options?: {signal?: AbortSignal}): Promise<Lease>;
|
|
20
|
+
refreshTags(): Promise<void>;
|
|
21
|
+
getExpiration(): Promise<number>;
|
|
22
|
+
updateTags(tags: string[], durations?: {expire: number}): Promise<void>;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface IncrementalRedisLease {token: string; epoch: string; incarnation: string; startedAt: number}
|
|
27
|
+
/** Configure its returned constructor as cacheHandler; Cache Components use createRedisCache separately. */
|
|
28
|
+
export declare function createRedisIncrementalCache(options: RedisCacheOptions): new (context?: unknown) => {
|
|
29
|
+
readonly prnextLeaseVersion: 1;
|
|
30
|
+
acquire(key: string, options?: {signal?: AbortSignal}): Promise<IncrementalRedisLease>;
|
|
31
|
+
release(key: string, lease: IncrementalRedisLease): Promise<void>;
|
|
32
|
+
get(key: string, context?: {tags?: string[]; softTags?: string[]}): Promise<{value: unknown; lastModified: number; isStale?: boolean} | null>;
|
|
33
|
+
set(key: string, value: unknown, context?: {prnextLease?: IncrementalRedisLease; tags?: string[]; softTags?: string[]; cacheControl?: {revalidate: number | false}}): Promise<boolean>;
|
|
34
|
+
revalidateTag(tags: string | string[], durations?: {expire?: number}): Promise<void>;
|
|
35
|
+
resetRequestCache(): void;
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
const {
|
|
2
|
+
const { RequestCookieStore, ResponseCookieStore } = require('./cookies.cjs');
|
|
3
3
|
const { hasBasePath, addBasePath, removeBasePath } = require('./paths.cjs');
|
|
4
4
|
const { localePath, withLocale } = require('./locale.cjs');
|
|
5
5
|
|
|
@@ -14,21 +14,37 @@ function middlewareHeaders(init, headers) {
|
|
|
14
14
|
headers.set('x-middleware-override-headers', names.join(','));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function nextDataPath(pathname) {
|
|
18
|
+
const match = /^\/_next\/data\/([^/]+)\/(.*)\.json$/.exec(pathname);
|
|
19
|
+
if (!match) return { pathname };
|
|
20
|
+
const parts = match[2].split('/');
|
|
21
|
+
return { buildId: match[1], pathname: parts[0] === 'index' ? '/' : '/' + parts.join('/') };
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
class NextURL extends URL {
|
|
18
25
|
constructor(input, baseOrOptions, options) {
|
|
19
26
|
const base = typeof baseOrOptions === 'string' || baseOrOptions instanceof URL ? baseOrOptions : undefined;
|
|
20
27
|
const config = options || (base ? {} : baseOrOptions) || {};
|
|
21
28
|
super(input, base || config.base);
|
|
22
29
|
this._nextConfig = config.nextConfig || {};
|
|
30
|
+
this._forceLocale = config.forceLocale === true;
|
|
31
|
+
this._parseData = !(this._nextConfig.skipProxyUrlNormalize ?? this._nextConfig.skipMiddlewareUrlNormalize);
|
|
23
32
|
this._trailingSlash = super.pathname === '/' ? !!this._nextConfig.trailingSlash : super.pathname.endsWith('/');
|
|
24
33
|
this._basePath = hasBasePath(super.pathname, this._nextConfig.basePath) ? this._nextConfig.basePath || '' : '';
|
|
25
|
-
|
|
34
|
+
const logical = this._parseData ? nextDataPath(removeBasePath(super.pathname, this._basePath)) : { pathname: removeBasePath(super.pathname, this._basePath) };
|
|
35
|
+
this._buildId = logical.buildId;
|
|
36
|
+
this._locale = this._nextConfig.i18n ? localePath(logical.pathname, {...this._nextConfig.i18n,defaultLocale:this.defaultLocale}).locale : '';
|
|
37
|
+
}
|
|
38
|
+
get pathname() {
|
|
39
|
+
const raw = removeBasePath(super.pathname, this._basePath);
|
|
40
|
+
return localePath(this._parseData ? nextDataPath(raw).pathname : raw, this._nextConfig.i18n).pathname;
|
|
26
41
|
}
|
|
27
|
-
get pathname() { return localePath(removeBasePath(super.pathname, this._basePath), this._nextConfig.i18n).pathname; }
|
|
28
42
|
set pathname(value) { super.pathname = addBasePath(withLocale(String(value).startsWith('/') ? value : `/${value}`, this._locale, this.defaultLocale), this._basePath); }
|
|
29
43
|
get defaultLocale() { return this.domainLocale?.defaultLocale || this._nextConfig.i18n?.defaultLocale; }
|
|
30
44
|
get domainLocale() { return this._nextConfig.i18n?.domains?.find(value=>value.domain.toLowerCase()===this.host.toLowerCase()); }
|
|
31
45
|
get locale() { return this._locale; }
|
|
46
|
+
get buildId() { return this._buildId; }
|
|
47
|
+
set buildId(value) { this._buildId = value; }
|
|
32
48
|
set locale(value) {
|
|
33
49
|
if (!this._nextConfig.i18n?.locales.includes(value)) throw new TypeError(`Unknown locale: ${value}`);
|
|
34
50
|
const pathname=this.pathname;this._locale=value;this.pathname=pathname;
|
|
@@ -43,8 +59,13 @@ class NextURL extends URL {
|
|
|
43
59
|
set trailingSlash(value) { this._trailingSlash = value; }
|
|
44
60
|
get href() {
|
|
45
61
|
const url = new URL(super.href);
|
|
46
|
-
|
|
47
|
-
if (this.
|
|
62
|
+
let pathname = withLocale(this.pathname, this._locale, this._buildId || this._forceLocale ? undefined : this.defaultLocale);
|
|
63
|
+
if (this._buildId) {
|
|
64
|
+
pathname = pathname.replace(/\/$/, '');
|
|
65
|
+
pathname = '/_next/data/' + this._buildId + pathname + (this.pathname === '/' ? '/index.json' : '.json');
|
|
66
|
+
}
|
|
67
|
+
url.pathname = addBasePath(pathname, this._basePath);
|
|
68
|
+
if (this._trailingSlash && !this._buildId) { if (!url.pathname.endsWith('/')) url.pathname += '/'; }
|
|
48
69
|
else if (url.pathname !== '/') url.pathname = url.pathname.replace(/\/$/, '');
|
|
49
70
|
return url.href;
|
|
50
71
|
}
|
|
@@ -54,19 +75,18 @@ class NextURL extends URL {
|
|
|
54
75
|
super.href = value;
|
|
55
76
|
this._trailingSlash = super.pathname === '/' ? !!this._nextConfig.trailingSlash : super.pathname.endsWith('/');
|
|
56
77
|
this._basePath = hasBasePath(super.pathname, this._nextConfig.basePath) ? this._nextConfig.basePath || '' : '';
|
|
57
|
-
|
|
78
|
+
const logical = this._parseData ? nextDataPath(removeBasePath(super.pathname, this._basePath)) : { pathname: removeBasePath(super.pathname, this._basePath) };
|
|
79
|
+
this._buildId = logical.buildId;
|
|
80
|
+
this._locale = this._nextConfig.i18n ? localePath(logical.pathname, {...this._nextConfig.i18n,defaultLocale:this.defaultLocale}).locale : '';
|
|
58
81
|
}
|
|
59
|
-
clone() { return new NextURL(this.href, { nextConfig: this._nextConfig }); }
|
|
82
|
+
clone() { return new NextURL(this.href, { nextConfig: this._nextConfig, forceLocale: this._forceLocale }); }
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
class NextRequest extends Request {
|
|
63
86
|
constructor(input, init) {
|
|
64
87
|
super(input, init);
|
|
65
88
|
this.nextUrl = new NextURL(this.url, { nextConfig: init?.nextConfig || input?.nextUrl?._nextConfig });
|
|
66
|
-
this.cookies = new
|
|
67
|
-
mutable: true,
|
|
68
|
-
onChange: () => this.headers.set('cookie', this.cookies.toString()),
|
|
69
|
-
});
|
|
89
|
+
this.cookies = new RequestCookieStore(this.headers);
|
|
70
90
|
}
|
|
71
91
|
}
|
|
72
92
|
|
package/compat/server.d.cts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import type { CookieStore } from './headers.cjs';
|
|
2
2
|
export interface NextURLConfig { i18n?: import('./index.js').NextConfig['i18n']; basePath?: string; trailingSlash?: boolean; skipMiddlewareUrlNormalize?: boolean; skipProxyUrlNormalize?: boolean }
|
|
3
|
-
export class NextURL extends URL { constructor(input: string | URL, options?: { base?: string | URL; nextConfig?: NextURLConfig }); constructor(input: string | URL, base: string | URL, options?: { nextConfig?: NextURLConfig }); clone(): NextURL; locale: string; readonly defaultLocale?: string; readonly domainLocale?: NonNullable<NonNullable<import('./index.js').NextConfig['i18n']>['domains']>[number]; basePath: string; trailingSlash: boolean }
|
|
4
|
-
export
|
|
3
|
+
export class NextURL extends URL { constructor(input: string | URL, options?: { base?: string | URL; nextConfig?: NextURLConfig; forceLocale?: boolean }); constructor(input: string | URL, base: string | URL, options?: { nextConfig?: NextURLConfig; forceLocale?: boolean }); clone(): NextURL; buildId: string | undefined; locale: string; readonly defaultLocale?: string; readonly domainLocale?: NonNullable<NonNullable<import('./index.js').NextConfig['i18n']>['domains']>[number]; basePath: string; trailingSlash: boolean }
|
|
4
|
+
export interface RequestCookie { name: string; value: string }
|
|
5
|
+
export interface RequestCookieStore extends Iterable<[string, RequestCookie]> {
|
|
6
|
+
readonly size: number;
|
|
7
|
+
get(name: string | {name: string}): RequestCookie | undefined;
|
|
8
|
+
getAll(name?: string | {name: string}): RequestCookie[];
|
|
9
|
+
has(name: string): boolean;
|
|
10
|
+
set(name: string, value: string): this;
|
|
11
|
+
set(cookie: RequestCookie): this;
|
|
12
|
+
delete(name: string): boolean;
|
|
13
|
+
delete(names: string[]): boolean[];
|
|
14
|
+
clear(): this;
|
|
15
|
+
toString(): string;
|
|
16
|
+
}
|
|
17
|
+
export class NextRequest extends Request { constructor(input: RequestInfo | URL, init?: RequestInit & { nextConfig?: NextURLConfig }); nextUrl: NextURL; cookies: RequestCookieStore }
|
|
5
18
|
export interface MiddlewareResponseInit extends ResponseInit { request?: { headers?: Headers } }
|
|
6
19
|
export class NextResponse<Body = unknown> extends Response {
|
|
7
20
|
cookies: CookieStore;
|
|
@@ -27,6 +27,13 @@ function dynamicUsage(reason, request = context()) {
|
|
|
27
27
|
function staticBailout(reason, request = context()) {
|
|
28
28
|
if (diagnostics.enabled) diagnostics.record('dynamic', { route: diagnostics.route(request), reason });
|
|
29
29
|
const state = request?.staticState;
|
|
30
|
+
if (!state && request?.navigationStage && ['an uncached fetch', 'connection()', 'unstable_noStore()'].includes(reason)
|
|
31
|
+
&& !require('./data-cache.cjs').inCacheScope()) require('./navigation-stages.cjs').postponeStage(reason);
|
|
32
|
+
const requirement = request?.cacheConfig?.ensureStatic;
|
|
33
|
+
if (!state && (request?.navigationStage === 'shell' && ['shell', 'prefetch', 'navigation'].includes(requirement)
|
|
34
|
+
|| request?.navigationStage === 'prefetch' && ['prefetch', 'navigation'].includes(requirement))) {
|
|
35
|
+
require('./navigation-stages.cjs').postponeStage(reason);
|
|
36
|
+
}
|
|
30
37
|
if (!state) return true;
|
|
31
38
|
if (state.partial && state.mode === 'auto') {
|
|
32
39
|
state.dynamicReasons.add(reason);
|
package/compat/use-cache.cjs
CHANGED
|
@@ -35,6 +35,9 @@ function recordCacheDependency({ tags = [], revalidate } = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
function metadata(scope, context) { return { ...scope.life, revalidate: Math.min(scope.life.revalidate, scope.life.expire), tags: [...scope.tags], paths: getCachePaths(context), ...(scope.externalCache ? { externalCache: true } : {}) }; }
|
|
37
37
|
function propagate(info, parent, context) {
|
|
38
|
+
if (context?.staticState && context.cacheConfig?.ensureStatic === 'navigation' && (info.expire < 300 || info.stale < 30 || info.revalidate === 0)) {
|
|
39
|
+
dynamicUsage('a cache lifetime with expire below 300 seconds, stale below 30 seconds, or revalidate=0', context);
|
|
40
|
+
}
|
|
38
41
|
trackStaticDependency(info, context);
|
|
39
42
|
if (!parent || parent === true) return;
|
|
40
43
|
if (info.externalCache) parent.externalCache = true;
|
package/docs/ai/compatibility.md
CHANGED
|
@@ -18,12 +18,181 @@ transpilation, image/font tooling and caching deserve targeted build and browser
|
|
|
18
18
|
tests. Support for one configuration shape does not imply support for all
|
|
19
19
|
upstream plugin behavior.
|
|
20
20
|
|
|
21
|
+
## Instrumentation and tracing
|
|
22
|
+
|
|
23
|
+
Use a single `instrumentation.ts`, `.js`, `.mjs` or `.cjs` at the project root
|
|
24
|
+
or in `src`. Export an optional async `register()` for runtime initialization
|
|
25
|
+
and an optional async `onRequestError(error, request, context)` for reporting.
|
|
26
|
+
The `Instrumentation.onRequestError` type is exported from PRNext's root API.
|
|
27
|
+
`process.env.NEXT_RUNTIME` distinguishes `nodejs` and `edge`; conditional imports
|
|
28
|
+
keep Node SDKs out of an Edge bundle. Registration runs once per server realm,
|
|
29
|
+
including the separate RSC realm, before application requests. A production
|
|
30
|
+
prerender does not run the runtime startup hook. Request-time ISR and PPR
|
|
31
|
+
regeneration initialize their server realm and report failures through the same
|
|
32
|
+
hooks. Repeated propagation of an RSC error to the HTML worker does not report
|
|
33
|
+
the same error again.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import type { Instrumentation } from '@thomas.f/prnext'
|
|
37
|
+
|
|
38
|
+
export async function register() {
|
|
39
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
40
|
+
await import('./observability.node')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const onRequestError: Instrumentation.onRequestError = async (
|
|
45
|
+
error, request, context
|
|
46
|
+
) => {
|
|
47
|
+
await reportError(error, request, context)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`reportError` above is an application function, not a PRNext API. Initialization
|
|
52
|
+
has a 30-second budget. Each error hook has a five-second budget; reporting
|
|
53
|
+
failure does not replace the application failure. Redirect/not-found control
|
|
54
|
+
signals are excluded. Hooks receive request information: the application owns
|
|
55
|
+
redaction before sending that information to a third party.
|
|
56
|
+
|
|
57
|
+
For built-in correlated traces, explicitly set
|
|
58
|
+
`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` to an OTLP HTTP collector's complete traces
|
|
59
|
+
URL and `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/json`. Alternatively,
|
|
60
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` supplies a base URL with `/v1/traces` appended.
|
|
61
|
+
`OTEL_SERVICE_NAME`, `OTEL_EXPORTER_OTLP_TRACES_HEADERS` (or general
|
|
62
|
+
`OTEL_EXPORTER_OTLP_HEADERS`) and `OTEL_SDK_DISABLED=true` are supported. Keep
|
|
63
|
+
exporter credentials in the server environment. This built-in path supports
|
|
64
|
+
HTTP JSON, not gRPC or HTTP protobuf; use your own SDK for other protocols.
|
|
65
|
+
|
|
66
|
+
Native HTTP spans cover static and dynamic responses through body completion
|
|
67
|
+
or cancellation. Node, RSC and fetch spans share W3C trace context. Export queues
|
|
68
|
+
are bounded and telemetry is best effort; an unavailable collector does not
|
|
69
|
+
block a site response. Built-in span attributes exclude bodies, cookie values,
|
|
70
|
+
query strings and authorization headers. An application-installed
|
|
71
|
+
`@opentelemetry/api` can use the same parent context when its SDK installs a
|
|
72
|
+
context manager in `register`. No SDK is installed automatically, and this does
|
|
73
|
+
not certify every vendor integration.
|
|
74
|
+
|
|
75
|
+
## Production bytes and navigation
|
|
76
|
+
|
|
77
|
+
Production CSS Modules use deterministic compact class and animation names.
|
|
78
|
+
Global classes and declared composition exports retain their meaning; development
|
|
79
|
+
keeps readable names. Internal JavaScript exports are shortened by the selected
|
|
80
|
+
compiler, while public exports and dynamic imports keep their interfaces. Asset
|
|
81
|
+
hashes and source maps remain compiler-owned.
|
|
82
|
+
|
|
83
|
+
App navigation can negotiate binary differential Flight responses. Every request
|
|
84
|
+
still runs fresh rendering and access checks. The server retains at most 64
|
|
85
|
+
completed bases, 32 MiB in total, with a two-MiB per-base limit and 30-second
|
|
86
|
+
expiry. Bases are keyed by deployment and credentials; an unknown, expired or
|
|
87
|
+
incompatible base produces ordinary Flight. The browser retains one completed
|
|
88
|
+
base, streams reconstructed bytes to React and can retry one failed differential
|
|
89
|
+
navigation as a full GET. Server Actions are never replayed through this path.
|
|
90
|
+
Per worker, at most eight captures and two encoders run together. Additional
|
|
91
|
+
requests use ordinary Flight, keeping transient transport work bounded too.
|
|
92
|
+
|
|
21
93
|
Configuration uses one `next.config.*` or `prnext.config.*` file and rejects
|
|
22
|
-
unknown keys rather than ignoring them.
|
|
23
|
-
|
|
94
|
+
unknown keys rather than ignoring them. React Compiler is available through an
|
|
95
|
+
explicit `reactCompiler` option described below. `typedRoutes: true` is
|
|
24
96
|
implemented through PRNext's generated route registry. Do not copy a full
|
|
25
97
|
Next.js configuration template into a new application.
|
|
26
98
|
|
|
99
|
+
The repository's reference tests build and run matching applications against
|
|
100
|
+
Next.js 16.4.3, including mounted App navigation, form Actions and cookies,
|
|
101
|
+
metadata, Pages locale routing, redirects, APIs and selected webpack loader
|
|
102
|
+
contracts. These executable examples cover specific behavior; they do not
|
|
103
|
+
certify every Next.js application or plugin. `NextRequest.nextUrl` understands
|
|
104
|
+
data-route build IDs and locales, and request-cookie mutations synchronize the
|
|
105
|
+
request header. Request cookies have different mutation results from response
|
|
106
|
+
cookies; do not use them interchangeably.
|
|
107
|
+
|
|
108
|
+
A locked real-application fixture additionally exercises Better Auth 1.7.14
|
|
109
|
+
(email/password, session cookies and logout), Drizzle ORM 0.45.4 over SQLite
|
|
110
|
+
through its proxy driver, next-intl 4.14.9 (server and client translations),
|
|
111
|
+
and the MDX 3.1.1 webpack loader against Next.js 16.4.3. It covers authenticated
|
|
112
|
+
Server Actions, user-owned rows, cookie/locale isolation, client navigation and
|
|
113
|
+
Server Component `after()` callbacks. This is evidence for those workflows,
|
|
114
|
+
not a guarantee for every plugin, provider or deployment configuration.
|
|
115
|
+
|
|
116
|
+
PRNext-specific options such as `prerender` and `htmlOnly` are described in
|
|
117
|
+
[Build and deployment](deployment.md). Do not pass them to an upstream Next.js
|
|
118
|
+
build when comparing the two implementations.
|
|
119
|
+
|
|
120
|
+
## TypeScript 7 and earlier compilers
|
|
121
|
+
|
|
122
|
+
PRNext uses the application's installed TypeScript. TypeScript 7 runs through its
|
|
123
|
+
official native `tsc` command for production checks, `prnext typecheck` and live
|
|
124
|
+
development diagnostics. Install TypeScript normally, including its platform
|
|
125
|
+
optional dependencies. PRNext does not download a compiler during a build.
|
|
126
|
+
TypeScript 5 and 6 retain their compiler API integration.
|
|
127
|
+
|
|
128
|
+
Checks use `--noEmit` and keep incremental compiler state in an owned temporary
|
|
129
|
+
directory. Development reports updated errors and clears fixed diagnostics;
|
|
130
|
+
new source files and configuration changes restart the native watch generation.
|
|
131
|
+
`typedRoutes` also supports native TypeScript and preserves JSONC comments when
|
|
132
|
+
adding its generated declaration to the config. Compiler support does not imply
|
|
133
|
+
that every application is ready for TypeScript 7's own language/config changes.
|
|
134
|
+
|
|
135
|
+
## React Compiler
|
|
136
|
+
|
|
137
|
+
Enable the bundled official React Compiler in the project's existing config:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
export default {
|
|
141
|
+
reactCompiler: true,
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
No separate compiler installation is required. The default is disabled;
|
|
146
|
+
`reactCompiler: false` preserves the uncompiled component path. Enabled builds
|
|
147
|
+
optimize project-owned browser components in both Pages and App applications,
|
|
148
|
+
in development and production. App Client Components also use the transform in
|
|
149
|
+
their isolated server-rendering graph. The Pages server graph stays uncompiled,
|
|
150
|
+
including APIs, custom Document code and data-loader utilities, so ordinary
|
|
151
|
+
server functions never acquire client-only memoization hooks. React Server
|
|
152
|
+
Components, Server Action modules, PRNext internals, generated entrypoints and
|
|
153
|
+
npm packages are not compiled. Keep the matching React 19 peer versions required
|
|
154
|
+
by the installed framework.
|
|
155
|
+
|
|
156
|
+
Ordinary `.mts`/`.cts` hook helpers can be optimized; server/client directives
|
|
157
|
+
in those extensions remain unsupported. Decorated `.mts`/`.cts` utility modules
|
|
158
|
+
keep their native compilation path while importing React components can still
|
|
159
|
+
be optimized.
|
|
160
|
+
|
|
161
|
+
For gradual adoption, use `reactCompiler: { compilationMode: 'annotation' }`, then
|
|
162
|
+
put `"use memo";` at the beginning of each component or hook to optimize. In
|
|
163
|
+
either mode, `"use no memo";` excludes an individual function. Supported object
|
|
164
|
+
options are `compilationMode: 'infer' | 'annotation'`,
|
|
165
|
+
`panicThreshold: 'none' | 'critical_errors' | 'all_errors'` and `target: '19'`.
|
|
166
|
+
Defaults are `'infer'`, `'none'` and `'19'`; other fields are rejected.
|
|
167
|
+
|
|
168
|
+
After a successful build, inspect the compiler's actual results:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
prnext compiler
|
|
172
|
+
prnext compiler --json
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The report names observed functions, their project-relative files, optimization
|
|
176
|
+
or skip status, and the compiler's reasons. These are transform decisions;
|
|
177
|
+
server-code removal and tree shaking can later omit a function from the browser
|
|
178
|
+
bundle. Cached transforms retain their
|
|
179
|
+
diagnostics and are identified as reused. Browser and server graphs do not
|
|
180
|
+
inflate the function count. An incremental build can reuse graph inputs without
|
|
181
|
+
observing every transform; the report states that scope rather than claiming
|
|
182
|
+
complete application coverage. Disabled compilation is reported explicitly.
|
|
183
|
+
The report describes the last successful build, stays outside public assets and
|
|
184
|
+
does not include source code. A failed build preserves the previous report.
|
|
185
|
+
|
|
186
|
+
The compiler can skip code that cannot be safely optimized. Follow the Rules of
|
|
187
|
+
React and check actual behavior rather than assuming every component was
|
|
188
|
+
compiled. It reduces repeated component work; it does not guarantee faster
|
|
189
|
+
server responses, smaller JavaScript or a specific speed multiplier. Validate
|
|
190
|
+
hydration, interactions and Fast Refresh in the application, then compare a
|
|
191
|
+
production build with the option enabled and disabled. Compilation adds build
|
|
192
|
+
work; PRNext caches pure transforms with their compiler version and settings.
|
|
193
|
+
|
|
194
|
+
See the [official React Compiler documentation](https://react.dev/learn/react-compiler).
|
|
195
|
+
|
|
27
196
|
Only explicit private Next.js import adapters are supported. Do not add arbitrary
|
|
28
197
|
`next/dist/...` imports or install another Next runtime to satisfy one: isolate
|
|
29
198
|
or replace the incompatible dependency, retain the original framework where
|