@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.
Files changed (90) hide show
  1. package/README.md +2 -0
  2. package/build/actions.mjs +26 -14
  3. package/build/app.mjs +46 -25
  4. package/build/compiler.mjs +5 -4
  5. package/build/config.mjs +4 -2
  6. package/build/dev-css-assets.mjs +173 -0
  7. package/build/dev-css-update.mjs +14 -12
  8. package/build/dev-lazy-build.mjs +77 -0
  9. package/build/dynamic.mjs +13 -6
  10. package/build/incremental.mjs +112 -18
  11. package/build/index.mjs +32 -3
  12. package/build/lazy-dynamic-imports.mjs +137 -0
  13. package/build/migrate.mjs +4 -4
  14. package/build/migration-audit.mjs +19 -31
  15. package/build/migration-imports.mjs +245 -0
  16. package/build/migration-scenarios-runner.mjs +73 -0
  17. package/build/migration-scenarios.mjs +72 -0
  18. package/build/native-discovery.mjs +65 -5
  19. package/build/native-packages.mjs +2 -0
  20. package/build/npm-packages.mjs +1 -0
  21. package/build/production-builder.mjs +1 -1
  22. package/build/standalone.mjs +21 -3
  23. package/build/webpack-graph.mjs +13 -3
  24. package/cli.mjs +10 -1
  25. package/compat/app-dynamic.cjs +12 -1
  26. package/compat/cache-handlers.cjs +3 -1
  27. package/compat/flight-delta.cjs +3 -1
  28. package/compat/index.d.ts +1 -1
  29. package/compat/link.cjs +2 -2
  30. package/docs/ai/compatibility.md +77 -0
  31. package/docs/ai/routing.md +11 -0
  32. package/docs/ai/troubleshooting.md +79 -0
  33. package/package.json +21 -9
  34. package/runtime/app-client.mjs +119 -42
  35. package/runtime/atomic-file.mjs +27 -0
  36. package/runtime/cache-components-rsc.mjs +3 -1
  37. package/runtime/dev-client.mjs +7 -2
  38. package/runtime/dev-file-changes.mjs +123 -8
  39. package/runtime/dev-lazy-imports.mjs +121 -0
  40. package/runtime/dev-lazy-staging.mjs +14 -0
  41. package/runtime/dev-style-retention.mjs +15 -0
  42. package/runtime/development.mjs +60 -19
  43. package/runtime/flight-delta.mjs +28 -4
  44. package/runtime/fs.mjs +15 -8
  45. package/runtime/lazy-import-client.mjs +38 -0
  46. package/vendor/nft/LICENSE +7 -0
  47. package/vendor/nft/README.md +16 -0
  48. package/vendor/nft/out/analyze.d.ts +8 -0
  49. package/vendor/nft/out/analyze.js +1216 -0
  50. package/vendor/nft/out/analyze.js.map +1 -0
  51. package/vendor/nft/out/cli.d.ts +2 -0
  52. package/vendor/nft/out/cli.js +94 -0
  53. package/vendor/nft/out/cli.js.map +1 -0
  54. package/vendor/nft/out/fs.d.ts +22 -0
  55. package/vendor/nft/out/fs.js +106 -0
  56. package/vendor/nft/out/fs.js.map +1 -0
  57. package/vendor/nft/out/index.d.ts +4 -0
  58. package/vendor/nft/out/index.js +25 -0
  59. package/vendor/nft/out/index.js.map +1 -0
  60. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  61. package/vendor/nft/out/node-file-trace.js +380 -0
  62. package/vendor/nft/out/node-file-trace.js.map +1 -0
  63. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  64. package/vendor/nft/out/resolve-dependency.js +427 -0
  65. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  66. package/vendor/nft/out/types.d.ts +66 -0
  67. package/vendor/nft/out/types.js +2 -0
  68. package/vendor/nft/out/types.js.map +1 -0
  69. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  70. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  71. package/vendor/nft/out/utils/binary-locators.js +88 -0
  72. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  73. package/vendor/nft/out/utils/get-package-base.js +30 -0
  74. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  75. package/vendor/nft/out/utils/interop-require.js +24 -0
  76. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  77. package/vendor/nft/out/utils/node-version.js +10 -0
  78. package/vendor/nft/out/utils/node-version.js.map +1 -0
  79. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  80. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  81. package/vendor/nft/out/utils/special-cases.js +347 -0
  82. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  83. package/vendor/nft/out/utils/static-eval.js +579 -0
  84. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  85. package/vendor/nft/out/utils/types.js +2 -0
  86. package/vendor/nft/out/utils/types.js.map +1 -0
  87. package/vendor/nft/out/utils/wrappers.js +659 -0
  88. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  89. package/vendor/nft/package.json +4 -0
  90. package/vendor/nft/upstream.json +51 -0
@@ -1,5 +1,6 @@
1
- import { nodeFileTrace } from '@vercel/nft';
2
- import resolver from '@vercel/nft/out/resolve-dependency.js';
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; }
@@ -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===state.root){sessions.delete(key);await session.dispose();}
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===state.root&&(previous.identity!==identity||previous.base===base&&old!==key)){sessions.delete(old);await previous.dispose();}
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:state.root,base,identity,builds:0});
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 note of [...report.notes, ...(report.comparison ? [report.comparison.scope] : [])]) console.log(note);
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;
@@ -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
- const Lazy = React.lazy(() => Promise.resolve().then(opts.loader).then(moduleOf));
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
- return Buffer.concat(chunks, size);
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();
@@ -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
- let index=base instanceof Uint8Array&&base.length<=MAX_VALUE?createIndex(base):null;
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?.router.partialPrefetch) app.router.prefetch(target, { priority, full: prefetch === true });
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' });
@@ -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`.
@@ -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.18-alpha",
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
- "@vercel/nft": "1.11.0",
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.18-alpha",
173
- "prnext-darwin-x64": "0.1.18-alpha",
174
- "prnext-linux-arm64-gnu": "0.1.18-alpha",
175
- "prnext-linux-x64-gnu": "0.1.18-alpha",
176
- "@thomas.f/prnext-win32-x64-msvc": "0.1.18-alpha",
177
- "@thomas.f/prnext-win32-arm64-msvc": "0.1.18-alpha"
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"