@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.
Files changed (137) hide show
  1. package/README.md +26 -869
  2. package/build/actions.mjs +1 -1
  3. package/build/analyze.mjs +180 -24
  4. package/build/app-config.mjs +13 -0
  5. package/build/app-static.mjs +34 -12
  6. package/build/app-styles.mjs +79 -0
  7. package/build/app.mjs +81 -26
  8. package/build/bundle-runtime.mjs +22 -9
  9. package/build/cache-components.mjs +1 -1
  10. package/build/cache-handlers.mjs +1 -1
  11. package/build/compact-css.mjs +53 -0
  12. package/build/compilation-cache.mjs +269 -0
  13. package/build/compilation-store.mjs +142 -0
  14. package/build/compiler.mjs +31 -8
  15. package/build/config.mjs +30 -1
  16. package/build/create.mjs +239 -0
  17. package/build/css-modules.mjs +56 -12
  18. package/build/deployment-assets.mjs +89 -0
  19. package/build/deployment-config.mjs +18 -0
  20. package/build/dev-css-update.mjs +4 -2
  21. package/build/dev-dependencies.mjs +1 -1
  22. package/build/dev-discovery.mjs +3 -3
  23. package/build/dev.mjs +15 -8
  24. package/build/dynamic.mjs +1 -1
  25. package/build/edge.mjs +3 -3
  26. package/build/fonts.mjs +1 -1
  27. package/build/framework-imports.mjs +15 -5
  28. package/build/html-only-config.mjs +53 -0
  29. package/build/html-only-graph.mjs +67 -0
  30. package/build/index.mjs +59 -26
  31. package/build/instrumentation.mjs +30 -0
  32. package/build/metadata-routes.mjs +1 -1
  33. package/build/migration-audit.mjs +1 -1
  34. package/build/native-type-config.mjs +37 -0
  35. package/build/persistent-cache.mjs +18 -3
  36. package/build/precompress.mjs +1 -1
  37. package/build/prerender-cache.mjs +70 -0
  38. package/build/prerender-config.mjs +29 -0
  39. package/build/prerender-pages.mjs +19 -9
  40. package/build/prerender-pool.mjs +165 -0
  41. package/build/prerender-process-worker.mjs +58 -0
  42. package/build/prerender-worker.mjs +5 -5
  43. package/build/production-builder.mjs +58 -9
  44. package/build/react-compiler-report.mjs +225 -0
  45. package/build/react-compiler.mjs +139 -0
  46. package/build/runtime-contract.mjs +2 -2
  47. package/build/static-export.mjs +4 -4
  48. package/build/styles.mjs +6 -4
  49. package/build/transform.mjs +1 -1
  50. package/build/typed-routes.mjs +4 -0
  51. package/build/typescript-dev.mjs +35 -8
  52. package/build/typescript-native-worker.mjs +55 -0
  53. package/build/typescript-native.mjs +18 -0
  54. package/build/typescript-toolchain.mjs +19 -0
  55. package/build/typescript.mjs +6 -4
  56. package/build/webpack-graph.mjs +6 -3
  57. package/build/worker-memory.mjs +9 -0
  58. package/cli.mjs +60 -4
  59. package/compat/activity-cache.cjs +26 -0
  60. package/compat/after.cjs +6 -4
  61. package/compat/app-layout-context.cjs +52 -2
  62. package/compat/base64.cjs +10 -0
  63. package/compat/cache-handlers.cjs +12 -2
  64. package/compat/cache-wire.cjs +28 -0
  65. package/compat/cache.cjs +2 -1
  66. package/compat/cache.d.cts +4 -0
  67. package/compat/cookies.cjs +21 -5
  68. package/compat/data-cache.cjs +12 -8
  69. package/compat/flight-delta.cjs +150 -0
  70. package/compat/headers.cjs +4 -2
  71. package/compat/incremental-cache.cjs +20 -7
  72. package/compat/index.d.ts +68 -3
  73. package/compat/link.cjs +12 -9
  74. package/compat/navigation-authorization.cjs +9 -0
  75. package/compat/navigation-stages.cjs +19 -0
  76. package/compat/offline-store.cjs +11 -0
  77. package/compat/offline.cjs +5 -0
  78. package/compat/offline.d.cts +2 -0
  79. package/compat/redis-cache.cjs +201 -0
  80. package/compat/redis-cache.d.cts +37 -0
  81. package/compat/server-primitives.cjs +31 -11
  82. package/compat/server.d.cts +15 -2
  83. package/compat/static-generation.cjs +7 -0
  84. package/compat/use-cache.cjs +3 -0
  85. package/docs/ai/compatibility.md +171 -2
  86. package/docs/ai/data.md +165 -5
  87. package/docs/ai/deployment.md +158 -0
  88. package/docs/ai/quickstart.md +29 -4
  89. package/docs/ai/routing.md +82 -0
  90. package/docs/ai/troubleshooting.md +37 -2
  91. package/package.json +26 -7
  92. package/runtime/action-client.mjs +10 -4
  93. package/runtime/action-server.mjs +2 -0
  94. package/runtime/app-client.mjs +212 -65
  95. package/runtime/app-content.mjs +4 -3
  96. package/runtime/app-partial-client.mjs +3 -0
  97. package/runtime/app-partial.mjs +1 -1
  98. package/runtime/app-prefetch-constants.mjs +4 -0
  99. package/runtime/app-prefetch.mjs +2 -3
  100. package/runtime/app-render-key.mjs +5 -0
  101. package/runtime/app-render.mjs +88 -28
  102. package/runtime/app-routing.mjs +22 -9
  103. package/runtime/app-static.mjs +18 -8
  104. package/runtime/app-stream-html.mjs +10 -53
  105. package/runtime/app-style-selection.mjs +34 -0
  106. package/runtime/app-styles.mjs +154 -0
  107. package/runtime/cache-components-rsc.mjs +4 -1
  108. package/runtime/cache-request.mjs +2 -1
  109. package/runtime/deployment-client.mjs +19 -0
  110. package/runtime/dev-client.mjs +7 -2
  111. package/runtime/dev-compiler-broker.mjs +42 -0
  112. package/runtime/dev-indicator.mjs +8 -3
  113. package/runtime/dev-lazy-tool.mjs +16 -0
  114. package/runtime/development.mjs +29 -12
  115. package/runtime/document.mjs +1 -1
  116. package/runtime/edge.mjs +6 -5
  117. package/runtime/fetch-cache.mjs +10 -8
  118. package/runtime/flight-delta.mjs +50 -0
  119. package/runtime/flight-tap.mjs +28 -0
  120. package/runtime/html-only.mjs +27 -0
  121. package/runtime/http.mjs +2 -0
  122. package/runtime/incremental-cache.mjs +25 -4
  123. package/runtime/instrumentation.mjs +49 -0
  124. package/runtime/navigation-recovery.mjs +36 -0
  125. package/runtime/navigation.mjs +7 -0
  126. package/runtime/node-html-stream.mjs +47 -0
  127. package/runtime/pages-client.mjs +14 -7
  128. package/runtime/pages-navigation.mjs +48 -63
  129. package/runtime/prefetch-scheduler.mjs +110 -0
  130. package/runtime/profile.mjs +14 -0
  131. package/runtime/profiles.json +2 -2
  132. package/runtime/render.mjs +3 -0
  133. package/runtime/route-pattern.mjs +44 -0
  134. package/runtime/rsc-worker.mjs +34 -12
  135. package/runtime/trace.mjs +78 -0
  136. package/runtime/watch-project.mjs +19 -15
  137. package/runtime/worker.mjs +70 -26
package/build/app.mjs CHANGED
@@ -17,13 +17,17 @@ import { createNativePackages } from './native-packages.mjs';
17
17
  import { createRefreshTransform, createSourceLocationTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
18
18
  import { npmPackagesPlugin } from './npm-packages.mjs';
19
19
  import { transformCacheComponents } from './cache-components.mjs';
20
+ import {createReactCompilerTransform} from './react-compiler.mjs';
21
+ import {createAppStyleGraph, appStyleScopes, planAppStyleEntries} from './app-styles.mjs';
22
+ import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
23
+ import {createHtmlOnlyGraph} from './html-only-graph.mjs';
20
24
 
21
25
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
- const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx' };
26
+ const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx', '.mts': 'ts', '.cts': 'ts' };
23
27
  const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
24
28
  const builtins = new Set([...builtinModules, ...builtinModules.map(name => `node:${name}`)]);
25
- const compatibility = new Set(['link', 'image', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'navigation', 'headers', 'server', 'cache', 'dynamic', 'script', 'og']);
26
- const forcedClient = new Set(['link.cjs', 'image.cjs', 'head.cjs', 'router.cjs', 'compat-router.cjs', 'next-router-context.cjs', 'next-app-router-context.cjs', 'app-context.cjs', 'script.cjs']);
29
+ const compatibility = new Set(['link', 'image', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'navigation', 'headers', 'server', 'cache', 'dynamic', 'script', 'og', 'offline', 'redis-cache']);
30
+ const forcedClient = new Set(['link.cjs', 'image.cjs', 'head.cjs', 'router.cjs', 'compat-router.cjs', 'next-router-context.cjs', 'next-app-router-context.cjs', 'app-context.cjs', 'script.cjs', 'offline.cjs']);
27
31
 
28
32
  export async function validateReactVersions(resolveFromProject) {
29
33
  const names = ['react', 'react-dom', 'react-server-dom-webpack'];
@@ -39,7 +43,7 @@ export async function validateReactVersions(resolveFromProject) {
39
43
  }
40
44
 
41
45
  function sourceAst(source, filename) {
42
- return parse(source, { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] });
46
+ return parse(source, { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] });
43
47
  }
44
48
  function clientDirective(source, filename) {
45
49
  if (!source.includes('use client')) return false;
@@ -140,7 +144,7 @@ function routeEntry(route, globalError, dev) {
140
144
  if (globalError) imports.push(`export {default as GlobalError} from ${JSON.stringify(globalError)};`);
141
145
  imports.push(`export {default as NavigationBoundary} from ${JSON.stringify(path.join(packageRoot, 'compat/app-navigation-boundary.cjs'))};`);
142
146
  imports.push(`export {ClientPageRoot} from ${JSON.stringify(path.join(packageRoot, 'compat/app-client-page.cjs'))};`);
143
- imports.push(`export {LayoutProvider,LayoutSlot} from ${JSON.stringify(path.join(packageRoot, 'compat/app-layout-context.cjs'))};`);
147
+ imports.push(`export {LayoutProvider,LayoutSlot,ActivitySlot} from ${JSON.stringify(path.join(packageRoot, 'compat/app-layout-context.cjs'))};`);
144
148
  if (route.routing || route.segments.some(segment => segment.error)) imports.push(`export {default as ErrorBoundary} from ${JSON.stringify(path.join(packageRoot, 'compat/app-error-boundary.cjs'))};`);
145
149
  if (route.routing) {
146
150
  let index = 0;
@@ -173,12 +177,15 @@ function routeEntry(route, globalError, dev) {
173
177
  }
174
178
 
175
179
  /** Compile independent React Server and client graphs. The RSC graph contains only references to client boundaries. */
176
- export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {} }) {
180
+ export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, reactCompiler = false, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {} }) {
177
181
  const refresh = dev ? createRefreshTransform(project.root, {generation:manifest.cacheId,forceGeneration:manifest.devForceRefresh,forceFiles:manifest.devAffectedModules}) : undefined;
178
182
  const locations = dev ? createSourceLocationTransform(project.root) : undefined;
183
+ const compileReact = reactCompiler ? createReactCompilerTransform(project.root, reactCompiler, {dev}) : undefined;
179
184
  const { basePath = '', assetPrefix = '', assetBase = '/_prnext/assets', trailingSlash = false, skipTrailingSlashRedirect = false } = manifest.config || {};
180
185
  const publicPath = assetBase + '/';
181
186
  const routes = project.routes.filter(route => route.router === 'app');
187
+ const htmlOnly = dev ? new Set() : selectHtmlOnlyRoutes(project.routes, manifest.config.htmlOnly, manifest.config);
188
+ const htmlGraph = !dev && htmlOnly.size ? createHtmlOnlyGraph(project.root) : undefined;
182
189
  if (project.appNotFound) {
183
190
  const file = path.join(stage, '.entries', 'app-not-found.jsx');
184
191
  await writeFile(file, `import {notFound} from 'next/navigation';export default function MissingRoute(){notFound()}`);
@@ -194,6 +201,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
194
201
  }
195
202
  if (!routes.length) return;
196
203
  if (!manifest.config.cacheComponents && routes.some(route => route.cacheConfig?.instant !== undefined || route.pageConfig?.instant !== undefined || route.segments?.some(segment => segment.staticConfig?.instant !== undefined))) throw new Error('instant requires cacheComponents: true.');
204
+ if (!manifest.config.cacheComponents && routes.some(route => route.cacheConfig?.ensureStatic !== undefined || route.pageConfig?.ensureStatic !== undefined || route.segments?.some(segment => segment.staticConfig?.ensureStatic !== undefined))) throw new Error('ensureStatic requires cacheComponents: true.');
197
205
  const actions = createServerActions({ projectRoot: project.root });
198
206
  function actionTransform(source, file, mode, runtime = 'nodejs') {
199
207
  const result = actions.transform(source, file, mode);
@@ -204,6 +212,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
204
212
  }
205
213
  const boundaries = new Map();
206
214
  const cssFiles = new Set();
215
+ const styleGraph = createAppStyleGraph(project.root);
207
216
  const globalErrorCssFiles = new Set();
208
217
  const globalErrorOnlyCss = new Set();
209
218
  const canonicalPaths = new Map();
@@ -229,30 +238,33 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
229
238
  throw new Error('app/global-error must be a Client Component. Add the "use client" directive.');
230
239
  }
231
240
 
232
- function graphPlugin(mode, { styleTarget = cssFiles, omitGlobalStyles = true } = {}) {
241
+ function graphPlugin(mode, { styleTarget = cssFiles, omitGlobalStyles = true, omitAllStyles = false } = {}) {
233
242
  const browser = mode === 'browser';
234
243
  const rsc = mode === 'rsc';
235
244
  return {
236
245
  name: `prnext-app-${mode}`,
237
246
  setup(esbuild) {
238
- applicationEntry(esbuild,readCompilerFile);
247
+ applicationEntry(esbuild,readCompilerFile,{transformTypedHelpers:!!compileReact && (browser || mode==='ssr')});
239
248
  esbuild.onResolve({ filter: /^prnext-internal:use-cache$/ }, () => browser
240
249
  ? { errors: [{ text: "'use cache' cannot enter a Client Component graph" }] }
241
250
  : { path: '../compat/use-cache.cjs', external: true });
242
251
  if (browser && omitGlobalStyles) esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, async args => {
243
- if (!globalErrorOnlyCss.size || /\.module\.(?:css|scss|sass)$/i.test(args.path) || args.pluginData?.prnextResolvingCss || args.pluginData?.prnextAppCss || args.kind === 'import-rule' || args.path.startsWith('prnext-css:')) return;
252
+ if ((!omitAllStyles && !globalErrorOnlyCss.size) || /\.module\.(?:css|scss|sass)$/i.test(args.path) || args.pluginData?.prnextResolvingCss || args.pluginData?.prnextAppCss || args.kind === 'import-rule' || args.path.startsWith('prnext-css:')) return;
244
253
  const resolved = await esbuild.resolve(args.path, { resolveDir: args.resolveDir, kind: args.kind, pluginData: { prnextAppCss: true } });
245
- if (!resolved.errors.length && !resolved.external && resolved.namespace === 'file' && globalErrorOnlyCss.has(await canonicalFile(resolved.path))) return { path: resolved.path, namespace: 'prnext-app-empty' };
254
+ if (!resolved.errors.length && !resolved.external && resolved.namespace === 'file' && (omitAllStyles || globalErrorOnlyCss.has(await canonicalFile(resolved.path)))) return { path: resolved.path, namespace: 'prnext-app-empty' };
246
255
  });
247
256
  esbuild.onResolve({ filter: /^prnext-internal:action-(?:crypto|client|ssr)$/ }, args => {
248
257
  const helper = args.path.slice('prnext-internal:'.length);
249
258
  if (browser && helper !== 'action-client') return { errors: [{ text: 'Server Action implementation cannot enter the browser graph.' }] };
250
259
  return browser ? { path: path.join(packageRoot, 'runtime', helper + '.mjs') } : { path: '../runtime/' + helper + '.mjs', external: true };
251
260
  });
261
+ esbuild.onResolve({ filter: /^prnext-internal:metadata-route$/ }, () => browser
262
+ ? { errors: [{ text: 'Metadata route handlers are server-only.' }] }
263
+ : { path: '../runtime/metadata-route.mjs', external: true });
252
264
  esbuild.onResolve({ filter: frameworkRuntimeImportPattern }, args => {
253
265
  const name = frameworkImportName(args.path);
254
266
  if (!compatibility.has(name)) return { errors: [{ text: `PRNext does not implement ${args.path} yet (imported by ${args.importer}).` }] };
255
- if (browser && ['headers', 'server', 'cache', 'og'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a Client Component.` }] };
267
+ if (browser && ['headers', 'server', 'cache', 'og', 'redis-cache'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a Client Component.` }] };
256
268
  const target = path.join(packageRoot, 'compat', (name === 'compat/router' ? 'compat-router' : name === 'dynamic' ? 'app-dynamic' : rsc && name === 'navigation' ? 'navigation-server' : name) + '.cjs');
257
269
  if (!browser && (!rsc || !forcedClient.has(path.basename(target)))) return { path: '../compat/' + path.basename(target), external: true };
258
270
  return { path: target };
@@ -296,10 +308,14 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
296
308
  if (!/\.module\.(?:css|scss|sass)$/i.test(args.path)) return { path: file, namespace: 'prnext-app-empty' };
297
309
  });
298
310
  }
299
- esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
311
+ esbuild.onLoad({ filter: /\.(?:[cm]?[jt]s|[jt]sx)$/, namespace: 'file' }, async args => {
300
312
  if (browser && args.path === project.document) return { errors: [{ text: 'pages/_document is server-only and cannot be imported by a Client Component.' }] };
301
313
  const original=await readCompilerFile(args.path,args.prnextSource);
302
- const source = await fonts.transform(await expandActionExports(locations?await locations(original,args.path):original, args.path, async (specifier, importer) => {
314
+ htmlGraph?.source(args.path, original);
315
+ // Only Client Component graphs use React's memo-cache runtime. Server
316
+ // Components, route handlers and Server Action graphs remain untouched.
317
+ const compiled=compileReact && (browser || mode==='ssr') ? await compileReact(original,args.path,{loaderOutput:args.prnextSource!==undefined}) : original;
318
+ const source = await fonts.transform(await expandActionExports(locations?await locations(compiled,args.path):compiled, args.path, async (specifier, importer) => {
303
319
  const resolved = await esbuild.resolve(specifier, { resolveDir: path.dirname(importer), kind: 'import-statement', pluginData: { prnextExportScan: true } });
304
320
  return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
305
321
  }), args.path);
@@ -308,6 +324,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
308
324
  const actionSource = actionTransform(cachedSource, args.path, mode);
309
325
  const builtin = path.dirname(args.path) === path.join(packageRoot, 'compat') && forcedClient.has(path.basename(args.path));
310
326
  if (rsc && (builtin || clientDirective(source, args.path))) {
327
+ htmlGraph?.issue(args.path, 'a Client Component or interactive framework component');
311
328
  let boundary = boundaries.get(args.path);
312
329
  if (!boundary) {
313
330
  const exports = await exportedNames(args.path, esbuild);
@@ -334,7 +351,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
334
351
  const rsc = mode === 'rsc';
335
352
  const framework = new Set(['link', 'image', 'head', 'router', 'compat-router', 'next-router-context', 'next-app-router-context', 'navigation', 'script', 'app-context', 'app-layout-context', 'app-navigation-boundary', 'app-client-page', 'app-error-boundary', 'app-dynamic']);
336
353
  return { name: 'prnext-edge-app', setup(esbuild) {
337
- applicationEntry(esbuild,readCompilerFile);
354
+ applicationEntry(esbuild,readCompilerFile,{transformTypedHelpers:!!compileReact && mode==='ssr'});
338
355
  esbuild.onResolve({filter:/^prnext-internal:action-(?:crypto|ssr)$/}, args => ({path:args.path.slice('prnext-internal:'.length),namespace:'prnext-edge-action'}));
339
356
  esbuild.onLoad({filter:/.*/,namespace:'prnext-edge-action'}, args => ({loader:'js',contents:(args.path==='action-crypto'?['encryptBoundArgs','decryptBoundArgs','bindEncryptedReference']:['createServerReference']).map(name=>`export const ${name}=globalThis.__PRNEXT_EDGE_ACTIONS[${JSON.stringify(args.path)}][${JSON.stringify(name)}];`).join('\n')}));
340
357
  esbuild.onResolve({ filter: frameworkSubpathPattern }, args => {
@@ -362,7 +379,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
362
379
  const names = await exportedNames(args.path, esbuild), name = path.basename(args.path, '.cjs');
363
380
  return { loader: 'js', contents: `const module=globalThis.__PRNEXT_EDGE_FRAMEWORK[${JSON.stringify(name)}];\n` + [...names].map((name, index) => `const value${index}=${name === 'default' ? '(module.default||module)' : `module[${JSON.stringify(name)}]`};export {value${index} as ${JSON.stringify(name)}};`).join('\n') };
364
381
  });
365
- if (rsc) esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
382
+ if (rsc) esbuild.onLoad({ filter: /\.(?:[cm]?[jt]s|[jt]sx)$/, namespace: 'file' }, async args => {
366
383
  const source = args.prnextSource ?? await readFile(args.path, 'utf8');
367
384
  const builtin = path.dirname(args.path) === path.join(packageRoot, 'compat') && forcedClient.has(path.basename(args.path));
368
385
  if (!builtin && !clientDirective(source, args.path)) return;
@@ -377,13 +394,18 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
377
394
  } };
378
395
  }
379
396
  async function edgeAppBuild(file, outfile, mode, defaultExport = false) {
380
- const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport,
397
+ const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport, transformTypedHelpers:!!compileReact && mode==='ssr',
381
398
  plugins: [moduleResolutionPlugin(moduleResolution, project.root), edgeGraphPlugin(mode), images.plugin(), fonts.plugin(), cssModules.plugin(false)],
382
- transformSource: async (source, file, builder) => actionTransform(await fonts.transform(await expandActionExports(locations?await locations(source,file):source, file, async (specifier, importer) => {
383
- const resolved = await builder.resolve(specifier, {resolveDir:path.dirname(importer),kind:'import-statement',pluginData:{prnextExportScan:true}});
384
- return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
385
- }), file), file, mode, 'edge') });
399
+ transformSource: async (source, file, builder, {loaderOutput=false}={}) => {
400
+ const compiled=compileReact && mode==='ssr' ? await compileReact(source,file,{loaderOutput}) : source;
401
+ return actionTransform(await fonts.transform(await expandActionExports(locations?await locations(compiled,file):compiled, file, async (specifier, importer) => {
402
+ const resolved = await builder.resolve(specifier, {resolveDir:path.dirname(importer),kind:'import-statement',pluginData:{prnextExportScan:true}});
403
+ return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
404
+ }), file), file, mode, 'edge');
405
+ } });
386
406
  fonts.record(result.metafile);
407
+ styleGraph.record(result.metafile);
408
+ htmlGraph?.record(result.metafile);
387
409
  }
388
410
  async function serverBuild(entryPoints, outdir, mode, styleTarget = cssFiles) {
389
411
  if (!Object.keys(entryPoints).length) return;
@@ -411,6 +433,8 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
411
433
  }
412
434
  }
413
435
  fonts.record(output.metafile);
436
+ styleGraph.record(output.metafile);
437
+ htmlGraph?.record(output.metafile);
414
438
  for (const file of output.outputFiles) {
415
439
  await mkdir(path.dirname(file.path), { recursive: true });
416
440
  await writeFile(file.path, file.contents);
@@ -425,7 +449,6 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
425
449
  }
426
450
 
427
451
  async function collectBrowserStyles(browserEntries) {
428
- if (!globalErrorId) return;
429
452
  const revision = `${boundaries.size}:${actions.actions.size}:${nativePackages.version}`;
430
453
  if (browserStyleRevision === revision) return;
431
454
  // SSR intentionally erases ssr:false loaders. Discover their dependencies
@@ -435,6 +458,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
435
458
  define: { 'process.env': '{}', ...common.define },
436
459
  plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), nativePackages.plugin('browser'), cssModules.plugin(true)] });
437
460
  fonts.record(discovery.metafile);
461
+ styleGraph.record(discovery.metafile);
438
462
  const entryIds = new Map(await Promise.all(Object.entries(browserEntries).map(async ([id, file]) => [await canonicalFile(file), id])));
439
463
  const globalRoots = [], normalRoots = [];
440
464
  for (const output of Object.values(discovery.metafile.outputs)) {
@@ -477,6 +501,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
477
501
  const collectInstant = node => {if(!node)return;instantConfigs.push(...Object.values(node.config || {}));for(const child of [...(node.children || []),...Object.values(node.slots || {})])collectInstant(child);};
478
502
  collectInstant(route.routing);
479
503
  manifest.routes.push({ id: route.id, pattern: route.pattern, kind: route.kind, router: 'app', module: `server/${route.id}.mjs`, css: [], ...(route.internal ? { internal: true } : {}),
504
+ ...(!dev && htmlOnly.has(route.id) ? { htmlOnly:true } : {}),
480
505
  ...(route.parallel ? { parallel: true, interception: !!route.interception } : {}),
481
506
  ...(route.cacheConfig ? { cacheConfig: route.cacheConfig } : {}),
482
507
  ...(route.kind === 'page' ? { instantSamples: [route.pageConfig, ...[...route.segments].reverse().map(segment => segment.staticConfig)].find(config => config?.instant?.unstable_samples)?.instant.unstable_samples, instantBuild: instantConfigs.some(config => config?.instant?.level === 'experimental-error'), hasStaticParams: route.id !== manifest.appNotFound && !!(route.pageConfig?.generateStaticParams || route.segments.some(segment => segment.staticConfig?.generateStaticParams)) }
@@ -536,25 +561,50 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
536
561
  }
537
562
  if (globalErrorId) manifest.app.globalError = { id: globalErrorId, name: 'default', css: globalErrorCss };
538
563
  const runtimeEntry = path.join(stage, '.entries', 'app-runtime.mjs');
564
+ const htmlOnlyRoutes = !dev && htmlOnly.size ? routes.filter(route => route.kind === 'page' && !route.internal).map(route => ({pattern:route.pattern,...(htmlOnly.has(route.id)?{htmlOnly:true}:{})})) : undefined;
539
565
  const importers = Object.entries(browserEntries).map(([id, file]) => `${JSON.stringify(id)}:()=>import(${JSON.stringify(file)})`);
540
- await writeFile(runtimeEntry, `${dev ? `import ${JSON.stringify(devClientFile)};\n` : ''}${[...cssFiles].map(file => `import ${JSON.stringify(file)};`).join('\n')}\nimport {bootstrapApp} from ${JSON.stringify(path.join(packageRoot, 'runtime/app-client.mjs'))};\nbootstrapApp({clientModules:{${importers.join(',')}},...${JSON.stringify({ basePath, assetPrefix, assetBase, trailingSlash, skipTrailingSlashRedirect, strictMode: manifest.config.reactStrictMode !== false, cacheComponents: manifest.config.cacheComponents === true, ...(globalErrorId ? { globalErrorId, globalErrorCss } : {}), ...(dev ? { dev: { buildId: manifest.buildId, clientManifest: manifest.devClient, onDemand:manifest.devOnDemand===true, sequence:manifest.devSequence, session:manifest.devSession, indicator:manifest.devIndicators, ...(manifest.devTools ? {tools:manifest.devTools} : {}) } } : {}) })}});\n`);
566
+ await writeFile(runtimeEntry, `${dev ? `import ${JSON.stringify(devClientFile)};\n` : ''}\nimport {bootstrapApp} from ${JSON.stringify(path.join(packageRoot, 'runtime/app-client.mjs'))};\nbootstrapApp({clientModules:{${importers.join(',')}},...${JSON.stringify({ basePath, assetPrefix, assetBase, htmlOnlyRoutes, trailingSlash, skipTrailingSlashRedirect, deploymentId: dev ? undefined : manifest.cacheId, strictMode: manifest.config.reactStrictMode !== false, cacheComponents: manifest.config.cacheComponents === true, partialPrefetching: manifest.config.partialPrefetching === true, navigationCache: manifest.config.navigationCache, ...(globalErrorId ? { globalErrorId, globalErrorCss } : {}), ...(dev ? { dev: { buildId: manifest.buildId, clientManifest: manifest.devClient, onDemand:manifest.devOnDemand===true, sequence:manifest.devSequence, session:manifest.devSession, indicator:manifest.devIndicators, ...(manifest.devTools ? {tools:manifest.devTools} : {}) } } : {}) })}});\n`);
541
567
  const clientEntries = { 'app-runtime': runtimeEntry, ...browserEntries };
542
568
  const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'app-chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
543
- define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser'), nativePackages.plugin('browser'), cssModules.plugin(true, { emitStyles: file => !globalErrorOnlyCss.has(file) })] });
569
+ define: { 'process.env': '{}', ...common.define, __PRNEXT_CACHE_COMPONENTS__: JSON.stringify(manifest.config.cacheComponents === true) }, plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitAllStyles: true }), nativePackages.plugin('browser'), cssModules.plugin(true, { emitStyles: () => false })] });
544
570
  fonts.record(browser.metafile);
545
571
  const entryIds = new Map(Object.entries(clientEntries).map(([id, file]) => [file, id]));
546
572
  const edgeBoundaries = new Set([...boundaries.values()].filter(boundary => boundary.edge).map(boundary => boundary.id));
547
- const styles = new Set();
548
573
  let client;
549
574
  for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
550
- if (metadata.cssBundle) styles.add(publicPath + path.basename(metadata.cssBundle));
551
575
  if (!metadata.entryPoint || !output.endsWith('.js')) continue;
552
576
  const id = entryIds.get(path.resolve(project.root, metadata.entryPoint));
553
577
  if (id === 'app-runtime') { client = publicPath + path.basename(output); continue; }
554
578
  if (!id) continue;
555
579
  manifest.app.clientModules[id] = { id, chunks: [id, publicPath + path.basename(output)], name: '*', ssrModule: `app-ssr/${id}.mjs`, ...(edgeBoundaries.has(id) ? { edgeSsrModule: `app-ssr-edge/${id}.mjs` } : {}), browserModule: publicPath + path.basename(output) };
556
580
  }
557
- for (const route of manifest.routes) if (route.router === 'app' && route.kind === 'page') { route.client = client; route.css = [...styles]; }
581
+ const styleBundles = new Map();
582
+ for (const route of routes.filter(route => route.kind === 'page')) {
583
+ // All esbuild graphs canonicalize their inputs, including symlinked
584
+ // convention files. Match those identities before querying reachability.
585
+ for (const files of appStyleScopes(route)) for (const file of files) styleGraph.alias(file, await canonicalFile(file));
586
+ const plan = planAppStyleEntries(route, styleGraph);
587
+ const styles = [];
588
+ const cssScopes = [];
589
+ for (const { styles:files, path:scopePath, kind } of plan) {
590
+ const signature = JSON.stringify(files);
591
+ if (!styleBundles.has(signature)) {
592
+ const id = 'app-style-' + createHash('sha256').update(files.map(file => path.relative(project.root, file).replaceAll(path.sep, '/')).join('\0')).digest('hex').slice(0, 16);
593
+ const styleEntry = path.join(stage, '.entries', id + '.mjs');
594
+ await writeFile(styleEntry, files.map(file => `import ${JSON.stringify(file)};`).join('\n'));
595
+ const output = await bundle({ ...common, entryPoints: { [id]: styleEntry }, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
596
+ plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] });
597
+ styleBundles.set(signature, Object.values(output.metafile.outputs).filter(output => output.cssBundle).map(output => publicPath + path.basename(output.cssBundle)));
598
+ }
599
+ const css = styleBundles.get(signature);
600
+ styles.push(...css);
601
+ if (route.parallel) cssScopes.push({ path:scopePath, kind, css });
602
+ }
603
+ const emitted = manifest.routes.find(item => item.id === route.id);
604
+ if (!emitted.htmlOnly) emitted.client = client;
605
+ emitted.css = [...new Set(styles)];
606
+ if (route.parallel) emitted.cssScopes = cssScopes;
607
+ }
558
608
  }
559
609
  manifest.app.actions = Object.fromEntries([...actions.actions.values()].map(action => [action.id, {
560
610
  module: `server/action-${action.runtimes?.has('nodejs')?'':'edge-'}${action.id}.mjs`, export: 'invoke',
@@ -564,4 +614,9 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
564
614
  serverNativeVersion === nativePackages.version && emittedBrowserInputs === browserRevision()) break;
565
615
  }
566
616
 
617
+ if (htmlGraph) for (const route of routes) if (htmlOnly.has(route.id)) {
618
+ for (const file of [...appStyleScopes(route).flat(), ...(route.metadataFiles || []).map(item=>item.file)].filter(Boolean)) htmlGraph.alias(file, await canonicalFile(file));
619
+ htmlGraph.validate(route);
620
+ }
621
+
567
622
  }
@@ -15,7 +15,7 @@ export const runtimeEntryNames = Object.freeze([
15
15
  ].sort());
16
16
 
17
17
  /** Run once, after compiling application/Edge graphs and before any prerender. */
18
- export async function bundleRuntime(stage,{projectRoot}={}) {
18
+ export async function bundleRuntime(stage,{projectRoot,sourceDirectory}={}) {
19
19
  const resolvedStage = path.resolve(stage);
20
20
  if (!path.basename(resolvedStage).startsWith('.prnext-build-')) {
21
21
  throw new Error('Runtime bundling requires an unpublished PRNext build stage.');
@@ -29,20 +29,31 @@ export async function bundleRuntime(stage,{projectRoot}={}) {
29
29
  if (path.relative(canonicalStage, canonicalRuntime) !== 'runtime') {
30
30
  throw new Error('Runtime bundling escaped the build stage.');
31
31
  }
32
- const entries = await readdir(runtime, { withFileTypes: true });
33
- const originals = entries.filter(entry => entry.name.endsWith('.mjs'));
32
+ const input=sourceDirectory?await realpath(sourceDirectory):runtime;
33
+ const entries = await readdir(input, { withFileTypes: true });
34
+ const originals = entries.filter(entry => entry.name.endsWith('.mjs')&&!entry.name.endsWith('.test.mjs'));
34
35
  if (originals.some(entry => !entry.isFile() || entry.isSymbolicLink())) {
35
36
  throw new Error('Runtime source modules must be ordinary files.');
36
37
  }
37
38
  for (const name of runtimeEntryNames) {
38
39
  if (!originals.some(entry => entry.name === `${name}.mjs`)) throw new Error(`Missing runtime entry ${name}.mjs`);
39
40
  }
41
+ const sources=new Map();
42
+ // Production compiles the installed sources directly. Only generated stage
43
+ // overrides (currently the bundled env loader) replace an installed module.
44
+ // Hash and compile the same captured bytes; do not trust mtimes or skip app
45
+ // graph callbacks. This avoids copying hundreds of modules into every stage.
46
+ for(const entry of originals){
47
+ let file=path.join(input,entry.name);
48
+ if(sourceDirectory){const override=path.join(runtime,entry.name);const info=await lstat(override).catch(e=>{if(e.code!=='ENOENT')throw e;});if(info){if(!info.isFile()||info.isSymbolicLink())throw Error('Runtime overrides must be ordinary files');file=override;}}
49
+ sources.set(path.join(input,entry.name),await readFile(file));
50
+ }
40
51
  let directory,key,cached;
41
52
  if(projectRoot){
42
- directory=await privateCacheDirectory(projectRoot,'runtime-bundle-v1');
53
+ directory=await privateCacheDirectory(projectRoot,'runtime-bundle-v2');
43
54
  if(directory){
44
55
  const hash=createHash('sha256').update(esbuildVersion).update(JSON.stringify(runtimeEntryNames)).update(await readFile(new URL('./bundle-runtime.mjs',import.meta.url)));
45
- for(const entry of [...originals].sort((a,b)=>a.name.localeCompare(b.name)))hash.update(JSON.stringify([entry.name,createHash('sha256').update(await readFile(path.join(runtime,entry.name))).digest('hex')]));
56
+ for(const entry of [...originals].sort((a,b)=>a.name.localeCompare(b.name)))hash.update(JSON.stringify([entry.name,createHash('sha256').update(sources.get(path.join(input,entry.name))).digest('hex')]));
46
57
  key=hash.digest('hex');cached=await readCacheRecord(directory,key);
47
58
  }
48
59
  }
@@ -52,10 +63,10 @@ export async function bundleRuntime(stage,{projectRoot}={}) {
52
63
  const virtualOutput = path.join(resolvedStage, '.runtime-bundle');
53
64
  const validCache=cached&&Array.isArray(cached.files)&&cached.files.length<=128&&cached.files.every(item=>Array.isArray(item)&&item.length===2&&/^[\w-]+\.mjs$/.test(item[0])&&typeof item[1]==='string')&&new Set(cached.files.map(([name])=>name)).size===cached.files.length&&runtimeEntryNames.every(name=>cached.files.some(([file])=>file===name+'.mjs'));
54
65
  const result = validCache?null:await build({
55
- absWorkingDir: runtime,
56
- entryPoints: runtimeEntryNames.map(name => path.join(runtime, `${name}.mjs`)),
66
+ absWorkingDir: input,
67
+ entryPoints: runtimeEntryNames.map(name => path.join(input, `${name}.mjs`)),
57
68
  outdir: virtualOutput,
58
- outbase: runtime,
69
+ outbase: input,
59
70
  entryNames: '[name]',
60
71
  chunkNames: '_shared-[hash]',
61
72
  outExtension: { '.js': '.mjs' },
@@ -73,6 +84,7 @@ export async function bundleRuntime(stage,{projectRoot}={}) {
73
84
  metafile: true,
74
85
  write: false,
75
86
  logLevel: 'silent',
87
+ plugins:[{name:'prnext-runtime-snapshot',setup(build){build.onLoad({filter:/\.mjs$/},args=>sources.has(args.path)?{contents:sources.get(args.path),loader:'js',resolveDir:path.dirname(args.path)}:undefined);}}],
76
88
  });
77
89
  const emitted = new Map();
78
90
  if(validCache)for(const [name,contents]of cached.files)emitted.set(name,Buffer.from(contents));
@@ -87,7 +99,8 @@ export async function bundleRuntime(stage,{projectRoot}={}) {
87
99
  // This is an unpublished, disposable stage. On any write failure buildProject
88
100
  // discards the stage and preserves the previous published build unchanged.
89
101
  for (const [name, contents] of emitted) await writeFile(path.join(runtime, name), contents);
90
- for (const entry of originals) {
102
+ for (const entry of await readdir(runtime,{withFileTypes:true})) {
103
+ if(!entry.name.endsWith('.mjs')||!entry.isFile())continue;
91
104
  if (!emitted.has(entry.name)) await unlink(path.join(runtime, entry.name));
92
105
  }
93
106
  // JSON, CJS, license files and the OG assets directory are never traversed or
@@ -15,7 +15,7 @@ export function transformCacheComponents(source, file, { enabled, projectRoot, m
15
15
  return cachedTransform('cache-components',source,[file,enabled,projectRoot,mode,buildId,configuredSegment,handlers],()=>transformUncached(source,file,{enabled,projectRoot,mode,buildId,configuredSegment,handlers}));
16
16
  }
17
17
  function transformUncached(source,file,{enabled,projectRoot,mode,buildId,configuredSegment,handlers}) {
18
- const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.tsx?$/.test(file) ? ['typescript'] : [])] });
18
+ const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(file) ? ['typescript'] : [])] });
19
19
  let program;
20
20
  traverse(ast, { Program(value) { program = value; value.stop(); } });
21
21
  const moduleDirective = ast.program.directives.find(cacheDirective);
@@ -47,7 +47,7 @@ export async function prepareCacheHandlers({ config, projectRoot, stage, environ
47
47
  platform: 'node', target: 'node22', format: 'esm', packages: 'external', logLevel: 'silent', define: environment,
48
48
  banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" },
49
49
  plugins: [{ name: 'cache-handler-compatibility', setup(builder) {
50
- builder.onResolve({ filter: /^(?:next|prnext|@thomas\.f\/prnext)\/(cache|headers|server)(?:\.js)?$/ }, ({ path: specifier }) => ({ path: `../compat/${frameworkImportName(specifier)}.cjs`, external: true }));
50
+ builder.onResolve({ filter: /^(?:next|prnext|@thomas\.f\/prnext)\/(cache(?:\/redis)?|headers|server)(?:\.js)?$/ }, ({ path: specifier }) => ({ path: `../compat/${frameworkImportName(specifier)}.cjs`, external: true }));
51
51
  } }],
52
52
  });
53
53
  result[name] = output;
@@ -0,0 +1,53 @@
1
+ import postcss from 'postcss';
2
+ import selectors from 'postcss-selector-parser';
3
+ import values from 'postcss-value-parser';
4
+ import { createHash } from 'node:crypto';
5
+
6
+ // Names are independent of compilation order and of which routes are built.
7
+ // A collision fails the build instead of changing one of two published maps.
8
+ export function compactCssModule(css, exports, {globals = new Set(), hash = name => 'p' + createHash('sha256').update(name).digest('base64url').slice(0, 10)} = {}) {
9
+ const names = new Map(), reverse = new Map();
10
+ for (const list of Object.values(exports)) for (const name of list.split(/\s+/).filter(Boolean)) {
11
+ if (names.has(name) || globals.has(name)) continue;
12
+ const short = hash(name);
13
+ if (!/^[a-zA-Z_][\w-]*$/.test(short) || globals.has(short) && short!==name || reverse.has(short) && reverse.get(short) !== name) throw new Error('CSS Module compact-name collision.');
14
+ names.set(name, short); reverse.set(short, name);
15
+ }
16
+ const root = postcss.parse(css);
17
+ root.walkRules(rule => {
18
+ rule.selector = selectors(ast => ast.walkClasses(node => {
19
+ if (names.has(node.value)) node.value = names.get(node.value);
20
+ })).processSync(rule.selector);
21
+ });
22
+ root.walkAtRules(/^(?:-webkit-)?keyframes$/i, rule => {
23
+ const parsed = values(rule.params);
24
+ parsed.walk(node => { if (node.type === 'word' && names.has(node.value)) node.value = names.get(node.value); });
25
+ rule.params = parsed.toString();
26
+ });
27
+ root.walkDecls(/^(?:-webkit-)?animation(?:-name)?$/i, declaration => {
28
+ const parsed = values(declaration.value);
29
+ parsed.walk(node => { if (node.type === 'word' && names.has(node.value)) node.value = names.get(node.value); });
30
+ declaration.value = parsed.toString();
31
+ });
32
+ return { css: root.toString(), names:[...names], mapping: Object.fromEntries(Object.entries(exports).map(([key, list]) => [key, list.split(/\s+/).map(name => names.get(name) || name).join(' ')])) };
33
+ }
34
+
35
+ export function globalCssNames(css, {local = true} = {}, result = new Set()) {
36
+ const root = postcss.parse(css);
37
+ root.walkRules(rule => {
38
+ selectors(ast => ast.each(selector => {
39
+ let global = !local;
40
+ selector.each(node => {
41
+ if (node.type === 'pseudo' && [':global',':local'].includes(node.value)) {
42
+ if (node.nodes?.length) { if (node.value === ':global') node.walkClasses(item=>result.add(item.value)); }
43
+ else global = node.value === ':global';
44
+ } else if (global) { if(node.type==='class') result.add(node.value); node.walkClasses?.(item=>result.add(item.value)); }
45
+ });
46
+ })).processSync(rule.selector);
47
+ });
48
+ root.walkDecls(/^composes$/i, declaration => {
49
+ const words = values(declaration.value).nodes.filter(node=>node.type==='word').map(node=>node.value);
50
+ if (words.at(-1)==='global' && words.at(-2)==='from') for(const name of words.slice(0,-2)) result.add(name);
51
+ });
52
+ return result;
53
+ }