@thomas.f/prnext 0.1.18-alpha → 0.1.19-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 +46 -25
- package/build/compiler.mjs +5 -4
- package/build/config.mjs +4 -2
- 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/incremental.mjs +112 -18
- package/build/index.mjs +32 -3
- 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/npm-packages.mjs +1 -0
- package/build/production-builder.mjs +1 -1
- package/build/standalone.mjs +21 -3
- package/build/webpack-graph.mjs +13 -3
- package/cli.mjs +10 -1
- package/compat/app-dynamic.cjs +12 -1
- package/compat/cache-handlers.cjs +3 -1
- package/compat/flight-delta.cjs +3 -1
- package/compat/index.d.ts +1 -1
- package/compat/link.cjs +2 -2
- package/docs/ai/compatibility.md +77 -0
- package/docs/ai/routing.md +11 -0
- package/docs/ai/troubleshooting.md +79 -0
- package/package.json +21 -9
- package/runtime/app-client.mjs +119 -42
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +3 -1
- package/runtime/dev-client.mjs +7 -2
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +60 -19
- package/runtime/flight-delta.mjs +28 -4
- package/runtime/fs.mjs +15 -8
- package/runtime/lazy-import-client.mjs +38 -0
- package/vendor/nft/LICENSE +7 -0
- package/vendor/nft/README.md +16 -0
- package/vendor/nft/out/analyze.d.ts +8 -0
- package/vendor/nft/out/analyze.js +1216 -0
- package/vendor/nft/out/analyze.js.map +1 -0
- package/vendor/nft/out/cli.d.ts +2 -0
- package/vendor/nft/out/cli.js +94 -0
- package/vendor/nft/out/cli.js.map +1 -0
- package/vendor/nft/out/fs.d.ts +22 -0
- package/vendor/nft/out/fs.js +106 -0
- package/vendor/nft/out/fs.js.map +1 -0
- package/vendor/nft/out/index.d.ts +4 -0
- package/vendor/nft/out/index.js +25 -0
- package/vendor/nft/out/index.js.map +1 -0
- package/vendor/nft/out/node-file-trace.d.ts +41 -0
- package/vendor/nft/out/node-file-trace.js +380 -0
- package/vendor/nft/out/node-file-trace.js.map +1 -0
- package/vendor/nft/out/resolve-dependency.d.ts +6 -0
- package/vendor/nft/out/resolve-dependency.js +427 -0
- package/vendor/nft/out/resolve-dependency.js.map +1 -0
- package/vendor/nft/out/types.d.ts +66 -0
- package/vendor/nft/out/types.js +2 -0
- package/vendor/nft/out/types.js.map +1 -0
- package/vendor/nft/out/utils/ast-helpers.js +49 -0
- package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
- package/vendor/nft/out/utils/binary-locators.js +88 -0
- package/vendor/nft/out/utils/binary-locators.js.map +1 -0
- package/vendor/nft/out/utils/get-package-base.js +30 -0
- package/vendor/nft/out/utils/get-package-base.js.map +1 -0
- package/vendor/nft/out/utils/interop-require.js +24 -0
- package/vendor/nft/out/utils/interop-require.js.map +1 -0
- package/vendor/nft/out/utils/node-version.js +10 -0
- package/vendor/nft/out/utils/node-version.js.map +1 -0
- package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
- package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
- package/vendor/nft/out/utils/special-cases.js +347 -0
- package/vendor/nft/out/utils/special-cases.js.map +1 -0
- package/vendor/nft/out/utils/static-eval.js +579 -0
- package/vendor/nft/out/utils/static-eval.js.map +1 -0
- package/vendor/nft/out/utils/types.js +2 -0
- package/vendor/nft/out/utils/types.js.map +1 -0
- package/vendor/nft/out/utils/wrappers.js +659 -0
- package/vendor/nft/out/utils/wrappers.js.map +1 -0
- package/vendor/nft/package.json +4 -0
- package/vendor/nft/upstream.json +51 -0
package/build/standalone.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { nodeFileTrace } from '
|
|
2
|
-
import resolver from '
|
|
1
|
+
import { nodeFileTrace } from '../vendor/nft/out/index.js';
|
|
2
|
+
import resolver from '../vendor/nft/out/resolve-dependency.js';
|
|
3
|
+
import { CachedFileSystem } from '../vendor/nft/out/fs.js';
|
|
3
4
|
import picomatch from 'picomatch';
|
|
4
5
|
import { mkdir, readFile, writeFile, readdir, realpath, readlink, stat, lstat, copyFile, chmod, symlink, cp } from 'node:fs/promises';
|
|
5
6
|
import path from 'node:path';
|
|
@@ -12,6 +13,10 @@ const within = (root, file) => file === root || file.startsWith(root + path.sep)
|
|
|
12
13
|
const missing = error => error.code === 'ENOENT' || error.code === 'ENOTDIR';
|
|
13
14
|
const maxFiles = 100_000;
|
|
14
15
|
const maxBytes = 2 * 1024 ** 3;
|
|
16
|
+
const linuxRuntimeProbes = new Set([
|
|
17
|
+
'/proc/self', '/proc/self/exe', '/usr/bin/ldd',
|
|
18
|
+
'/proc/version', '/proc/self/cgroup', '/.dockerenv', '/run/.containerenv',
|
|
19
|
+
]);
|
|
15
20
|
|
|
16
21
|
export async function resolveTraceDependency(...args) {
|
|
17
22
|
const result = await resolver.default(...args);
|
|
@@ -75,6 +80,10 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
|
|
|
75
80
|
const warnings = new Set();
|
|
76
81
|
let omittedWarnings = 0;
|
|
77
82
|
const cache = {};
|
|
83
|
+
const traceFileSystem = new CachedFileSystem({ cache, fileIOConcurrency: 16 });
|
|
84
|
+
// libc and environment detectors read the running host, not application assets.
|
|
85
|
+
// Keep this exact and outside the tracing root; other external files still fail.
|
|
86
|
+
const systemProbe = file => process.platform === 'linux' && !within(tracingRoot, file) && linuxRuntimeProbes.has(file);
|
|
78
87
|
const traceLocations = new Map();
|
|
79
88
|
let traceReads = 0;
|
|
80
89
|
let traceBytes = 0;
|
|
@@ -120,7 +129,16 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
|
|
|
120
129
|
for (const conditions of [['node'], ['node', 'react-server']]) {
|
|
121
130
|
const result = await nodeFileTrace(entries, {
|
|
122
131
|
base: path.parse(projectRoot).root, processCwd: projectRoot, conditions, cache, fileIOConcurrency: 16,
|
|
123
|
-
ignore(file) { const absolute = path.resolve(path.parse(projectRoot).root, file); return !within(stage, absolute) && (excluded.some(match => match(slash(absolute))) || /^\.env(?:\.|$)/.test(path.basename(absolute))); },
|
|
132
|
+
ignore(file) { const absolute = path.resolve(path.parse(projectRoot).root, file); return systemProbe(absolute) || !within(stage, absolute) && (excluded.some(match => match(slash(absolute))) || /^\.env(?:\.|$)/.test(path.basename(absolute))); },
|
|
133
|
+
// Share the metadata concurrency limit between stat and readlink.
|
|
134
|
+
stat: file => traceFileSystem.stat(file),
|
|
135
|
+
async readlink(file) {
|
|
136
|
+
// NFT follows symlinks before ignore(), so never follow these probes
|
|
137
|
+
// into /proc/<pid> or the build machine's Node executable.
|
|
138
|
+
if (systemProbe(file)) return null;
|
|
139
|
+
try { return await traceFileSystem.readlink(file); }
|
|
140
|
+
catch (error) { if (error.code === 'ENOTDIR') return null; throw error; }
|
|
141
|
+
},
|
|
124
142
|
async readFile(file) {
|
|
125
143
|
let info;
|
|
126
144
|
try { info = await stat(file); } catch (error) { if (missing(error)) return null; throw error; }
|
package/build/webpack-graph.mjs
CHANGED
|
@@ -77,6 +77,13 @@ function pluginIdentity(plugin,webpack){
|
|
|
77
77
|
export const webpackCompilerStats=()=>({contexts:sessions.size,created,reused,limit:sessionLimit,buildLimit:sessionBuildLimit});
|
|
78
78
|
function queued(callback){const task=graphQueue.then(callback);graphQueue=task.catch(()=>{});return task;}
|
|
79
79
|
export function closeWebpackCompiler(root){return queued(async()=>{
|
|
80
|
+
if(root!==undefined){
|
|
81
|
+
try{root=await realpath(root);}
|
|
82
|
+
catch(error){
|
|
83
|
+
if(error.code!=='ENOENT'&&error.code!=='ENOTDIR')throw error;
|
|
84
|
+
root=path.resolve(root);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
80
87
|
const closing=[];for(const[key,session]of sessions)if(root===undefined||session.root===root){sessions.delete(key);closing.push(session.dispose());}
|
|
81
88
|
const results=await Promise.allSettled(closing),errors=results.filter(result=>result.status==='rejected').map(result=>result.reason);
|
|
82
89
|
if(errors.length)throw new AggregateError(errors,'Unable to dispose webpack compiler sessions');
|
|
@@ -84,6 +91,9 @@ export function closeWebpackCompiler(root){return queued(async()=>{
|
|
|
84
91
|
|
|
85
92
|
async function compileGraph(resolver,loads,options,{state,config,target}){
|
|
86
93
|
return queued(async()=>{
|
|
94
|
+
// Ownership uses one canonical identity for invalidation and disposal.
|
|
95
|
+
// Compiler paths and graph fingerprints retain the caller's lexical root.
|
|
96
|
+
const ownerRoot=await realpath(state.root);
|
|
87
97
|
const webpack=(await import('webpack')).default;
|
|
88
98
|
let reusable=state.dev&&state.incremental,identity;
|
|
89
99
|
if(reusable){try{identity=await buildCacheIdentity(state.root);}catch{}if(!identity)reusable=false;}
|
|
@@ -97,17 +107,17 @@ async function compileGraph(resolver,loads,options,{state,config,target}){
|
|
|
97
107
|
// Unknown data structures, safe mode and unavailable identities all keep
|
|
98
108
|
// compilation correct through a one-shot compiler. Hidden mutable closure
|
|
99
109
|
// state cannot be fingerprinted: replace the plugin instance when it changes.
|
|
100
|
-
if(state.dev&&!reusable)for(const[key,session]of sessions)if(session.root===
|
|
110
|
+
if(state.dev&&!reusable)for(const[key,session]of sessions)if(session.root===ownerRoot){sessions.delete(key);await session.dispose();}
|
|
101
111
|
let session=reusable&&sessions.get(key);
|
|
102
112
|
if(session&&session.builds>=sessionBuildLimit){sessions.delete(key);await session.dispose();session=undefined;}
|
|
103
113
|
if(reusable){
|
|
104
|
-
for(const[old,previous]of sessions)if(previous.root===
|
|
114
|
+
for(const[old,previous]of sessions)if(previous.root===ownerRoot&&(previous.identity!==identity||previous.base===base&&old!==key)){sessions.delete(old);await previous.dispose();}
|
|
105
115
|
}
|
|
106
116
|
if(session){sessions.delete(key);reused++;}
|
|
107
117
|
else{
|
|
108
118
|
if(reusable)while(sessions.size>=sessionLimit){const[old,previous]=sessions.entries().next().value;sessions.delete(old);await previous.dispose();}
|
|
109
119
|
session=await createGraphSession(state.root,{webpack,reusable,stage:state.stage});created++;
|
|
110
|
-
Object.assign(session,{root:
|
|
120
|
+
Object.assign(session,{root:ownerRoot,base,identity,builds:0});
|
|
111
121
|
}
|
|
112
122
|
if(reusable)sessions.set(key,session);
|
|
113
123
|
try{session.builds++;return await session.run(resolver,loads,options,{state,config,target});}
|
package/cli.mjs
CHANGED
|
@@ -58,6 +58,8 @@ Migration options: --dry-run (preview), --no-install (prepare only), --json
|
|
|
58
58
|
prnext migrate --check Audit source compatibility without migrating
|
|
59
59
|
prnext migrate --check --against URL --candidate URL [--routes paths.json]
|
|
60
60
|
Compare GET responses before switching
|
|
61
|
+
prnext migrate --check --against URL --candidate URL --browser-scenarios ./scenarios.mjs
|
|
62
|
+
Run trusted application browser assertions
|
|
61
63
|
Migration keeps Next.js and backs up package.json and existing lockfiles.
|
|
62
64
|
Simple next dev/build/start scripts become prnext commands; custom shell scripts need a manual edit.
|
|
63
65
|
Installation honors npm, pnpm, Yarn Classic/Berry and the workspace root.
|
|
@@ -191,13 +193,20 @@ async function main() {
|
|
|
191
193
|
report.comparison = await compareServers({ ...options, routes });
|
|
192
194
|
report.ok &&= report.comparison.ok;
|
|
193
195
|
}
|
|
196
|
+
if (options.browserScenarios) {
|
|
197
|
+
const { runMigrationScenarios } = await import('./build/migration-scenarios.mjs');
|
|
198
|
+
report.browser = await runMigrationScenarios({ directory, ...options, scenarios: options.browserScenarios });
|
|
199
|
+
report.ok &&= report.browser.ok;
|
|
200
|
+
}
|
|
194
201
|
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
195
202
|
else {
|
|
196
203
|
console.log(`PRNext migration check ${report.ok ? 'passed' : 'failed'}: ${directory}`);
|
|
197
204
|
console.log(`${report.files} source files; ${report.routes.length} routes.`);
|
|
198
205
|
for (const item of report.findings) console.log(`${item.severity.toUpperCase()} ${item.file || 'project'}${item.line ? ':' + item.line : ''}: ${item.message}\n ${item.action}`);
|
|
199
206
|
for (const result of report.comparison?.results || []) console.log(`${result.equal ? 'MATCH' : 'DIFFERENT'} ${result.route}: ${result.error || result.differences.join(', ') || 'responses match'}`);
|
|
200
|
-
for (const
|
|
207
|
+
for (const result of report.browser?.results || []) console.log(`BROWSER ${result.scenario} (${result.target}): ${result.status}${result.reason ? ' — ' + result.reason : ''}`);
|
|
208
|
+
if (report.browser?.reason) console.log(`BROWSER unknown: ${report.browser.reason}`);
|
|
209
|
+
for (const note of [...report.notes, ...(report.comparison ? [report.comparison.scope] : []), ...(report.browser ? [report.browser.scope] : [])]) console.log(note);
|
|
201
210
|
}
|
|
202
211
|
if (!report.ok) process.exitCode = 1;
|
|
203
212
|
return;
|
package/compat/app-dynamic.cjs
CHANGED
|
@@ -19,7 +19,18 @@ function dynamic(input, options) {
|
|
|
19
19
|
if (!opts.ssr && typeof React.useEffect !== 'function') {
|
|
20
20
|
throw new Error('ssr: false is not supported with next/dynamic in Server Components. Move it into a Client Component.');
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
let Lazy;
|
|
23
|
+
if(process.env.NODE_ENV==='development'&&typeof window!=='undefined'&&typeof opts.__prnextDevIdentity==='string'){
|
|
24
|
+
const registry=globalThis.__PRNEXT_DEV_DYNAMIC__||=new Map();
|
|
25
|
+
let entry=registry.get(opts.__prnextDevIdentity);
|
|
26
|
+
if(!entry||entry.failed){
|
|
27
|
+
entry={loader:opts.loader};
|
|
28
|
+
entry.lazy=React.lazy(()=>Promise.resolve().then(()=>entry.loader()).then(moduleOf,error=>{entry.failed=true;throw error;}));
|
|
29
|
+
if(registry.size>=2048)registry.delete(registry.keys().next().value);
|
|
30
|
+
registry.set(opts.__prnextDevIdentity,entry);
|
|
31
|
+
}else entry.loader=opts.loader;
|
|
32
|
+
Lazy=entry.lazy;
|
|
33
|
+
}else Lazy=React.lazy(() => Promise.resolve().then(opts.loader).then(moduleOf));
|
|
23
34
|
function DynamicComponent(props) {
|
|
24
35
|
const fallback = opts.loading ? React.createElement(opts.loading, { isLoading: true, pastDelay: true, error: null }) : null;
|
|
25
36
|
const child = React.createElement(Lazy, props);
|
|
@@ -80,7 +80,9 @@ async function read(entry, context) {
|
|
|
80
80
|
if (size > MAX_BYTES) throw new Error('CacheEntry exceeds 2 MiB');
|
|
81
81
|
chunks.push(Buffer.from(value));
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
// Buffer.from above owns each chunk even when a custom producer reuses its
|
|
84
|
+
// Uint8Array on the next pull. Keep that copy, but do not copy it twice.
|
|
85
|
+
return chunks.length===1?chunks[0]:Buffer.concat(chunks, size);
|
|
84
86
|
} finally {
|
|
85
87
|
if (!complete) void reader.cancel().catch(() => {});
|
|
86
88
|
reader.releaseLock();
|
package/compat/flight-delta.cjs
CHANGED
|
@@ -55,7 +55,9 @@ function collectStream(source,{onComplete,onCancel,onFinally,capture=true}={}) {
|
|
|
55
55
|
async function deltaStream(source,base,{assess}={}) {
|
|
56
56
|
const reader=source.getReader(), first=await reader.read();
|
|
57
57
|
if(first.done)return {delta:false,body:new ReadableStream({start(c){c.close();}})};
|
|
58
|
-
|
|
58
|
+
// A short first chunk always selects ordinary Flight. Do not build a base
|
|
59
|
+
// index that this response cannot use, or wait for a later streaming chunk.
|
|
60
|
+
let index=first.value.length>=WINDOW&&base instanceof Uint8Array&&base.length<=MAX_VALUE?createIndex(base):null;
|
|
59
61
|
const profitable=(raw,frames,initial)=>frames&&frames.reduce((n,b)=>n+b.length,0)+(initial?5:1)<raw.length*0.9&&(!assess||assess(raw,join(initial?[MAGIC,...frames,new Uint8Array([2])]:frames,frames.reduce((n,b)=>n+b.length,0)+(initial?5:0)))===true);
|
|
60
62
|
const firstFrames=index&&first.value.length>=WINDOW?encodeChunk(first.value,base,index):null;
|
|
61
63
|
let useDelta;
|
package/compat/index.d.ts
CHANGED
|
@@ -167,7 +167,7 @@ export interface NextConfig {
|
|
|
167
167
|
implementation?: 'sass' | 'sass-embedded';
|
|
168
168
|
[option: string]: unknown;
|
|
169
169
|
};
|
|
170
|
-
experimental?: { nextScriptWorkers?: boolean; optimizePackageImports?: string[]; proxyClientMaxBodySize?: number | string; serverActions?: true | { allowedOrigins?: string[]; bodySizeLimit?: number | string } };
|
|
170
|
+
experimental?: { nextScriptWorkers?: boolean; /** App Router development only; compile literal client imports on first use. */ lazyDynamicImports?: boolean; optimizePackageImports?: string[]; proxyClientMaxBodySize?: number | string; serverActions?: true | { allowedOrigins?: string[]; bodySizeLimit?: number | string } };
|
|
171
171
|
trailingSlash?: boolean;
|
|
172
172
|
skipTrailingSlashRedirect?: boolean;
|
|
173
173
|
skipMiddlewareUrlNormalize?: boolean;
|
package/compat/link.cjs
CHANGED
|
@@ -21,11 +21,11 @@ const Link = React.forwardRef(function Link({ href, as, children, replace, scrol
|
|
|
21
21
|
const source = formatUrl(href);
|
|
22
22
|
const requestPrefetch = React.useCallback((priority = 'intent') => {
|
|
23
23
|
if (prefetch === false || process.env.NODE_ENV !== 'production') return;
|
|
24
|
-
if (app
|
|
24
|
+
if (app && (app.router.partialPrefetch || prefetch === true)) app.router.prefetch(target, { priority, full: prefetch === true });
|
|
25
25
|
if (!app && pages) void pages.prefetch(source, as ? target : undefined, { locale, priority }).catch(() => {});
|
|
26
26
|
}, [app, pages, source, target, as, locale, prefetch]);
|
|
27
27
|
React.useEffect(() => {
|
|
28
|
-
if ((app ? !app.router.partialPrefetch : !pages) || prefetch === false || process.env.NODE_ENV !== 'production' || typeof IntersectionObserver === 'undefined' || !anchorRef.current) return;
|
|
28
|
+
if ((app ? !app.router.partialPrefetch && prefetch !== true : !pages) || prefetch === false || process.env.NODE_ENV !== 'production' || typeof IntersectionObserver === 'undefined' || !anchorRef.current) return;
|
|
29
29
|
const observer = new IntersectionObserver(entries => {
|
|
30
30
|
if (entries.some(entry => entry.isIntersecting)) { observer.disconnect(); requestPrefetch('viewport'); }
|
|
31
31
|
}, { rootMargin: '200px' });
|
package/docs/ai/compatibility.md
CHANGED
|
@@ -236,6 +236,29 @@ by silently adding `--force` or `--legacy-peer-deps`.
|
|
|
236
236
|
can run project code, and installed custom loaders still require a real build.
|
|
237
237
|
The audit is not a sandbox for untrusted projects.
|
|
238
238
|
|
|
239
|
+
The version 2 JSON report follows static imports from project sources into the
|
|
240
|
+
installed packages they reference. It checks explicit PRNext adapters and their
|
|
241
|
+
statically declared named exports, and includes the importing file, line and
|
|
242
|
+
import chain when a dependency uses an unavailable API. It resolves selected
|
|
243
|
+
Node, browser and React Server Component package conditions, including workspace
|
|
244
|
+
symlinks, without importing those package modules. Unreferenced packages are not
|
|
245
|
+
enumerated. Runtime branches, custom loader output and every possible bundler
|
|
246
|
+
condition remain outside this check.
|
|
247
|
+
|
|
248
|
+
A module-level `use server` directive is a server boundary even when a Client
|
|
249
|
+
Component reaches it through a re-export. Its dependencies are still audited in
|
|
250
|
+
the server context. A client dependency on a Node builtin needs an actual browser
|
|
251
|
+
implementation; package `browser` mappings and installed shims are resolved
|
|
252
|
+
before diagnosing it. Custom configuration aliases still require a build with
|
|
253
|
+
that application's configuration.
|
|
254
|
+
|
|
255
|
+
`evidence.state: "checked"` means the selected static graph was inspected, not
|
|
256
|
+
that migration is certified. `"unknown"` identifies unresolved/computed imports,
|
|
257
|
+
loader inputs or incomplete coverage. The graph is bounded to 2,000 modules,
|
|
258
|
+
12,000 imports, 32 MiB of source reads and 32 import hops. Hitting a limit leaves
|
|
259
|
+
the remaining coverage unknown. `ok` means no blocking diagnostic was found;
|
|
260
|
+
inspect warnings and coverage even when it is true.
|
|
261
|
+
|
|
239
262
|
## Compare running applications
|
|
240
263
|
|
|
241
264
|
Start the original application and the candidate on separate available ports.
|
|
@@ -258,6 +281,60 @@ in a real browser, test authorized and unauthorized states, and run the existing
|
|
|
258
281
|
application tests. Keep the original setup available until the candidate is
|
|
259
282
|
actually verified. Timings from this comparison are not controlled benchmarks.
|
|
260
283
|
|
|
284
|
+
## Run application browser scenarios
|
|
285
|
+
|
|
286
|
+
An explicit browser option can run the application's own assertions against
|
|
287
|
+
each server. Install Playwright and its Chromium browser in the application
|
|
288
|
+
using its normal development setup; PRNext never installs them for this check.
|
|
289
|
+
Provide a trusted `.mjs`, `.cjs` or `.js` module inside the application directory:
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
// migration.scenarios.mjs
|
|
293
|
+
export default [
|
|
294
|
+
{
|
|
295
|
+
async run({page, baseURL}) {
|
|
296
|
+
await page.goto(`${baseURL}/sign-in`);
|
|
297
|
+
await page.getByLabel('Email').fill(process.env.MIGRATION_TEST_EMAIL);
|
|
298
|
+
await page.getByLabel('Password').fill(process.env.MIGRATION_TEST_PASSWORD);
|
|
299
|
+
await page.getByRole('button', {name: 'Sign in', exact: true}).click();
|
|
300
|
+
await page.getByRole('heading', {name: 'Dashboard', exact: true}).waitFor();
|
|
301
|
+
await page.getByRole('button', {name: 'Save note', exact: true}).click();
|
|
302
|
+
await page.getByText('Note saved', {exact: true}).waitFor();
|
|
303
|
+
await page.getByRole('button', {name: 'Sign out', exact: true}).click();
|
|
304
|
+
await page.getByRole('heading', {name: 'Sign in', exact: true}).waitFor();
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Replace the URLs and accessible names with the application's real controls.
|
|
311
|
+
Supply dedicated test account values through the command's environment rather than
|
|
312
|
+
command arguments or source files. Use isolated test databases: these explicit
|
|
313
|
+
callbacks execute application code and can submit forms or mutate data.
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
prnext migrate --check --against http://localhost:3000 --candidate http://localhost:3001 --routes paths.json --browser-scenarios migration.scenarios.mjs --timeout 15000 --json
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Only this option loads the scenario module and starts a browser. It requires
|
|
320
|
+
the application's installed `playwright` or `@playwright/test`. Each callback
|
|
321
|
+
receives a new isolated browser context for each target. Assert observable
|
|
322
|
+
behavior inside the callback; its return value is ignored. A callback that
|
|
323
|
+
fulfills passes for that target, and a thrown assertion fails. Both targets
|
|
324
|
+
must pass independently; equal durations or response samples are not used to
|
|
325
|
+
infer browser equivalence.
|
|
326
|
+
|
|
327
|
+
The report records scenario numbers, targets and pass/fail results. It excludes
|
|
328
|
+
application console output, raw error messages, request data, cookies and
|
|
329
|
+
screenshots. Read or debug the original application test separately when a
|
|
330
|
+
generic failure needs investigation. Keep credentials out of assertion labels
|
|
331
|
+
and application-managed artifacts too. An unavailable browser or incomplete run
|
|
332
|
+
is reported as unknown. There are at most 20 scenarios, a 100–60,000 ms budget
|
|
333
|
+
per callback, a bounded startup period and a five-minute maximum for the worker.
|
|
334
|
+
A timeout closes its context and ends the run before another target starts.
|
|
335
|
+
The worker also closes its owned browser when it finishes. This is explicit
|
|
336
|
+
execution of trusted test code, not a sandbox for an untrusted module.
|
|
337
|
+
|
|
261
338
|
## Boundaries that often matter
|
|
262
339
|
|
|
263
340
|
React server/client directives in `.mts`/`.cts` are rejected; use `.ts`/`.tsx`.
|
package/docs/ai/routing.md
CHANGED
|
@@ -88,6 +88,9 @@ Client Components can participate in server rendering. Read `window`,
|
|
|
88
88
|
`useEffect`, not unconditionally during render. Server and first browser render
|
|
89
89
|
must agree. Use `.ts` or `.tsx` for `'use client'` and `'use server'` boundaries;
|
|
90
90
|
PRNext rejects those directives in `.mts`/`.cts` instead of silently misbundling.
|
|
91
|
+
The migration audit reports these unsupported boundaries too. It also checks
|
|
92
|
+
`server-only` and `client-only` markers through imported helpers, respecting
|
|
93
|
+
module-level Server Action boundaries and Node Route Handlers.
|
|
91
94
|
|
|
92
95
|
## Navigation and typed routes
|
|
93
96
|
|
|
@@ -110,6 +113,14 @@ without connection information retain bounded prefetching. `prefetch={false}`
|
|
|
110
113
|
disables Link speculation, including hover and focus. None of these policies
|
|
111
114
|
blocks a real click or changes authorization and private-data checks. App Link
|
|
112
115
|
automatic prefetching retains its existing partial-rendering requirement.
|
|
116
|
+
Set `prefetch={true}` to warm the full destination explicitly in an ordinary
|
|
117
|
+
App Router application without `cacheComponents`. The same network limits
|
|
118
|
+
apply, and development does not prefetch. Full prefetch warms modules and data;
|
|
119
|
+
the click still sends a fresh server request before showing the destination.
|
|
120
|
+
This includes prerendered pages whose access is controlled by a proxy: a
|
|
121
|
+
prefetched response cannot authorize a later navigation after an HttpOnly
|
|
122
|
+
cookie changes. Leave the prop unset to keep automatic behavior, or use `false`
|
|
123
|
+
to disable speculation.
|
|
113
124
|
|
|
114
125
|
### Connection loss during App navigation
|
|
115
126
|
|
|
@@ -2,6 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
[AI documentation index](../../AI.md)
|
|
4
4
|
|
|
5
|
+
## Compile large client imports only when used in development
|
|
6
|
+
|
|
7
|
+
For App Router applications, the explicit experimental option below keeps
|
|
8
|
+
literal client `import()` targets outside the initial browser graph. It defers
|
|
9
|
+
their transformation and compilation, rather than only downloading an already
|
|
10
|
+
compiled chunk later.
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
// prnext.config.mjs
|
|
14
|
+
export default {
|
|
15
|
+
experimental: { lazyDynamicImports: true }
|
|
16
|
+
};
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The option affects `prnext dev` only. Production keeps its ordinary dynamic
|
|
20
|
+
chunks and does not use a development compilation endpoint. SSR also keeps its
|
|
21
|
+
normal behavior. A `next/dynamic` component with `{ ssr: false }` is a useful
|
|
22
|
+
candidate for an editor or other large browser-only feature; an import needed
|
|
23
|
+
by SSR still needs its server compilation.
|
|
24
|
+
|
|
25
|
+
On first use, the development server compiles the requested subgraph, publishes
|
|
26
|
+
its immutable assets, and loads its CSS before resolving the import. Already
|
|
27
|
+
loaded ESM dependencies are reused through their original module URLs, keeping
|
|
28
|
+
shared objects and live bindings. Once activated, the module participates in
|
|
29
|
+
subsequent JavaScript and stylesheet updates. A failed compilation can be
|
|
30
|
+
requested again after correcting its source.
|
|
31
|
+
|
|
32
|
+
Activated styles remain loaded for the generation, including when parallel
|
|
33
|
+
slots select their own styles. Prefer CSS Modules or selectors scoped to the
|
|
34
|
+
deferred feature when its global styles should not affect other components.
|
|
35
|
+
|
|
36
|
+
The initial scope is App Router applications using the native compiler, with
|
|
37
|
+
same-origin assets and no webpack hook or custom loaders. Literal imports from
|
|
38
|
+
project `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts` and `.tsx` sources are eligible;
|
|
39
|
+
computed imports, import attributes, literal requests containing `?` or `#`,
|
|
40
|
+
imports inside installed packages and typed `.mts`/`.cts` helpers keep the
|
|
41
|
+
ordinary compiler behavior. The compiler keeps the distinct module identities
|
|
42
|
+
of those literal query and fragment imports. If an alias or a transitive
|
|
43
|
+
dependency introduces a resolved query or fragment suffix inside a deferred
|
|
44
|
+
graph, activation returns an explicit error
|
|
45
|
+
before reusing any shared export bridge. Disable the option for that graph;
|
|
46
|
+
query and fragment identities in deferred graphs are not currently supported.
|
|
47
|
+
The compiler does not silently duplicate a shared CommonJS dependency: that
|
|
48
|
+
case returns an explicit error. Convert that shared dependency to ESM or disable
|
|
49
|
+
the option.
|
|
50
|
+
Server Actions first introduced only by a deferred graph also require a static
|
|
51
|
+
import or the ordinary compiler, because the running server implementation
|
|
52
|
+
cannot be extended by publishing browser assets alone.
|
|
53
|
+
|
|
54
|
+
There are at most 128 registered imports and 4,096 static JavaScript dependency
|
|
55
|
+
identities per generation. Deferred assets have a cumulative 128 MiB budget per
|
|
56
|
+
active generation. Existing development request and generation limits still
|
|
57
|
+
apply. Public requests contain opaque registered IDs, never source filenames;
|
|
58
|
+
an unknown ID cannot initiate a deferred compilation. Static ESM export bridges
|
|
59
|
+
have their own startup cost, so measure your application's initial build and
|
|
60
|
+
first-use delay before keeping the option enabled.
|
|
61
|
+
|
|
62
|
+
Deferred JSON imports share their native ESM namespace and default object across
|
|
63
|
+
importers. Deferred assets are written outside the served directory and published
|
|
64
|
+
as complete immutable files. This experimental path requires hardlinks within
|
|
65
|
+
the development generation's filesystem; disable `lazyDynamicImports` if that
|
|
66
|
+
filesystem cannot create them. A failed activation keeps existing assets intact
|
|
67
|
+
and conservatively accounts for any publication whose outcome is uncertain.
|
|
68
|
+
|
|
5
69
|
## Collect a useful failure
|
|
6
70
|
|
|
7
71
|
Read the installed version, command, working directory, Node version, manager,
|
|
@@ -54,6 +118,21 @@ repair. Verify the interactive result after the change.
|
|
|
54
118
|
|
|
55
119
|
## HMR and cache failures
|
|
56
120
|
|
|
121
|
+
When an Action overlaps a refresh or navigation, its mutation result remains
|
|
122
|
+
independent from whether its older render is applied. A newer interaction can
|
|
123
|
+
release a superseded development commit wait; it never replays the mutation.
|
|
124
|
+
|
|
125
|
+
For a development state publication failure, keep the private
|
|
126
|
+
`.prnext-dev/<session>/diagnostics/state-publication.json` receipt. It records the
|
|
127
|
+
operation phase, error code, bounded rename attempts and last published sequence,
|
|
128
|
+
without source contents or a full source path. Windows transient rename failures
|
|
129
|
+
are retried for up to two seconds; a permanent failure preserves the previous
|
|
130
|
+
complete state file. After a replacement runtime has activated, publication
|
|
131
|
+
failure still retires obsolete runtimes through the gateway and keeps the
|
|
132
|
+
existing three-generation snapshot bound. In-flight requests drain before
|
|
133
|
+
their old runtime closes. Record the actual code and phase instead of assuming
|
|
134
|
+
an antivirus or file watcher caused the lock.
|
|
135
|
+
|
|
57
136
|
Development compiles requested routes and preserves state for compatible edits;
|
|
58
137
|
config/environment, custom Document or incompatible module changes can reload
|
|
59
138
|
the document. Production prerendering and compression are not development HMR.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomas.f/prnext",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19-alpha",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Experimental Rust-powered runtime for Next.js apps, React and npm",
|
|
6
6
|
"homepage": "https://prnext.dev",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"runtime/",
|
|
42
42
|
"compat/",
|
|
43
43
|
"native/",
|
|
44
|
-
"release/"
|
|
44
|
+
"release/",
|
|
45
|
+
"vendor/"
|
|
45
46
|
],
|
|
46
47
|
"scripts": {
|
|
47
48
|
"prepublishOnly": "node release/verify.mjs"
|
|
@@ -133,18 +134,28 @@
|
|
|
133
134
|
"@babel/types": "^7.29.0",
|
|
134
135
|
"@edge-runtime/vm": "5.0.0",
|
|
135
136
|
"@jridgewell/trace-mapping": "0.3.31",
|
|
136
|
-
"@
|
|
137
|
+
"@mapbox/node-pre-gyp": "2.0.3",
|
|
138
|
+
"@rollup/pluginutils": "5.4.0",
|
|
139
|
+
"acorn": "8.18.0",
|
|
140
|
+
"acorn-import-attributes": "1.9.5",
|
|
141
|
+
"async-sema": "3.1.1",
|
|
137
142
|
"babel-plugin-react-compiler": "1.0.0",
|
|
143
|
+
"bindings": "1.5.0",
|
|
138
144
|
"browserslist": "4.29.0",
|
|
139
145
|
"dotenv": "16.6.1",
|
|
140
146
|
"dotenv-expand": "11.0.7",
|
|
141
147
|
"esbuild": "^0.28.2",
|
|
148
|
+
"estree-walker": "2.0.2",
|
|
142
149
|
"fontkit": "2.0.4",
|
|
143
150
|
"get-tsconfig": "4.14.3",
|
|
151
|
+
"glob": "14.0.2",
|
|
152
|
+
"graceful-fs": "4.2.11",
|
|
144
153
|
"html-to-image": "1.11.13",
|
|
145
154
|
"jsonc-parser": "3.3.1",
|
|
146
155
|
"lightningcss": "1.31.0",
|
|
147
156
|
"loader-runner": "4.3.2",
|
|
157
|
+
"minimatch": "11.0.1",
|
|
158
|
+
"node-gyp-build": "4.8.4",
|
|
148
159
|
"oxc-parser": "0.155.0",
|
|
149
160
|
"path-to-regexp": "6.3.0",
|
|
150
161
|
"picomatch": "4.0.7",
|
|
@@ -154,6 +165,7 @@
|
|
|
154
165
|
"postcss-selector-parser": "7.1.6",
|
|
155
166
|
"postcss-value-parser": "4.2.0",
|
|
156
167
|
"react-refresh": "0.19.0",
|
|
168
|
+
"resolve-from": "5.0.0",
|
|
157
169
|
"schema-utils": "4.3.3",
|
|
158
170
|
"webpack": "5.111.1"
|
|
159
171
|
},
|
|
@@ -169,12 +181,12 @@
|
|
|
169
181
|
}
|
|
170
182
|
},
|
|
171
183
|
"optionalDependencies": {
|
|
172
|
-
"prnext-darwin-arm64": "0.1.
|
|
173
|
-
"prnext-darwin-x64": "0.1.
|
|
174
|
-
"prnext-linux-arm64-gnu": "0.1.
|
|
175
|
-
"prnext-linux-x64-gnu": "0.1.
|
|
176
|
-
"@thomas.f/prnext-win32-x64-msvc": "0.1.
|
|
177
|
-
"@thomas.f/prnext-win32-arm64-msvc": "0.1.
|
|
184
|
+
"prnext-darwin-arm64": "0.1.19-alpha",
|
|
185
|
+
"prnext-darwin-x64": "0.1.19-alpha",
|
|
186
|
+
"prnext-linux-arm64-gnu": "0.1.19-alpha",
|
|
187
|
+
"prnext-linux-x64-gnu": "0.1.19-alpha",
|
|
188
|
+
"@thomas.f/prnext-win32-x64-msvc": "0.1.19-alpha",
|
|
189
|
+
"@thomas.f/prnext-win32-arm64-msvc": "0.1.19-alpha"
|
|
178
190
|
},
|
|
179
191
|
"engines": {
|
|
180
192
|
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
|