vinext 1.0.0-beta.3 → 1.0.0-beta.4

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/dist/build/precompress.js +1 -1
  2. package/dist/build/prerender.d.ts +2 -1
  3. package/dist/build/prerender.js +12 -2
  4. package/dist/cli-args.d.ts +2 -1
  5. package/dist/cli-args.js +10 -0
  6. package/dist/cli.js +14 -8
  7. package/dist/client/navigation-runtime.d.ts +6 -0
  8. package/dist/client/navigation-runtime.js +1 -1
  9. package/dist/config/dotenv.d.ts +3 -4
  10. package/dist/config/dotenv.js +30 -22
  11. package/dist/config/next-config.d.ts +4 -2
  12. package/dist/config/next-config.js +3 -0
  13. package/dist/entries/app-browser-entry.js +1 -1
  14. package/dist/entries/app-rsc-entry.js +18 -3
  15. package/dist/entries/pages-server-entry.js +4 -1
  16. package/dist/index.js +11 -7
  17. package/dist/plugins/client-reference-dedup.js +7 -5
  18. package/dist/plugins/sass.js +3 -4
  19. package/dist/server/api-handler.js +4 -3
  20. package/dist/server/app-bfcache-identity.js +28 -2
  21. package/dist/server/app-browser-entry.js +73 -45
  22. package/dist/server/app-browser-navigation-controller.d.ts +1 -1
  23. package/dist/server/app-browser-navigation-controller.js +29 -4
  24. package/dist/server/app-browser-prefetch-response.d.ts +9 -0
  25. package/dist/server/app-browser-prefetch-response.js +32 -0
  26. package/dist/server/app-browser-state.d.ts +8 -3
  27. package/dist/server/app-browser-state.js +9 -3
  28. package/dist/server/app-page-dispatch.d.ts +1 -1
  29. package/dist/server/app-page-dispatch.js +6 -2
  30. package/dist/server/app-page-element-builder.d.ts +2 -1
  31. package/dist/server/app-page-element-builder.js +1 -0
  32. package/dist/server/app-page-render.d.ts +1 -1
  33. package/dist/server/app-page-render.js +13 -3
  34. package/dist/server/app-page-request.d.ts +0 -1
  35. package/dist/server/app-page-request.js +3 -12
  36. package/dist/server/app-page-response.d.ts +2 -0
  37. package/dist/server/app-page-response.js +6 -1
  38. package/dist/server/app-page-route-wiring.d.ts +3 -2
  39. package/dist/server/app-page-route-wiring.js +61 -13
  40. package/dist/server/app-pages-bridge.d.ts +2 -1
  41. package/dist/server/app-pages-bridge.js +2 -1
  42. package/dist/server/app-rsc-cache-busting.d.ts +7 -0
  43. package/dist/server/app-rsc-cache-busting.js +6 -0
  44. package/dist/server/app-rsc-handler.d.ts +2 -0
  45. package/dist/server/app-rsc-handler.js +9 -4
  46. package/dist/server/app-rsc-render-mode.d.ts +3 -2
  47. package/dist/server/app-rsc-render-mode.js +4 -1
  48. package/dist/server/app-rsc-request-normalization.js +65 -4
  49. package/dist/server/app-server-action-execution.d.ts +1 -1
  50. package/dist/server/app-visited-response-cache.d.ts +6 -1
  51. package/dist/server/app-visited-response-cache.js +49 -1
  52. package/dist/server/dev-server.d.ts +1 -1
  53. package/dist/server/dev-server.js +61 -38
  54. package/dist/server/edge-api-runtime.d.ts +10 -1
  55. package/dist/server/edge-api-runtime.js +29 -1
  56. package/dist/server/headers.d.ts +3 -1
  57. package/dist/server/headers.js +3 -1
  58. package/dist/server/pages-api-route.d.ts +2 -0
  59. package/dist/server/pages-api-route.js +2 -2
  60. package/dist/server/pages-asset-tags.d.ts +1 -0
  61. package/dist/server/pages-asset-tags.js +6 -4
  62. package/dist/server/pages-document-asset-props.d.ts +26 -0
  63. package/dist/server/pages-document-asset-props.js +69 -0
  64. package/dist/server/pages-node-compat.js +3 -3
  65. package/dist/server/pages-page-handler.d.ts +1 -0
  66. package/dist/server/pages-page-handler.js +4 -1
  67. package/dist/server/pages-page-response.d.ts +2 -0
  68. package/dist/server/pages-page-response.js +19 -8
  69. package/dist/server/pages-router-entry.js +1 -1
  70. package/dist/server/prerender-manifest.d.ts +1 -0
  71. package/dist/server/prod-server.js +2 -1
  72. package/dist/server/seed-cache.js +1 -1
  73. package/dist/server/worker-revalidation-context.js +1 -0
  74. package/dist/shims/app-router-scroll-state.d.ts +2 -1
  75. package/dist/shims/app-router-scroll-state.js +7 -2
  76. package/dist/shims/document.js +15 -2
  77. package/dist/shims/hash-scroll.d.ts +1 -1
  78. package/dist/shims/hash-scroll.js +2 -1
  79. package/dist/shims/link.d.ts +2 -0
  80. package/dist/shims/link.js +59 -18
  81. package/dist/shims/metadata.d.ts +11 -17
  82. package/dist/shims/metadata.js +24 -32
  83. package/dist/shims/navigation.d.ts +18 -2
  84. package/dist/shims/navigation.js +69 -20
  85. package/dist/shims/request-context.d.ts +2 -1
  86. package/dist/shims/slot.d.ts +2 -1
  87. package/dist/shims/slot.js +6 -2
  88. package/dist/shims/streamed-icons.d.ts +9 -0
  89. package/dist/shims/streamed-icons.js +33 -0
  90. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  import path from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
2
  import { ASSET_PREFIX_URL_DIR } from "../utils/asset-prefix.js";
3
3
  import fsp from "node:fs/promises";
4
+ import { promisify } from "node:util";
4
5
  import os from "node:os";
5
6
  import zlib from "node:zlib";
6
- import { promisify } from "node:util";
7
7
  //#region src/build/precompress.ts
8
8
  /**
9
9
  * Build-time precompression for hashed static assets.
@@ -23,7 +23,8 @@ type PrerenderRouteResult = {
23
23
  */
24
24
  path?: string; /** Which router produced this route. Used by cache seeding. */
25
25
  router: "app" | "pages"; /** Response headers that must be replayed with the prerendered artifact. */
26
- headers?: Record<string, string>; /** Set to true when this is a PPR fallback shell. */
26
+ headers?: Record<string, string>; /** Cache tags collected while rendering this route. */
27
+ tags?: string[]; /** Set to true when this is a PPR fallback shell. */
27
28
  fallback?: boolean;
28
29
  } | {
29
30
  route: string;
@@ -1,7 +1,7 @@
1
1
  import path from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
2
  import { createValidFileMatcher, findFileWithExtensions } from "../routing/file-matcher.js";
3
3
  import { VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_PRERENDER_SPECULATIVE_HEADER } from "../utils/protocol-headers.js";
4
- import { VINEXT_PRERENDER_CACHE_LIFE_HEADER } from "../server/headers.js";
4
+ import { NEXT_CACHE_TAGS_HEADER, VINEXT_PRERENDER_CACHE_LIFE_HEADER } from "../server/headers.js";
5
5
  import { classifyAppRoute, classifyPagesRoute, getAppRouteRenderEntryPath } from "./report.js";
6
6
  import { NoOpCacheHandler, getCacheHandler, setCacheHandler } from "../shims/cache-handler.js";
7
7
  import { headersContextFromRequest, runWithHeadersContext } from "../shims/headers.js";
@@ -760,6 +760,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
760
760
  const cacheControl = response.headers.get("cache-control") ?? "";
761
761
  const linkHeader = response.headers.get("link");
762
762
  const responseCacheLife = readPrerenderCacheLifeHeader(response.headers);
763
+ const cacheTags = readPrerenderCacheTagsHeader(response.headers);
763
764
  if (!response.ok || cacheControl.includes("no-store")) {
764
765
  await response.body?.cancel();
765
766
  return {
@@ -768,6 +769,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
768
769
  html: null,
769
770
  ok: response.ok,
770
771
  requestCacheLife: null,
772
+ tags: [],
771
773
  status: response.status
772
774
  };
773
775
  }
@@ -779,7 +781,8 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
779
781
  html,
780
782
  ok: true,
781
783
  requestCacheLife: responseCacheLife ?? processCacheLife,
782
- status: response.status
784
+ status: response.status,
785
+ tags: cacheTags
783
786
  };
784
787
  });
785
788
  const htmlCacheControl = htmlRender.cacheControl;
@@ -842,6 +845,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
842
845
  revalidate: renderedRevalidate,
843
846
  ...typeof renderedRevalidate === "number" ? { expire: renderedCacheControl.expire } : {},
844
847
  router: "app",
848
+ ...htmlRender.tags.length > 0 ? { tags: htmlRender.tags } : {},
845
849
  ...htmlRender.linkHeader ? { headers: { link: htmlRender.linkHeader } } : {},
846
850
  ...urlPath !== routePattern ? { path: urlPath } : {},
847
851
  ...isFallback ? { fallback: true } : {}
@@ -945,6 +949,11 @@ function readPrerenderCacheLifeHeader(headers) {
945
949
  return null;
946
950
  }
947
951
  }
952
+ function readPrerenderCacheTagsHeader(headers) {
953
+ const value = headers.get(NEXT_CACHE_TAGS_HEADER);
954
+ if (!value) return [];
955
+ return [...new Set(value.split(",").filter(Boolean))];
956
+ }
948
957
  function resolveRenderedExpireSeconds(options) {
949
958
  const { fallbackExpireSeconds, sMaxage, staleWhileRevalidate } = options;
950
959
  if (sMaxage === void 0 || staleWhileRevalidate === void 0) return fallbackExpireSeconds;
@@ -976,6 +985,7 @@ function writePrerenderIndex(routes, outDir, options) {
976
985
  revalidate: r.revalidate,
977
986
  ...typeof r.revalidate === "number" ? { expire: r.expire } : {},
978
987
  router: r.router,
988
+ ...r.tags && r.tags.length > 0 ? { tags: r.tags } : {},
979
989
  ...r.headers ? { headers: r.headers } : {},
980
990
  ...r.path ? { path: r.path } : {},
981
991
  ...r.fallback ? { fallback: true } : {}
@@ -3,12 +3,13 @@
3
3
  * CLI argument parser for the vinext CLI.
4
4
  *
5
5
  * Parses flags for `vinext dev`, `vinext start`, `vinext build`, etc.
6
- * Validates that value-taking flags (`--port`, `--hostname`) have actual values
6
+ * Validates that value-taking flags (`--port`, `--hostname`, `--mode`) have actual values
7
7
  * rather than silently consuming the next flag or returning NaN/undefined.
8
8
  */
9
9
  type ParsedArgs = {
10
10
  port?: number;
11
11
  hostname?: string;
12
+ mode?: string;
12
13
  help?: boolean;
13
14
  verbose?: boolean;
14
15
  turbopack?: boolean;
package/dist/cli-args.js CHANGED
@@ -96,6 +96,10 @@ function parseArgs(args) {
96
96
  result.hostname = takeValue(arg, args, i);
97
97
  i++;
98
98
  break;
99
+ case "--mode":
100
+ result.mode = takeValue(arg, args, i);
101
+ i++;
102
+ break;
99
103
  default: {
100
104
  const eqRaw = tryEqualsForm(arg, "port");
101
105
  if (eqRaw !== null) {
@@ -108,6 +112,12 @@ function parseArgs(args) {
108
112
  result.hostname = hostRaw;
109
113
  break;
110
114
  }
115
+ const modeRaw = tryEqualsForm(arg, "mode");
116
+ if (modeRaw !== null) {
117
+ if (modeRaw === "") throw new Error(`--mode requires a value, but none was provided.`);
118
+ result.mode = modeRaw;
119
+ break;
120
+ }
111
121
  const prerenderConcurrencyRaw = tryEqualsForm(arg, "prerender-concurrency");
112
122
  if (prerenderConcurrencyRaw !== null) {
113
123
  result.prerenderConcurrency = parsePositiveIntegerArg(prerenderConcurrencyRaw, "--prerender-concurrency");
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { parseArgs } from "./cli-args.js";
6
6
  import { createDevServerConfigPlugin, normalizeDevServerHostname } from "./cli-dev-config.js";
7
7
  import { PHASE_PRODUCTION_BUILD } from "./shims/constants.js";
8
8
  import { createRscCompatibilityId, findVinextNextConfigInPlugins, loadNextConfig, resolveNextConfig, resolveNextConfigInput } from "./config/next-config.js";
9
+ import { loadDotenv } from "./config/dotenv.js";
9
10
  import { generateRouteTypes } from "./typegen.js";
10
11
  import { clearPagesClientAssetsBuildMetadata } from "./build/pages-client-assets-module.js";
11
12
  import { runWithPreviewBuildCredentials } from "./build/preview-credentials.js";
@@ -16,7 +17,6 @@ import { emitPrerenderPathManifest } from "./build/prerender-paths.js";
16
17
  import { getReactUpgradeDeps } from "./utils/react-version.js";
17
18
  import { init } from "./init.js";
18
19
  import { resolveInitOptions } from "./init-platform.js";
19
- import { loadDotenv } from "./config/dotenv.js";
20
20
  import { resolveVinextPackageRoot } from "./utils/vinext-root.js";
21
21
  import { emitStandaloneOutput } from "./build/standalone.js";
22
22
  import { cleanBuildOutput } from "./build/clean-output.js";
@@ -125,7 +125,7 @@ function createBuildLogger(vite) {
125
125
  function hasPagesDir() {
126
126
  return fs.existsSync(path.join(process.cwd(), "pages")) || fs.existsSync(path.join(process.cwd(), "src", "pages"));
127
127
  }
128
- async function loadBuildViteConfigMetadata(vite, root) {
128
+ async function loadBuildViteConfigMetadata(vite, root, mode) {
129
129
  if (!hasViteConfig(root)) return {
130
130
  nextConfig: null,
131
131
  prerenderConfig: null,
@@ -133,7 +133,7 @@ async function loadBuildViteConfigMetadata(vite, root) {
133
133
  };
134
134
  const loaded = await vite.loadConfigFromFile({
135
135
  command: "build",
136
- mode: "production"
136
+ mode
137
137
  }, void 0, root);
138
138
  const emptyOutDir = loaded?.config.build?.emptyOutDir;
139
139
  return {
@@ -148,14 +148,16 @@ async function loadBuildViteConfigMetadata(vite, root) {
148
148
  * project, Vite will merge our config with it (theirs takes precedence).
149
149
  * If there's no vite.config, this provides everything needed.
150
150
  */
151
- function buildViteConfig(overrides = {}, logger) {
151
+ function buildViteConfig(overrides = {}, logger, mode) {
152
152
  if (hasViteConfig(process.cwd())) return {
153
153
  root: process.cwd(),
154
+ ...mode ? { mode } : {},
154
155
  ...logger ? { customLogger: logger } : {},
155
156
  ...overrides
156
157
  };
157
158
  return {
158
159
  root: process.cwd(),
160
+ ...mode ? { mode } : {},
159
161
  configFile: false,
160
162
  plugins: [vinext()],
161
163
  resolve: { dedupe: [
@@ -274,10 +276,12 @@ async function dev() {
274
276
  async function buildApp() {
275
277
  const parsed = parseArgs(rawArgs);
276
278
  if (parsed.help) return printHelp("build");
279
+ const buildMode = parsed.mode ?? "production";
280
+ if (!process.env.NODE_ENV) Reflect.set(process.env, "NODE_ENV", "production");
277
281
  if (parsed.precompress) process.env.VINEXT_PRECOMPRESS = "1";
278
282
  loadDotenv({
279
283
  root: process.cwd(),
280
- mode: "production"
284
+ mode: buildMode
281
285
  });
282
286
  applyViteConfigCompatibility(process.cwd());
283
287
  const vite = await loadVite();
@@ -285,7 +289,7 @@ async function buildApp() {
285
289
  console.log(`\n vinext build (Vite ${getViteVersion()})\n`);
286
290
  const root = toSlash(process.cwd());
287
291
  const isApp = hasAppDir(root);
288
- const buildConfigMetadata = await loadBuildViteConfigMetadata(vite, root);
292
+ const buildConfigMetadata = await loadBuildViteConfigMetadata(vite, root, buildMode);
289
293
  const resolvedNextConfig = await resolveNextConfig(buildConfigMetadata.nextConfig ? await resolveNextConfigInput(buildConfigMetadata.nextConfig, PHASE_PRODUCTION_BUILD) : await loadNextConfig(root, PHASE_PRODUCTION_BUILD), root);
290
294
  process.env.__VINEXT_SHARED_BUILD_ID = resolvedNextConfig.buildId;
291
295
  process.env.__VINEXT_SHARED_RSC_COMPATIBILITY_ID = createRscCompatibilityId(resolvedNextConfig);
@@ -322,7 +326,7 @@ async function buildApp() {
322
326
  if (pagesClientAssetsBuildSession) process.env.__VINEXT_PAGES_CLIENT_ASSETS_BUILD_SESSION = pagesClientAssetsBuildSession;
323
327
  await runWithPreviewBuildCredentials(async () => {
324
328
  try {
325
- const config = buildViteConfig({}, logger);
329
+ const config = buildViteConfig({}, logger, buildMode);
326
330
  await (await vite.createBuilder(config)).buildApp();
327
331
  if (isHybrid) {
328
332
  console.log(" Building Pages Router server (hybrid)...");
@@ -331,13 +335,14 @@ async function buildApp() {
331
335
  if (hasViteConfig(process.cwd())) {
332
336
  const loaded = await vite.loadConfigFromFile({
333
337
  command: "build",
334
- mode: "production",
338
+ mode: buildMode,
335
339
  isSsrBuild: true
336
340
  }, void 0, root);
337
341
  if (loaded?.config.plugins) userTransformPlugins = loaded.config.plugins.flat(Infinity).filter((p) => !!p && typeof p.name === "string" && !p.name.startsWith("vinext:") && !p.name.startsWith("vite:react") && !p.name.startsWith("rsc:") && p.name !== "vite-rsc-load-module-dev-proxy" && !p.name.startsWith("vite-plugin-cloudflare"));
338
342
  }
339
343
  await vite.build({
340
344
  root,
345
+ mode: buildMode,
341
346
  configFile: false,
342
347
  plugins: [...userTransformPlugins, vinext({ disableAppRouter: true })],
343
348
  resolve: { dedupe: [
@@ -536,6 +541,7 @@ function printHelp(cmd) {
536
541
  to vinext({ prerender: true }) and takes priority.
537
542
  --prerender-concurrency <count>
538
543
  Maximum number of routes to pre-render in parallel
544
+ --mode <mode> Load .env.<mode>* files and pass mode to Vite
539
545
  --precompress Precompress static assets at build time (.br, .gz, .zst)
540
546
  -h, --help Show this help
541
547
  `);
@@ -18,6 +18,10 @@ type NavigationRuntimeRscBootstrap = {
18
18
  type NavigationRuntimeKind = "navigate" | "traverse" | "refresh";
19
19
  type NavigationRuntimeHistoryUpdateMode = "push" | "replace";
20
20
  type NavigationRuntimeVisibleCommitMode = "transition" | "synchronous";
21
+ type NavigationRuntimePrefetchRouterState = {
22
+ pathAndSearch: string;
23
+ routeId: string;
24
+ };
21
25
  type NavigationRuntimeTraversalIntent = {
22
26
  direction: "back" | "forward" | "unknown";
23
27
  historyState: unknown;
@@ -29,6 +33,7 @@ type NavigationRuntimeFunctions = {
29
33
  commitHashNavigation?: (href: string, historyUpdateMode: NavigationRuntimeHistoryUpdateMode, scroll: boolean) => void;
30
34
  navigateExternal?: (href: string, historyUpdateMode: NavigationRuntimeHistoryUpdateMode) => Promise<void>;
31
35
  navigate?: NavigationRuntimeNavigate;
36
+ getPrefetchRouterState?: () => NavigationRuntimePrefetchRouterState | null;
32
37
  /**
33
38
  * Called at the start of every App Router navigation so the <Link> shim can
34
39
  * reset any link that is still showing a `useLinkStatus()` pending state but
@@ -38,6 +43,7 @@ type NavigationRuntimeFunctions = {
38
43
  */
39
44
  notifyLinkNavigationStart?: () => void;
40
45
  pingVisibleLinks?: () => void;
46
+ preparePrefetchResponse?: (response: Response) => Promise<unknown>;
41
47
  };
42
48
  type NavigationRuntimeBootstrap = {
43
49
  routeManifest: RouteManifest | null;
@@ -31,7 +31,7 @@ function readRuntimeWindow() {
31
31
  }
32
32
  function isNavigationRuntimeFunctions(value) {
33
33
  if (!isUnknownRecord(value)) return false;
34
- return isOptionalRuntimeFunction(Reflect.get(value, "clearNavigationCaches")) && isOptionalRuntimeFunction(Reflect.get(value, "commitHashNavigation")) && isOptionalRuntimeFunction(Reflect.get(value, "navigateExternal")) && isOptionalRuntimeFunction(Reflect.get(value, "navigate")) && isOptionalRuntimeFunction(Reflect.get(value, "notifyLinkNavigationStart")) && isOptionalRuntimeFunction(Reflect.get(value, "pingVisibleLinks"));
34
+ return isOptionalRuntimeFunction(Reflect.get(value, "clearNavigationCaches")) && isOptionalRuntimeFunction(Reflect.get(value, "commitHashNavigation")) && isOptionalRuntimeFunction(Reflect.get(value, "navigateExternal")) && isOptionalRuntimeFunction(Reflect.get(value, "navigate")) && isOptionalRuntimeFunction(Reflect.get(value, "getPrefetchRouterState")) && isOptionalRuntimeFunction(Reflect.get(value, "notifyLinkNavigationStart")) && isOptionalRuntimeFunction(Reflect.get(value, "pingVisibleLinks")) && isOptionalRuntimeFunction(Reflect.get(value, "preparePrefetchResponse"));
35
35
  }
36
36
  function isNavigationRuntimeRscChunk(value) {
37
37
  if (typeof value === "string") return true;
@@ -1,5 +1,4 @@
1
1
  //#region src/config/dotenv.d.ts
2
- type VinextEnvMode = "development" | "production" | "test";
3
2
  /**
4
3
  * Environment-variable bag accepted by {@link loadDotenv}.
5
4
  *
@@ -12,18 +11,18 @@ type VinextEnvMode = "development" | "production" | "test";
12
11
  type EnvBag = Record<string, string | undefined>;
13
12
  type LoadDotenvOptions = {
14
13
  root: string;
15
- mode: VinextEnvMode;
14
+ mode: string;
16
15
  processEnv?: EnvBag;
17
16
  };
18
17
  type LoadDotenvResult = {
19
- mode: VinextEnvMode;
18
+ mode: string;
20
19
  loadedFiles: string[];
21
20
  loadedEnv: Record<string, string>;
22
21
  };
23
22
  /**
24
23
  * Next.js-compatible dotenv lookup order (highest priority first).
25
24
  */
26
- declare function getDotenvFiles(mode: VinextEnvMode): string[];
25
+ declare function getDotenvFiles(mode: string): string[];
27
26
  /**
28
27
  * Load .env files into processEnv with Next.js-like precedence:
29
28
  * process.env > .env.<mode>.local > .env.local > .env.<mode> > .env.
@@ -56,33 +56,41 @@ function loadDotenv({ root, mode, processEnv = process.env }) {
56
56
  loadedEnv
57
57
  };
58
58
  }
59
- const ENV_REF_RE = /(\\)?\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
60
59
  function expandEnv(parsed, processEnv) {
61
- const expanded = {};
62
- const resolving = /* @__PURE__ */ new Set();
63
- const context = {
60
+ const expanded = { ...parsed };
61
+ for (const key of Object.keys(expanded)) {
62
+ const processValue = processEnv[key];
63
+ expanded[key] = (processValue && processValue !== expanded[key] ? processValue : expandValue(expanded[key], processEnv, expanded)).replace(/\\\$/g, "$");
64
+ }
65
+ return expanded;
66
+ }
67
+ function expandValue(value, processEnv, parsed) {
68
+ const env = {
64
69
  ...parsed,
65
70
  ...processEnv
66
71
  };
67
- function resolveValue(key) {
68
- const cached = expanded[key];
69
- if (cached !== void 0) return cached;
70
- if (resolving.has(key)) return context[key] ?? "";
71
- const raw = context[key];
72
- if (raw === void 0) return "";
73
- resolving.add(key);
74
- let value = raw.replace(ENV_REF_RE, (match, escaped, braced, bare) => {
75
- if (escaped) return match.slice(1);
76
- return resolveValue(braced || bare);
77
- });
78
- value = value.replace(/\\\$/g, "$");
79
- resolving.delete(key);
80
- expanded[key] = value;
81
- context[key] = value;
82
- return value;
72
+ const envRefRe = /(?<!\\)\${([^{}]+)}|(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)/g;
73
+ const seen = /* @__PURE__ */ new Set();
74
+ let result = value;
75
+ let match;
76
+ while ((match = envRefRe.exec(result)) !== null) {
77
+ seen.add(result);
78
+ const [template, braced, bare] = match;
79
+ const expression = braced || bare;
80
+ const operator = expression.match(/(:\+|\+|:-|-)/)?.[0];
81
+ const parts = operator ? expression.split(operator) : [expression];
82
+ const refKey = parts.shift();
83
+ const operand = parts.join(operator ?? "");
84
+ const refValue = env[refKey];
85
+ let replacement;
86
+ if (operator === ":+" || operator === "+") replacement = refValue ? operand : "";
87
+ else if (refValue) replacement = seen.has(refValue) ? operand : refValue;
88
+ else replacement = operand;
89
+ result = result.replace(template, replacement);
90
+ if (result === parsed[refKey]) break;
91
+ envRefRe.lastIndex = 0;
83
92
  }
84
- for (const key of Object.keys(parsed)) resolveValue(key);
85
- return expanded;
93
+ return result;
86
94
  }
87
95
  //#endregion
88
96
  export { getDotenvFiles, loadDotenv };
@@ -99,7 +99,8 @@ type NextConfig = {
99
99
  * `basePath` affects route URLs; `assetPrefix` only affects asset URLs.
100
100
  * @see https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix
101
101
  */
102
- assetPrefix?: string; /** Whether to add trailing slashes */
102
+ assetPrefix?: string; /** Cross-origin mode applied to framework scripts and preload links. */
103
+ crossOrigin?: "anonymous" | "use-credentials"; /** Whether to add trailing slashes */
103
104
  trailingSlash?: boolean; /** TypeScript build settings. */
104
105
  typescript?: {
105
106
  /** Project-relative path to the TypeScript configuration file. */tsconfigPath?: string;
@@ -382,7 +383,8 @@ type ResolvedNextConfig = {
382
383
  * `defer={!disableOptimizedLoading}`) and the upstream
383
384
  * `test/e2e/optimized-loading` test fixture.
384
385
  */
385
- disableOptimizedLoading: boolean;
386
+ disableOptimizedLoading: boolean; /** Cross-origin mode applied to framework scripts and preload links. */
387
+ crossOrigin: "anonymous" | "use-credentials" | undefined;
386
388
  /**
387
389
  * Resolved `reactStrictMode` from next.config, preserved as `boolean | null`
388
390
  * so each router can apply its own default (Next.js resolves `null` to OFF
@@ -702,6 +702,7 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
702
702
  sassOptions: null,
703
703
  removeConsole: false,
704
704
  disableOptimizedLoading: false,
705
+ crossOrigin: void 0,
705
706
  reactStrictMode: null,
706
707
  scrollRestoration: false,
707
708
  compilerDefine: {},
@@ -718,6 +719,7 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
718
719
  detectNextIntlConfig(root, resolved);
719
720
  return resolved;
720
721
  }
722
+ if (config.crossOrigin !== void 0 && config.crossOrigin !== "anonymous" && config.crossOrigin !== "use-credentials") console.warn("Invalid next.config options detected:\n Invalid option at \"crossOrigin\": expected \"anonymous\" or \"use-credentials\"\nSee more info here: https://nextjs.org/docs/messages/invalid-next-config");
721
723
  warnDeprecatedConfigOptions(config, root);
722
724
  const i18n = normalizeI18nConfig(config.i18n);
723
725
  let redirects = [];
@@ -876,6 +878,7 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
876
878
  sassOptions: readOptionalRecord(config.sassOptions) ?? null,
877
879
  removeConsole: config.compiler?.removeConsole === true ? true : isUnknownRecord(config.compiler?.removeConsole) ? { exclude: readStringArray(config.compiler.removeConsole.exclude) } : false,
878
880
  disableOptimizedLoading: experimental?.disableOptimizedLoading === true,
881
+ crossOrigin: config.crossOrigin === "anonymous" || config.crossOrigin === "use-credentials" ? config.crossOrigin : void 0,
879
882
  reactStrictMode: typeof config.reactStrictMode === "boolean" ? config.reactStrictMode : null,
880
883
  scrollRestoration: experimental?.scrollRestoration === true,
881
884
  compilerDefine: serializeCompilerDefine(config.compiler?.define),
@@ -58,7 +58,7 @@ function interceptTargetsRoute(interceptTargetPattern, route) {
58
58
  return patternsStructurallyEquivalent(splitPatternParts(interceptTargetPattern), route.patternParts);
59
59
  }
60
60
  function hasLoadingBoundary(route, hasSiblingInterceptLoading) {
61
- return route.loadingPath !== null || (route.loadingPaths?.length ?? 0) > 0 || route.parallelSlots.some((slot) => slot.loadingPath !== null || (slot.loadingPaths?.length ?? 0) > 0 || slot.interceptingRoutes.some((intercept) => interceptTargetsRoute(intercept.targetPattern, route) && (intercept.loadingPaths?.length ?? 0) > 0)) || hasSiblingInterceptLoading;
61
+ return route.loadingPath !== null || (route.loadingPaths?.some((_, index) => (route.loadingTreePositions?.[index] ?? 0) > 0) ?? false) || route.parallelSlots.some((slot) => slot.loadingPath !== null || (slot.loadingPaths?.length ?? 0) > 0 || slot.interceptingRoutes.some((intercept) => interceptTargetsRoute(intercept.targetPattern, route) && (intercept.loadingPaths?.length ?? 0) > 0)) || hasSiblingInterceptLoading;
62
62
  }
63
63
  /** Project an `AppRoute` down to the public `VinextLinkPrefetchRoute` shape. */
64
64
  function toLinkPrefetchRoute(route, hasSiblingInterceptLoading = route.siblingIntercepts.some((intercept) => interceptTargetsRoute(intercept.targetPattern, route) && (intercept.loadingPaths?.length ?? 0) > 0)) {
@@ -414,7 +414,7 @@ function findIntercept(pathname, sourcePathname = null) {
414
414
  return __routeMatcher.findIntercept(pathname, sourcePathname);
415
415
  }
416
416
 
417
- async function buildPageElements(route, params, routePath, pageRequest, layoutParamAccess, displayPathname = routePath) {
417
+ async function buildPageElements(route, params, routePath, pageRequest, layoutParamAccess, displayPathname = routePath, scriptNonce) {
418
418
  // Hydrate lazy page/route-handler modules before any synchronous read.
419
419
  await __ensureRouteLoaded(route);
420
420
  return __buildPageElements({
@@ -433,6 +433,7 @@ async function buildPageElements(route, params, routePath, pageRequest, layoutPa
433
433
  basePath: __basePath,
434
434
  trailingSlash: __trailingSlash,
435
435
  htmlLimitedBots: __htmlLimitedBots,
436
+ scriptNonce,
436
437
  });
437
438
  }
438
439
 
@@ -598,7 +599,7 @@ export default createAppRscHandler({
598
599
  observePageSearchParamsAccess: buildOptions?.observePageSearchParamsAccess === true,
599
600
  serveStreamingMetadata: buildOptions?.serveStreamingMetadata,
600
601
  isProduction: process.env.NODE_ENV === "production",
601
- }, layoutParamAccess, displayPathname);
602
+ }, layoutParamAccess, displayPathname, scriptNonce);
602
603
  },
603
604
  clientReuseManifest,
604
605
  cleanPathname,
@@ -842,6 +843,7 @@ export default createAppRscHandler({
842
843
  middlewareContext,
843
844
  mountedSlotsHeader,
844
845
  request,
846
+ scriptNonce,
845
847
  routeMatch,
846
848
  routePathname,
847
849
  searchParams,
@@ -884,7 +886,7 @@ export default createAppRscHandler({
884
886
  renderMode: actionRenderMode,
885
887
  observeMetadataSearchParamsAccess: observeMetadataSearchParamsAccess === true,
886
888
  observePageSearchParamsAccess: observePageSearchParamsAccess === true,
887
- });
889
+ }, undefined, actionCleanPathname, scriptNonce);
888
890
  },
889
891
  cleanPathname,
890
892
  clearRequestContext() {
@@ -984,6 +986,19 @@ export default createAppRscHandler({
984
986
  },` : ""}
985
987
  matchRoute,
986
988
  matchRequestRoute,
989
+ matchInterceptRoute(pathname, sourcePathname) {
990
+ const intercept = findIntercept(pathname, sourcePathname);
991
+ if (!intercept) return null;
992
+ const route = routes[intercept.sourceRouteIndex];
993
+ if (!route) return null;
994
+ const params = Object.create(null);
995
+ for (const name of route.params) {
996
+ if (Object.prototype.hasOwnProperty.call(intercept.sourceMatchedParams, name)) {
997
+ params[name] = intercept.sourceMatchedParams[name];
998
+ }
999
+ }
1000
+ return { route, params };
1001
+ },
987
1002
  ${middlewarePath ? `runMiddleware({ cleanPathname, context, hadBasePath, isDataRequest, request }) {
988
1003
  return __applyAppMiddleware({
989
1004
  basePath: __basePath,
@@ -77,6 +77,7 @@ async function generateServerEntry(pagesDir, nextConfig, fileMatcher, middleware
77
77
  htmlLimitedBots: nextConfig?.htmlLimitedBots,
78
78
  i18n: nextConfig?.i18n ?? null,
79
79
  disableOptimizedLoading: nextConfig?.disableOptimizedLoading === true,
80
+ crossOrigin: nextConfig?.crossOrigin,
80
81
  clientTraceMetadata: nextConfig?.clientTraceMetadata,
81
82
  images: {
82
83
  deviceSizes: nextConfig?.images?.deviceSizes,
@@ -298,6 +299,7 @@ const _renderPage = __createPagesPageHandler({
298
299
  htmlLimitedBots: vinextConfig.htmlLimitedBots,
299
300
  clientTraceMetadata: vinextConfig.clientTraceMetadata,
300
301
  disableOptimizedLoading: vinextConfig.disableOptimizedLoading,
302
+ crossOrigin: vinextConfig.crossOrigin,
301
303
  },
302
304
  buildId,
303
305
  hasMiddleware,
@@ -385,11 +387,12 @@ export async function renderPage(request, url, manifest, ctx, middlewareHeaders,
385
387
 
386
388
 
387
389
 
388
- export async function handleApiRoute(request, url, ctx, trustedRevalidateOrigin) {
390
+ export async function handleApiRoute(request, url, ctx, trustedRevalidateOrigin, edgeRuntime = "worker") {
389
391
  __registerConfiguredCacheAdapters();
390
392
  const match = matchRoute(url, apiRoutes);
391
393
  return __handlePagesApiRoute({
392
394
  ctx,
395
+ edgeRuntime,
393
396
  match,
394
397
  nextConfig: vinextConfig,
395
398
  request,
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ import { planRouteClassificationInjection } from "./build/route-classification-i
41
41
  import { PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD } from "./shims/constants.js";
42
42
  import { RESOLVED_VIRTUAL_GOOGLE_FONTS, VIRTUAL_GOOGLE_FONTS, createGoogleFontsPlugin, createLocalFontsPlugin, generateGoogleFontsVirtualModule, parseStaticObjectLiteral } from "./plugins/fonts.js";
43
43
  import { VINEXT_NEXT_CONFIG_PLUGIN_PROPERTY, createRscCompatibilityId, findNextConfigPath, loadNextConfig, resolveNextConfig, resolveNextConfigInput } from "./config/next-config.js";
44
+ import { loadDotenv } from "./config/dotenv.js";
44
45
  import { mergeServerExternalPackages } from "./config/server-external-packages.js";
45
46
  import { precompressAssets } from "./build/precompress.js";
46
47
  import { ensureAssetsIgnore } from "./build/assets-ignore.js";
@@ -97,7 +98,7 @@ import { VINEXT_PRERENDER_CONFIG_PLUGIN_PROPERTY, VINEXT_ROUTE_ROOT_CONFIG_PLUGI
97
98
  import { staticExportApp, staticExportPages } from "./build/static-export.js";
98
99
  import { createRequire } from "node:module";
99
100
  import fs from "node:fs";
100
- import { createLogger, loadEnv, parseAst, transformWithOxc } from "vite";
101
+ import { createLogger, parseAst, transformWithOxc } from "vite";
101
102
  import { pathToFileURL } from "node:url";
102
103
  import { createHash, randomBytes } from "node:crypto";
103
104
  import commonjs from "vite-plugin-commonjs";
@@ -984,7 +985,7 @@ function vinext(options = {}) {
984
985
  transform: {
985
986
  filter: { id: /\.m?js(?:\?.*)?$/ },
986
987
  async handler(code, id) {
987
- const cleanId = id.split("?")[0];
988
+ const cleanId = toSlash(stripViteModuleQuery(id));
988
989
  if (isInsideDirectory(__dirname, cleanId)) return;
989
990
  if (cleanId.includes("/node_modules/")) {
990
991
  if (!code.includes("use client") && !code.includes("use server")) return;
@@ -1032,11 +1033,13 @@ function vinext(options = {}) {
1032
1033
  let tsconfigPathAliases = {};
1033
1034
  const swcHelpersAlias = resolveSwcHelpersAlias(root);
1034
1035
  const mode = env?.mode ?? "development";
1035
- const dotenvVars = loadEnv(mode, config.envDir ?? root, "");
1036
- for (const [key, value] of Object.entries(dotenvVars)) if (process.env[key] === void 0) process.env[key] = value;
1036
+ if (config.envDir !== false) loadDotenv({
1037
+ root: config.envDir ?? root,
1038
+ mode
1039
+ });
1037
1040
  let resolvedNodeEnv;
1038
- if (mode === "test") resolvedNodeEnv = "test";
1039
- else if (env?.command === "build" || env?.isPreview === true) resolvedNodeEnv = "production";
1041
+ if (env?.command === "build" || env?.isPreview === true) resolvedNodeEnv = "production";
1042
+ else if (mode === "test") resolvedNodeEnv = "test";
1040
1043
  else resolvedNodeEnv = "development";
1041
1044
  if (process.env.NODE_ENV !== resolvedNodeEnv) Reflect.set(process.env, "NODE_ENV", resolvedNodeEnv);
1042
1045
  if (env?.command === "build") previewBuildCredentials = getPreviewBuildCredentials() ?? createPreviewBuildCredentials();
@@ -1118,6 +1121,7 @@ function vinext(options = {}) {
1118
1121
  defines["process.env.__VINEXT_PREFETCH_INLINING"] = JSON.stringify(nextConfig.prefetchInlining ? "true" : "false");
1119
1122
  defines["process.env.__NEXT_GESTURE_TRANSITION"] = JSON.stringify(nextConfig.gestureTransition);
1120
1123
  defines["process.env.__NEXT_APP_NAV_FAIL_HANDLING"] = JSON.stringify(nextConfig.appNavFailHandling);
1124
+ defines["process.env.__NEXT_TEST_MODE"] = JSON.stringify(process.env.__NEXT_TEST_MODE ?? false);
1121
1125
  defines["process.env.__NEXT_SCROLL_RESTORATION"] = JSON.stringify(nextConfig.scrollRestoration ? "true" : "false");
1122
1126
  defines["process.env.__VINEXT_TRAILING_SLASH"] = JSON.stringify(nextConfig.trailingSlash ? "true" : "false");
1123
1127
  defines["process.env.__VINEXT_IMAGE_REMOTE_PATTERNS"] = JSON.stringify(JSON.stringify(nextConfig.images?.remotePatterns ?? []));
@@ -2518,7 +2522,7 @@ export const loadServerActionClient = ${hasServerActions ? `() => import(${JSON.
2518
2522
  }
2519
2523
  if (!cachedSSRHandler || cachedSSRHandler.routes !== routes) cachedSSRHandler = {
2520
2524
  routes,
2521
- handler: createSSRHandler(server, getPagesRunner(), routes, pagesDir, nextConfig?.i18n, fileMatcher, nextConfig?.basePath ?? "", nextConfig?.trailingSlash ?? false, middlewarePath !== null, (nextConfig?.rewrites.beforeFiles.length ?? 0) > 0 || (nextConfig?.rewrites.afterFiles.length ?? 0) > 0 || (nextConfig?.rewrites.fallback.length ?? 0) > 0, nextConfig?.clientTraceMetadata, nextConfig?.htmlLimitedBots, nextConfig?.reactStrictMode === true, nextConfig?.expireTime)
2525
+ handler: createSSRHandler(server, getPagesRunner(), routes, pagesDir, nextConfig?.i18n, fileMatcher, nextConfig?.basePath ?? "", nextConfig?.trailingSlash ?? false, middlewarePath !== null, (nextConfig?.rewrites.beforeFiles.length ?? 0) > 0 || (nextConfig?.rewrites.afterFiles.length ?? 0) > 0 || (nextConfig?.rewrites.fallback.length ?? 0) > 0, nextConfig?.clientTraceMetadata, nextConfig?.htmlLimitedBots, nextConfig?.reactStrictMode === true, nextConfig?.expireTime, nextConfig?.crossOrigin)
2522
2526
  };
2523
2527
  flushStagedHeaders();
2524
2528
  flushRequestHeaders();
@@ -1,3 +1,4 @@
1
+ import path, { toSlash } from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
1
2
  import { isUnknownRecord } from "../utils/record.js";
2
3
  import { readFile } from "node:fs/promises";
3
4
  //#region src/plugins/client-reference-dedup.ts
@@ -156,14 +157,15 @@ function clientReferenceDedupPlugin(options = {}) {
156
157
  async handler(id, importer) {
157
158
  if (this.environment?.name !== "client") return;
158
159
  if (!importer || !importer.includes(PROXY_MARKER)) return;
159
- if (!id.startsWith("/") || !id.includes("/node_modules/")) return;
160
- const packageName = extractPackageName(id);
160
+ const normalizedId = toSlash(id);
161
+ if (!path.isAbsolute(normalizedId) || !normalizedId.includes("/node_modules/")) return;
162
+ const packageName = extractPackageName(normalizedId);
161
163
  if (!packageName) return;
162
164
  if (excludeSet.has(packageName)) return;
163
- let packageImportPromise = packageImportCache.get(id);
165
+ let packageImportPromise = packageImportCache.get(normalizedId);
164
166
  if (!packageImportPromise) {
165
- packageImportPromise = extractPackageImportSpecifier(id, readPackageJson);
166
- packageImportCache.set(id, packageImportPromise);
167
+ packageImportPromise = extractPackageImportSpecifier(normalizedId, readPackageJson);
168
+ packageImportCache.set(normalizedId, packageImportPromise);
167
169
  }
168
170
  const packageImport = await packageImportPromise;
169
171
  if (!packageImport) return;
@@ -3,7 +3,7 @@ import { markCssUrlAssetReferences, rebaseCssUrlAssetReferences } from "../build
3
3
  import { createRequire } from "node:module";
4
4
  import fs from "node:fs";
5
5
  import { preprocessCSS } from "vite";
6
- import { pathToFileURL } from "node:url";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  //#region src/plugins/sass.ts
8
8
  /**
9
9
  * Map a Next.js `sassOptions` object onto Vite's
@@ -130,8 +130,7 @@ function createSassCssUrlAssetImporter() {
130
130
  if (!fs.statSync(candidate, { throwIfNoEntry: false })?.isFile()) continue;
131
131
  const prepared = prepareStylesheet(candidate, entryDirectory);
132
132
  if (prepared === null) return match;
133
- const canonicalUrl = pathToFileURL(candidate);
134
- markedStylesheets.set(canonicalUrl.href, prepared);
133
+ markedStylesheets.set(candidate, prepared);
135
134
  const namespace = rule === "use" && !/\bas\s+(?:\*|[-\w]+)/.test(suffix) ? ` as ${deriveSassNamespace(importUrl)}` : "";
136
135
  return `${prefix}${quote}${importUrlPrefix}${encodeURIComponent(candidate)}${quote}${namespace}${suffix}`;
137
136
  }
@@ -143,7 +142,7 @@ function createSassCssUrlAssetImporter() {
143
142
  return pathToFileURL(decodeURIComponent(url.slice(21)));
144
143
  },
145
144
  load(canonicalUrl) {
146
- return markedStylesheets.get(canonicalUrl.href) ?? null;
145
+ return markedStylesheets.get(toSlash(fileURLToPath(canonicalUrl))) ?? null;
147
146
  },
148
147
  rewriteImports
149
148
  };