vinext 0.1.3 → 0.1.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 (82) hide show
  1. package/dist/build/client-build-config.d.ts +11 -2
  2. package/dist/build/client-build-config.js +17 -6
  3. package/dist/build/prerender.js +1 -0
  4. package/dist/client/pages-router-link-navigation.d.ts +33 -7
  5. package/dist/client/pages-router-link-navigation.js +32 -2
  6. package/dist/client/vinext-next-data.js +2 -0
  7. package/dist/config/config-matchers.d.ts +11 -1
  8. package/dist/config/config-matchers.js +14 -2
  9. package/dist/config/tsconfig-paths.js +14 -1
  10. package/dist/deploy.js +20 -13
  11. package/dist/entries/app-rsc-entry.js +3 -2
  12. package/dist/entries/pages-client-entry.js +14 -13
  13. package/dist/entries/pages-server-entry.js +6 -26
  14. package/dist/index.js +217 -40
  15. package/dist/plugins/dynamic-preload-metadata.js +2 -4
  16. package/dist/plugins/fonts.js +5 -4
  17. package/dist/plugins/strip-server-exports.d.ts +9 -7
  18. package/dist/plugins/strip-server-exports.js +493 -46
  19. package/dist/routing/app-route-graph.js +2 -2
  20. package/dist/server/app-browser-action-result.js +1 -1
  21. package/dist/server/app-browser-entry.js +8 -1
  22. package/dist/server/app-browser-navigation-controller.d.ts +1 -1
  23. package/dist/server/app-browser-state.d.ts +1 -1
  24. package/dist/server/app-browser-state.js +19 -11
  25. package/dist/server/app-browser-visible-commit.d.ts +1 -1
  26. package/dist/server/app-pages-bridge.d.ts +5 -1
  27. package/dist/server/app-pages-bridge.js +5 -13
  28. package/dist/server/app-rsc-handler.d.ts +3 -0
  29. package/dist/server/app-rsc-handler.js +51 -15
  30. package/dist/server/app-rsc-route-matching.js +6 -2
  31. package/dist/server/app-server-action-execution.js +5 -2
  32. package/dist/server/app-ssr-entry.js +1 -29
  33. package/dist/server/before-interactive-head.d.ts +17 -0
  34. package/dist/server/before-interactive-head.js +35 -0
  35. package/dist/server/csp.js +1 -4
  36. package/dist/server/dev-server.js +81 -36
  37. package/dist/server/middleware-matcher.js +12 -3
  38. package/dist/server/middleware-runtime.d.ts +3 -4
  39. package/dist/server/middleware-runtime.js +2 -0
  40. package/dist/server/navigation-planner.d.ts +3 -12
  41. package/dist/server/navigation-planner.js +24 -0
  42. package/dist/server/navigation-trace.d.ts +2 -1
  43. package/dist/server/navigation-trace.js +1 -0
  44. package/dist/server/operation-token.d.ts +40 -0
  45. package/dist/server/operation-token.js +85 -0
  46. package/dist/server/pages-data-route.d.ts +1 -1
  47. package/dist/server/pages-data-route.js +7 -4
  48. package/dist/server/pages-dev-module-url.d.ts +4 -0
  49. package/dist/server/pages-dev-module-url.js +15 -0
  50. package/dist/server/pages-document-initial-props.d.ts +4 -15
  51. package/dist/server/pages-document-initial-props.js +27 -56
  52. package/dist/server/pages-i18n.js +2 -2
  53. package/dist/server/pages-page-data.js +3 -1
  54. package/dist/server/pages-page-handler.js +3 -1
  55. package/dist/server/pages-page-response.d.ts +2 -0
  56. package/dist/server/pages-page-response.js +4 -4
  57. package/dist/server/pages-readiness.js +1 -1
  58. package/dist/server/pages-request-pipeline.d.ts +7 -7
  59. package/dist/server/pages-request-pipeline.js +63 -21
  60. package/dist/server/prod-server.d.ts +3 -1
  61. package/dist/server/prod-server.js +41 -10
  62. package/dist/server/static-file-cache.js +16 -4
  63. package/dist/shims/before-interactive-context.d.ts +14 -3
  64. package/dist/shims/document.d.ts +15 -20
  65. package/dist/shims/document.js +5 -8
  66. package/dist/shims/image.js +9 -2
  67. package/dist/shims/internal/pages-data-fetch-dedup.d.ts +6 -7
  68. package/dist/shims/internal/pages-data-fetch-dedup.js +67 -14
  69. package/dist/shims/internal/pages-data-target.js +1 -1
  70. package/dist/shims/link.js +37 -16
  71. package/dist/shims/metadata.js +4 -4
  72. package/dist/shims/navigation.js +2 -0
  73. package/dist/shims/router.d.ts +6 -2
  74. package/dist/shims/router.js +99 -20
  75. package/dist/shims/script.js +8 -4
  76. package/dist/utils/has-trailing-comma.d.ts +24 -0
  77. package/dist/utils/has-trailing-comma.js +62 -0
  78. package/dist/utils/text-stream.d.ts +1 -1
  79. package/dist/utils/text-stream.js +2 -2
  80. package/dist/utils/vite-version.d.ts +12 -1
  81. package/dist/utils/vite-version.js +9 -1
  82. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { buildViteResolveExtensions, createValidFileMatcher, findFileWithExts, n
5
5
  import { apiRouter, invalidateRouteCache, matchRoute, pagesRouter } from "./routing/pages-router.js";
6
6
  import { INTERNAL_HEADERS, NEXTJS_DEPLOYMENT_ID_HEADER, VINEXT_INTERNAL_HEADERS, VINEXT_MW_CTX_HEADER, VINEXT_TIMING_HEADER } from "./server/headers.js";
7
7
  import { normalizePath as normalizePath$1 } from "./server/normalize-path.js";
8
- import { proxyExternalRequest } from "./config/config-matchers.js";
8
+ import { matchesRewriteSource, proxyExternalRequest } from "./config/config-matchers.js";
9
9
  import { filterInternalHeaders, isOpenRedirectShaped, normalizeTrailingSlash } from "./server/request-pipeline.js";
10
10
  import { findMiddlewareFile, runMiddleware } from "./server/middleware.js";
11
11
  import { generateServerEntry } from "./entries/pages-server-entry.js";
@@ -31,7 +31,7 @@ import { planRouteClassificationInjection } from "./build/route-classification-i
31
31
  import { PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD } from "./shims/constants.js";
32
32
  import { resolveAssetsDir } from "./utils/asset-prefix.js";
33
33
  import { RESOLVED_VIRTUAL_GOOGLE_FONTS, VIRTUAL_GOOGLE_FONTS, createGoogleFontsPlugin, createLocalFontsPlugin, generateGoogleFontsVirtualModule, parseStaticObjectLiteral } from "./plugins/fonts.js";
34
- import { getViteMajorVersion } from "./utils/vite-version.js";
34
+ import { getDepOptimizeNodeEnvOptions, getViteMajorVersion, serializeViteDefine } from "./utils/vite-version.js";
35
35
  import { createRscCompatibilityId, findNextConfigPath, loadNextConfig, resolveNextConfig, resolveNextConfigInput } from "./config/next-config.js";
36
36
  import { isNextDataPathname, parseNextDataPathname } from "./server/pages-data-route.js";
37
37
  import { precompressAssets } from "./build/precompress.js";
@@ -63,7 +63,7 @@ import { resolvePostcssStringPlugins } from "./plugins/postcss.js";
63
63
  import { buildSassPreprocessorOptions, createSassAwareFileSystemLoader, createSassTildeImporter } from "./plugins/sass.js";
64
64
  import { createClientAssetFileNames, createClientCodeSplittingConfig, createClientFileNameConfig, createClientManualChunks, createClientOutputConfig, createRscFrameworkChunkOutputConfig, getBuildBundlerOptions, getClientTreeshakeConfigForVite, withBuildBundlerOptions } from "./build/client-build-config.js";
65
65
  import { markCssUrlAssetReferences, restoreDedupedCssAssetReferences } from "./build/css-url-assets.js";
66
- import { stripServerExports } from "./plugins/strip-server-exports.js";
66
+ import { hasExportAllCandidate, hasServerExportCandidate, stripServerExports, validatePageExports } from "./plugins/strip-server-exports.js";
67
67
  import { removeConsoleCalls } from "./plugins/remove-console.js";
68
68
  import { createImportMetaUrlPlugin } from "./plugins/import-meta-url.js";
69
69
  import { createRequireContextPlugin } from "./plugins/require-context.js";
@@ -78,7 +78,7 @@ import fs from "node:fs";
78
78
  import path from "node:path";
79
79
  import { loadEnv, parseAst, transformWithOxc } from "vite";
80
80
  import { pathToFileURL } from "node:url";
81
- import { randomBytes, randomUUID } from "node:crypto";
81
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
82
82
  import commonjs from "vite-plugin-commonjs";
83
83
  import MagicString from "magic-string";
84
84
  import tsconfigPaths from "vite-tsconfig-paths";
@@ -162,6 +162,19 @@ function resolveTsconfigPathCandidate(candidate) {
162
162
  for (const item of candidates) if (fs.existsSync(item) && fs.statSync(item).isFile()) return item;
163
163
  return null;
164
164
  }
165
+ /**
166
+ * Normalize a tsconfig `extends` field into a list of specifier strings.
167
+ *
168
+ * TypeScript 5.0+ allows `extends` to be either a string or an array of
169
+ * strings. Matches Next.js's handling in
170
+ * packages/next/src/build/next-config-ts/transpile-config.ts, where parents
171
+ * are iterated in order and later entries override earlier ones.
172
+ */
173
+ function normalizeTsconfigExtends(extendsField) {
174
+ if (typeof extendsField === "string") return [extendsField];
175
+ if (Array.isArray(extendsField)) return extendsField.filter((value) => typeof value === "string");
176
+ return [];
177
+ }
165
178
  function resolveTsconfigExtends(configPath, specifier) {
166
179
  const fromDir = path.dirname(configPath);
167
180
  if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("\\")) return resolveTsconfigPathCandidate(path.resolve(fromDir, specifier));
@@ -221,9 +234,12 @@ function loadTsconfigPathAliases(configPath, projectRoot, seen = /* @__PURE__ */
221
234
  }
222
235
  if (!parsed) return {};
223
236
  let aliases = {};
224
- if (typeof parsed.extends === "string") {
225
- const extendedPath = resolveTsconfigExtends(normalizedPath, parsed.extends);
226
- if (extendedPath) aliases = loadTsconfigPathAliases(extendedPath, projectRoot, seen);
237
+ for (const extendsSpecifier of normalizeTsconfigExtends(parsed.extends)) {
238
+ const extendedPath = resolveTsconfigExtends(normalizedPath, extendsSpecifier);
239
+ if (extendedPath) aliases = {
240
+ ...aliases,
241
+ ...loadTsconfigPathAliases(extendedPath, projectRoot, seen)
242
+ };
227
243
  }
228
244
  const compilerOptions = isUnknownRecord(parsed.compilerOptions) ? parsed.compilerOptions : null;
229
245
  const pathsConfig = compilerOptions && isUnknownRecord(compilerOptions.paths) ? compilerOptions.paths : null;
@@ -288,6 +304,14 @@ const RESOLVED_INSTRUMENTATION_CLIENT = `\0${VIRTUAL_INSTRUMENTATION_CLIENT}.mjs
288
304
  /** Image file extensions handled by the vinext:image-imports plugin.
289
305
  * Shared between the Rolldown hook filter and the transform handler regex. */
290
306
  const IMAGE_EXTS = "png|jpe?g|gif|webp|avif|svg|ico|bmp|tiff?";
307
+ function createStaticImageAsset(imagePath) {
308
+ const source = fs.readFileSync(imagePath);
309
+ const extension = path.extname(imagePath);
310
+ return {
311
+ fileName: `media/${path.basename(imagePath, extension)}.${createHash("sha256").update(source).digest("hex").slice(0, 8)}${extension}`,
312
+ source
313
+ };
314
+ }
291
315
  /**
292
316
  * Absolute path to vinext's shims directory, with a trailing slash. Normalized
293
317
  * to forward slashes because it is prefix-matched against Vite module ids (which
@@ -394,6 +418,7 @@ function vinext(options = {}) {
394
418
  const viteMajorVersion = getViteMajorVersion();
395
419
  let root;
396
420
  let pagesDir;
421
+ let canonicalPagesDir;
397
422
  let appDir;
398
423
  let hasAppDir = false;
399
424
  let hasPagesDir = false;
@@ -413,6 +438,19 @@ function vinext(options = {}) {
413
438
  let rscClassificationManifest = null;
414
439
  const shimsDir = path.resolve(__dirname, "shims");
415
440
  const canonicalize = (p) => tryRealpathSync(p) ?? p;
441
+ const pageTransformCanonicalPaths = /* @__PURE__ */ new Map();
442
+ const canonicalizePageTransformPath = (modulePath) => {
443
+ const cached = pageTransformCanonicalPaths.get(modulePath);
444
+ if (cached) return cached;
445
+ const canonicalPath = canonicalize(modulePath);
446
+ pageTransformCanonicalPaths.set(modulePath, canonicalPath);
447
+ return canonicalPath;
448
+ };
449
+ const isWithinPagesDirectory = (modulePath) => modulePath === pagesDir || modulePath.startsWith(`${pagesDir}/`) || modulePath === canonicalPagesDir || modulePath.startsWith(`${canonicalPagesDir}/`);
450
+ const isApiPage = (canonicalId) => {
451
+ const relativePath = fileMatcher.stripExtension(canonicalId.slice(canonicalPagesDir.length));
452
+ return relativePath === "/api" || relativePath.startsWith("/api/");
453
+ };
416
454
  const dynamicShimPaths = new Set([
417
455
  resolveShimModulePath(shimsDir, "headers"),
418
456
  resolveShimModulePath(shimsDir, "server"),
@@ -481,6 +519,9 @@ function vinext(options = {}) {
481
519
  });
482
520
  }
483
521
  const imageImportDimCache = /* @__PURE__ */ new Map();
522
+ const staticImageAssets = /* @__PURE__ */ new Map();
523
+ const staticImageImportsByModule = /* @__PURE__ */ new Map();
524
+ const writtenStaticImageFiles = /* @__PURE__ */ new Set();
484
525
  let mdxDelegate = null;
485
526
  let mdxDelegatePromise = null;
486
527
  let hasUserMdxPlugin = false;
@@ -572,6 +613,7 @@ function vinext(options = {}) {
572
613
  else baseDir = root;
573
614
  }
574
615
  pagesDir = path.posix.join(baseDir, "pages");
616
+ canonicalPagesDir = canonicalize(pagesDir);
575
617
  appDir = path.posix.join(baseDir, "app");
576
618
  hasPagesDir = fs.existsSync(pagesDir);
577
619
  hasAppDir = !options.disableAppRouter && fs.existsSync(appDir);
@@ -605,7 +647,10 @@ function vinext(options = {}) {
605
647
  clientInjectModule = instrumentationClientInjects.length ? generateInstrumentationClientInjectModule(instrumentationClientInjects, instrumentationClientPath, INSTRUMENTATION_CLIENT_EMPTY_MODULE) : null;
606
648
  if (env?.command === "build") await writeRouteTypes();
607
649
  const defines = getNextPublicEnvDefines();
608
- if (!config.define || typeof config.define !== "object" || !("process.env.NODE_ENV" in config.define)) defines["process.env.NODE_ENV"] = JSON.stringify(resolvedNodeEnv);
650
+ const userNodeEnvDefine = config.define?.["process.env.NODE_ENV"];
651
+ const hasUserNodeEnvDefine = Object.hasOwn(config.define ?? {}, "process.env.NODE_ENV");
652
+ const nodeEnvDefine = hasUserNodeEnvDefine ? serializeViteDefine(userNodeEnvDefine) : JSON.stringify(resolvedNodeEnv);
653
+ if (!hasUserNodeEnvDefine) defines["process.env.NODE_ENV"] = nodeEnvDefine;
609
654
  for (const [key, value] of Object.entries(nextConfig.env)) {
610
655
  if (key === "NODE_ENV") continue;
611
656
  defines[`process.env.${key}`] = JSON.stringify(value);
@@ -836,10 +881,15 @@ function vinext(options = {}) {
836
881
  if (shimBase !== void 0) return resolveShimModulePath(shimsDir, shimBase);
837
882
  }
838
883
  };
884
+ const depOptimizeNodeEnvOptions = getDepOptimizeNodeEnvOptions(viteMajorVersion, nodeEnvDefine);
839
885
  viteConfig.optimizeDeps = {
840
886
  exclude: mergeOptimizeDepsExclude(incomingExclude, VINEXT_OPTIMIZE_DEPS_EXCLUDE, ["@tailwindcss/oxide"]),
841
887
  ...incomingInclude.length > 0 ? { include: incomingInclude } : {},
842
- rolldownOptions: { plugins: [depOptimizeAliasPlugin] }
888
+ ...depOptimizeNodeEnvOptions,
889
+ rolldownOptions: {
890
+ ...depOptimizeNodeEnvOptions.rolldownOptions,
891
+ plugins: [depOptimizeAliasPlugin]
892
+ }
843
893
  };
844
894
  const pagesOptimizeEntries = !hasAppDir ? [...hasPagesDir ? [toRelativeFileEntry(root, pagesDir) + "/**/*.{tsx,ts,jsx,js}"] : [], ...[instrumentationPath, instrumentationClientPath].flatMap((entry) => entry ? [toRelativeFileEntry(root, entry)] : [])] : [];
845
895
  if (hasAppDir) {
@@ -861,7 +911,9 @@ function vinext(options = {}) {
861
911
  } },
862
912
  optimizeDeps: {
863
913
  exclude: mergeOptimizeDepsExclude(incomingExclude, VINEXT_OPTIMIZE_DEPS_EXCLUDE),
864
- entries: optimizeEntries
914
+ entries: optimizeEntries,
915
+ include: [...new Set([...incomingInclude, "react-server-dom-webpack/static.edge"])],
916
+ ...depOptimizeNodeEnvOptions
865
917
  },
866
918
  build: {
867
919
  outDir: options.rscOutDir ?? "dist/server",
@@ -878,7 +930,8 @@ function vinext(options = {}) {
878
930
  } },
879
931
  optimizeDeps: {
880
932
  exclude: mergeOptimizeDepsExclude(incomingExclude, VINEXT_OPTIMIZE_DEPS_EXCLUDE, ["ipaddr.js"], userSsrExternal === true ? SSR_EXTERNAL_REACT_ENTRIES : []),
881
- entries: optimizeEntries
933
+ entries: optimizeEntries,
934
+ ...depOptimizeNodeEnvOptions
882
935
  },
883
936
  build: {
884
937
  outDir: options.ssrOutDir ?? "dist/server/ssr",
@@ -951,7 +1004,10 @@ function vinext(options = {}) {
951
1004
  ],
952
1005
  noExternal: true
953
1006
  },
954
- optimizeDeps: { exclude: ["ipaddr.js"] },
1007
+ optimizeDeps: {
1008
+ exclude: ["ipaddr.js"],
1009
+ ...depOptimizeNodeEnvOptions
1010
+ },
955
1011
  build: {
956
1012
  outDir: "dist/server",
957
1013
  ...withBuildBundlerOptions(viteMajorVersion, {
@@ -1435,6 +1491,51 @@ function vinext(options = {}) {
1435
1491
  });
1436
1492
  installDevStackSourcemapMiddleware(server);
1437
1493
  return () => {
1494
+ const viteFilesystemMiddlewares = server.middlewares.stack.filter(({ handle }) => {
1495
+ const name = typeof handle === "function" ? handle.name : "";
1496
+ return name === "viteServePublicMiddleware" || name === "viteServeStaticMiddleware";
1497
+ }).map(({ handle }) => handle).filter((handle) => typeof handle === "function");
1498
+ const serveRewrittenViteFilesystemRoute = async (req, res, requestPathname, stagedHeaders) => {
1499
+ const originalUrl = req.url;
1500
+ const originalStatusCode = res.statusCode;
1501
+ const originalStatusMessage = res.statusMessage;
1502
+ const originalHeaders = res.getHeaders();
1503
+ req.url = requestPathname;
1504
+ for (const [key, value] of Object.entries(stagedHeaders)) res.setHeader(key, value);
1505
+ const restore = () => {
1506
+ req.url = originalUrl;
1507
+ res.statusCode = originalStatusCode;
1508
+ res.statusMessage = originalStatusMessage;
1509
+ for (const key of Object.keys(res.getHeaders())) res.removeHeader(key);
1510
+ for (const [key, value] of Object.entries(originalHeaders)) if (value !== void 0) res.setHeader(key, value);
1511
+ };
1512
+ try {
1513
+ for (const middleware of viteFilesystemMiddlewares) if (await new Promise((resolve, reject) => {
1514
+ let settled = false;
1515
+ const settle = (value, error) => {
1516
+ if (settled) return;
1517
+ settled = true;
1518
+ res.off("finish", onServed);
1519
+ res.off("close", onServed);
1520
+ if (error) reject(error);
1521
+ else resolve(value);
1522
+ };
1523
+ const onServed = () => settle("served");
1524
+ res.once("finish", onServed);
1525
+ res.once("close", onServed);
1526
+ middleware(req, res, (error) => settle("next", error));
1527
+ if (res.writableEnded) settle("served");
1528
+ }) === "served") {
1529
+ req.url = originalUrl;
1530
+ return true;
1531
+ }
1532
+ } catch (error) {
1533
+ restore();
1534
+ throw error;
1535
+ }
1536
+ restore();
1537
+ return false;
1538
+ };
1438
1539
  if (instrumentationPath && !hasAppDir) runInstrumentation(getPagesRunner(), instrumentationPath).catch((err) => {
1439
1540
  console.error("[vinext] Instrumentation error:", err);
1440
1541
  });
@@ -1556,6 +1657,7 @@ function vinext(options = {}) {
1556
1657
  return;
1557
1658
  }
1558
1659
  }
1660
+ if (hasCloudflarePlugin) return next();
1559
1661
  let isDataReq = false;
1560
1662
  if (isNextDataPathname(pathname)) {
1561
1663
  const devBuildId = nextConfig?.buildId ?? process.env.__VINEXT_BUILD_ID ?? "development";
@@ -1575,10 +1677,17 @@ function vinext(options = {}) {
1575
1677
  return;
1576
1678
  }
1577
1679
  }
1578
- if (pathname.includes(".") && !pathname.endsWith(".html")) return next();
1579
- if (hasCloudflarePlugin) return next();
1680
+ const filePathMatchesRewrite = [
1681
+ ...nextConfig?.rewrites.beforeFiles ?? [],
1682
+ ...nextConfig?.rewrites.afterFiles ?? [],
1683
+ ...nextConfig?.rewrites.fallback ?? []
1684
+ ].some((rewrite) => matchesRewriteSource(pathname, rewrite, {
1685
+ basePath: bp,
1686
+ hadBasePath: true
1687
+ }));
1688
+ if (pathname.includes(".") && !pathname.endsWith(".html") && !filePathMatchesRewrite) return next();
1580
1689
  const rawHeaders = new Headers(Object.fromEntries(Object.entries(req.headers).filter(([k, v]) => v !== void 0 && !k.startsWith(":")).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v)])));
1581
- const isDataRequest = rawHeaders.get("x-nextjs-data") === "1";
1690
+ const isDataRequest = isDataReq;
1582
1691
  const nodeRequestHeaders = filterInternalHeaders(rawHeaders);
1583
1692
  for (const header of INTERNAL_HEADERS) delete req.headers[header];
1584
1693
  for (const header of VINEXT_INTERNAL_HEADERS) delete req.headers[header];
@@ -1649,6 +1758,10 @@ function vinext(options = {}) {
1649
1758
  externalInit.duplex = "half";
1650
1759
  }
1651
1760
  return proxyExternalRequest(new Request(new URL(url, requestOrigin), externalInit), externalUrl);
1761
+ },
1762
+ serveFilesystemRoute: async (requestPathname, stagedHeaders, phase) => {
1763
+ if (phase === "direct" || req.method !== "GET" && req.method !== "HEAD" || requestPathname === "/" || requestPathname === "/api" || requestPathname.startsWith("/api/")) return false;
1764
+ return serveRewrittenViteFilesystemRoute(req, res, requestPathname, stagedHeaders);
1652
1765
  }
1653
1766
  });
1654
1767
  if (pipelineResult.type === "response") {
@@ -1713,6 +1826,42 @@ function vinext(options = {}) {
1713
1826
  };
1714
1827
  }
1715
1828
  },
1829
+ {
1830
+ name: "vinext:validate-page-exports",
1831
+ transform: { handler(code, id) {
1832
+ if (this.environment?.name !== "client") return null;
1833
+ if (!hasPagesDir || id.startsWith("\0") || !hasExportAllCandidate(code)) return null;
1834
+ const modulePath = stripViteModuleQuery(id);
1835
+ if (!isWithinPagesDirectory(modulePath)) return null;
1836
+ const canonicalId = canonicalizePageTransformPath(modulePath);
1837
+ if (!isWithinPagesDirectory(canonicalId)) return null;
1838
+ if (!fileMatcher.isPageFile(canonicalId)) return null;
1839
+ if (isApiPage(canonicalId)) return null;
1840
+ validatePageExports(code);
1841
+ return null;
1842
+ } }
1843
+ },
1844
+ {
1845
+ name: "vinext:strip-server-exports",
1846
+ transform: { handler(code, id) {
1847
+ if (this.environment?.name !== "client") return null;
1848
+ if (!hasPagesDir || id.startsWith("\0") || !hasServerExportCandidate(code)) return null;
1849
+ const modulePath = stripViteModuleQuery(id);
1850
+ if (!isWithinPagesDirectory(modulePath)) return null;
1851
+ const canonicalId = canonicalizePageTransformPath(modulePath);
1852
+ if (!isWithinPagesDirectory(canonicalId)) return null;
1853
+ if (!fileMatcher.isPageFile(canonicalId)) return null;
1854
+ const relativePath = canonicalId.slice(canonicalPagesDir.length);
1855
+ if (isApiPage(canonicalId)) return null;
1856
+ if (/^\/(?:_app|_document|_error)(?:\.[^/]*)?$/.test(relativePath)) return null;
1857
+ const result = stripServerExports(code);
1858
+ if (!result) return null;
1859
+ return {
1860
+ code: result,
1861
+ map: null
1862
+ };
1863
+ } }
1864
+ },
1716
1865
  {
1717
1866
  name: "vinext:validate-server-only-client-imports",
1718
1867
  transform: {
@@ -1729,26 +1878,6 @@ function vinext(options = {}) {
1729
1878
  }
1730
1879
  }
1731
1880
  },
1732
- {
1733
- name: "vinext:strip-server-exports",
1734
- transform: {
1735
- filter: { id: /\.(tsx?|jsx?|mjs)$/ },
1736
- handler(code, id) {
1737
- if (this.environment?.name !== "client") return null;
1738
- if (!hasPagesDir) return null;
1739
- if (!id.startsWith(pagesDir)) return null;
1740
- const relativePath = id.slice(pagesDir.length);
1741
- if (relativePath.startsWith("/api/") || relativePath === "/api") return null;
1742
- if (/\/_(?:app|document|error)\b/.test(relativePath)) return null;
1743
- const result = stripServerExports(code);
1744
- if (!result) return null;
1745
- return {
1746
- code: result,
1747
- map: null
1748
- };
1749
- }
1750
- }
1751
- },
1752
1881
  {
1753
1882
  name: "vinext:remove-console",
1754
1883
  apply: "build",
@@ -1792,16 +1921,36 @@ function vinext(options = {}) {
1792
1921
  name: "vinext:image-imports",
1793
1922
  enforce: "pre",
1794
1923
  _dimCache: imageImportDimCache,
1924
+ buildStart() {
1925
+ imageImportDimCache.clear();
1926
+ staticImageAssets.clear();
1927
+ },
1928
+ watchChange(id) {
1929
+ imageImportDimCache.delete(id);
1930
+ staticImageAssets.delete(id);
1931
+ staticImageImportsByModule.delete(id);
1932
+ },
1795
1933
  resolveId: {
1796
- filter: { id: /\?vinext-meta$/ },
1934
+ filter: { id: /\?vinext-(?:image-url|meta)$/ },
1797
1935
  handler(source, _importer) {
1798
- if (!source.endsWith("?vinext-meta")) return null;
1799
- return `\0vinext-image-meta:${source.replace("?vinext-meta", "")}`;
1936
+ if (source.endsWith("?vinext-image-url")) return `\0vinext-image-url:${source.slice(0, -17)}`;
1937
+ if (source.endsWith("?vinext-meta")) return `\0vinext-image-meta:${source.slice(0, -12)}`;
1938
+ return null;
1800
1939
  }
1801
1940
  },
1802
1941
  async load(id) {
1942
+ if (id.startsWith("\0vinext-image-url:")) {
1943
+ const imagePath = id.replace("\0vinext-image-url:", "");
1944
+ this.addWatchFile(imagePath);
1945
+ if (this.environment.config.command === "serve") return `import url from ${JSON.stringify(imagePath + "?url")}; export default url;`;
1946
+ const asset = createStaticImageAsset(imagePath);
1947
+ staticImageAssets.set(imagePath, asset);
1948
+ const builtFileName = `${resolveAssetsDir(nextConfig.assetPrefix)}/${asset.fileName}`;
1949
+ return `export default ${JSON.stringify(renderVinextBuiltUrl(builtFileName, nextConfig.assetPrefix, nextConfig.deploymentId))};`;
1950
+ }
1803
1951
  if (!id.startsWith("\0vinext-image-meta:")) return null;
1804
1952
  const imagePath = id.replace("\0vinext-image-meta:", "");
1953
+ this.addWatchFile(imagePath);
1805
1954
  const cache = imageImportDimCache;
1806
1955
  let dims = cache.get(imagePath);
1807
1956
  if (!dims) try {
@@ -1842,6 +1991,7 @@ function vinext(options = {}) {
1842
1991
  }
1843
1992
  const s = new MagicString(code);
1844
1993
  let hasChanges = false;
1994
+ const imageImports = /* @__PURE__ */ new Set();
1845
1995
  for (const node of ast.body) {
1846
1996
  if (node.type !== "ImportDeclaration") continue;
1847
1997
  const importNode = node;
@@ -1857,18 +2007,45 @@ function vinext(options = {}) {
1857
2007
  const dir = path.dirname(id);
1858
2008
  const absImagePath = normalizePathSeparators(path.resolve(dir, importPath));
1859
2009
  if (!fs.existsSync(absImagePath)) continue;
2010
+ imageImports.add(absImagePath);
1860
2011
  const urlVar = `__vinext_img_url_${varName}`;
1861
2012
  const metaVar = `__vinext_img_meta_${varName}`;
1862
- const replacement = `import ${urlVar} from ${JSON.stringify(importPath)};\nimport ${metaVar} from ${JSON.stringify(absImagePath + "?vinext-meta")};\nconst ${varName} = { src: ${urlVar}, width: ${metaVar}.width, height: ${metaVar}.height };`;
2013
+ const replacement = `import ${urlVar} from ${JSON.stringify(absImagePath + "?vinext-image-url")};\nimport ${metaVar} from ${JSON.stringify(absImagePath + "?vinext-meta")};\nconst ${varName} = { src: ${urlVar}, width: ${metaVar}.width, height: ${metaVar}.height };`;
1863
2014
  s.overwrite(importNode.start, importNode.end, replacement);
1864
2015
  hasChanges = true;
1865
2016
  }
1866
- if (!hasChanges) return null;
2017
+ if (!hasChanges) {
2018
+ staticImageImportsByModule.delete(id);
2019
+ return null;
2020
+ }
2021
+ staticImageImportsByModule.set(id, imageImports);
1867
2022
  return {
1868
2023
  code: s.toString(),
1869
2024
  map: s.generateMap({ hires: "boundary" })
1870
2025
  };
1871
2026
  }
2027
+ },
2028
+ writeBundle: {
2029
+ sequential: true,
2030
+ order: "post",
2031
+ handler(outputOptions) {
2032
+ if (this.environment?.name !== "client") return;
2033
+ const clientOutDir = outputOptions.dir ? path.resolve(root, outputOptions.dir) : path.resolve(root, options.clientOutDir ?? "dist/client");
2034
+ const assetsDir = resolveAssetsDir(nextConfig.assetPrefix);
2035
+ const activeImagePaths = new Set(Array.from(staticImageImportsByModule.values()).flatMap((imports) => [...imports]));
2036
+ const nextWrittenFiles = /* @__PURE__ */ new Set();
2037
+ for (const imagePath of activeImagePaths) {
2038
+ if (!fs.existsSync(imagePath)) continue;
2039
+ const asset = staticImageAssets.get(imagePath) ?? createStaticImageAsset(imagePath);
2040
+ const outputPath = path.join(clientOutDir, assetsDir, asset.fileName);
2041
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
2042
+ fs.writeFileSync(outputPath, asset.source);
2043
+ nextWrittenFiles.add(outputPath);
2044
+ }
2045
+ for (const outputPath of writtenStaticImageFiles) if (!nextWrittenFiles.has(outputPath)) fs.rmSync(outputPath, { force: true });
2046
+ writtenStaticImageFiles.clear();
2047
+ for (const outputPath of nextWrittenFiles) writtenStaticImageFiles.add(outputPath);
2048
+ }
1872
2049
  }
1873
2050
  },
1874
2051
  createGoogleFontsPlugin(_fontGoogleShimPath, _shimsDir),
@@ -1,4 +1,5 @@
1
1
  import { isUnknownRecord } from "../utils/record.js";
2
+ import { hasTrailingComma } from "../utils/has-trailing-comma.js";
2
3
  import { relativeWithinRoot, tryRealpathSync } from "../build/ssr-manifest.js";
3
4
  import path from "node:path";
4
5
  import { parseAst } from "vite";
@@ -281,15 +282,12 @@ function appendObjectProperty(output, objectNode, property) {
281
282
  output.appendLeft(propertyEnd, `, ${property}`);
282
283
  return true;
283
284
  }
284
- function stripComments(source) {
285
- return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
286
- }
287
285
  function insertSecondOptionsArgument(output, code, callNode, firstArg, optionsLiteral) {
288
286
  const callEnd = getNumber(callNode, "end");
289
287
  const firstArgEnd = getNumber(firstArg, "end");
290
288
  if (callEnd === null || firstArgEnd === null) return false;
291
289
  const closeParen = callEnd - 1;
292
- const separator = stripComments(code.slice(firstArgEnd, closeParen)).trimEnd().endsWith(",") ? " " : ", ";
290
+ const separator = hasTrailingComma(code.slice(firstArgEnd, closeParen)) ? " " : ", ";
293
291
  output.appendLeft(closeParen, `${separator}${optionsLiteral}`);
294
292
  return true;
295
293
  }
@@ -1,4 +1,5 @@
1
1
  import { escapeRegExp } from "../utils/regex.js";
2
+ import { lastSignificantChar } from "../utils/has-trailing-comma.js";
2
3
  import { buildFallbackFontFace, getFallbackFontOverrideMetrics } from "../build/google-fonts/fallback-metrics.js";
3
4
  import { validateGoogleFontOptions } from "../build/google-fonts/validate.js";
4
5
  import { getFontAxes } from "../build/google-fonts/get-axes.js";
@@ -552,8 +553,8 @@ function createGoogleFontsPlugin(fontGoogleShimPath, shimsDir) {
552
553
  if (validatedFontStyle) internalFontProperties.push(`fontStyle: ${JSON.stringify(validatedFontStyle)}`);
553
554
  const injectedProperties = [`_vinext: { font: { ${internalFontProperties.join(", ")} } }`];
554
555
  const closingBrace = optionsStr.lastIndexOf("}");
555
- const beforeBrace = optionsStr.slice(0, closingBrace).trim();
556
- const separator = beforeBrace.endsWith("{") || beforeBrace.endsWith(",") ? "" : ", ";
556
+ const lastChar = lastSignificantChar(optionsStr.slice(0, closingBrace));
557
+ const separator = lastChar === "{" || lastChar === "," ? "" : ", ";
557
558
  const replacement = `${calleeSource}(${optionsStr.slice(0, closingBrace) + separator + injectedProperties.join(", ") + optionsStr.slice(closingBrace)})`;
558
559
  s.overwrite(callStart, callEnd, replacement);
559
560
  overwrittenRanges.push([callStart, callEnd]);
@@ -688,8 +689,8 @@ function createLocalFontsPlugin(shimsDir) {
688
689
  if (familyPayloadInsertions.has(insertAt)) continue;
689
690
  const optionsStr = code.slice(objRange[0], objRange[1]);
690
691
  if (/(?:^|[,{])\s*_vinext\s*:/.test(optionsStr)) continue;
691
- const beforeClosingBrace = optionsStr.slice(0, -1).trim();
692
- const separator = beforeClosingBrace.endsWith("{") || beforeClosingBrace.endsWith(",") ? "" : ", ";
692
+ const lastChar = lastSignificantChar(optionsStr.slice(0, -1));
693
+ const separator = lastChar === "{" || lastChar === "," ? "" : ", ";
693
694
  s.appendLeft(insertAt, `${separator}_vinext: { font: { family: ${JSON.stringify(bindingName)} } }`);
694
695
  familyPayloadInsertions.add(insertAt);
695
696
  hasChanges = true;
@@ -1,13 +1,15 @@
1
1
  //#region src/plugins/strip-server-exports.d.ts
2
+ declare function hasServerExportCandidate(code: string): boolean;
3
+ declare function hasExportAllCandidate(code: string): boolean;
4
+ declare function validatePageExports(code: string): void;
2
5
  /**
3
- * Strip server-only data-fetching exports (getServerSideProps,
4
- * getStaticProps, getStaticPaths) from page modules for the client
5
- * bundle. Uses Vite's parseAst (Rollup/acorn) for correct handling
6
- * of all export patterns including function expressions, arrow
7
- * functions with TS return types, and re-exports.
6
+ * Strip server-only Pages Router data-fetching exports and their unique
7
+ * dependency graph from browser bundles.
8
8
  *
9
- * Modeled after Next.js's SWC `next-ssg-transform`.
9
+ * Ported from Next.js:
10
+ * - test/unit/babel-plugin-next-ssg-transform.test.ts
11
+ * - crates/next-custom-transforms/src/transforms/strip_page_exports.rs
10
12
  */
11
13
  declare function stripServerExports(code: string): string | null;
12
14
  //#endregion
13
- export { stripServerExports };
15
+ export { hasExportAllCandidate, hasServerExportCandidate, stripServerExports, validatePageExports };