vinext 0.0.22 → 0.0.23

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 (45) hide show
  1. package/dist/build/static-export.d.ts.map +1 -1
  2. package/dist/build/static-export.js +9 -7
  3. package/dist/build/static-export.js.map +1 -1
  4. package/dist/config/next-config.d.ts +4 -1
  5. package/dist/config/next-config.d.ts.map +1 -1
  6. package/dist/config/next-config.js +10 -5
  7. package/dist/config/next-config.js.map +1 -1
  8. package/dist/deploy.d.ts.map +1 -1
  9. package/dist/deploy.js +11 -1
  10. package/dist/deploy.js.map +1 -1
  11. package/dist/index.d.ts +25 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +202 -28
  14. package/dist/index.js.map +1 -1
  15. package/dist/routing/app-router.d.ts +2 -1
  16. package/dist/routing/app-router.d.ts.map +1 -1
  17. package/dist/routing/app-router.js +73 -66
  18. package/dist/routing/app-router.js.map +1 -1
  19. package/dist/routing/file-matcher.d.ts +24 -0
  20. package/dist/routing/file-matcher.d.ts.map +1 -0
  21. package/dist/routing/file-matcher.js +75 -0
  22. package/dist/routing/file-matcher.js.map +1 -0
  23. package/dist/routing/pages-router.d.ts +3 -2
  24. package/dist/routing/pages-router.d.ts.map +1 -1
  25. package/dist/routing/pages-router.js +24 -17
  26. package/dist/routing/pages-router.js.map +1 -1
  27. package/dist/server/app-dev-server.d.ts.map +1 -1
  28. package/dist/server/app-dev-server.js +54 -45
  29. package/dist/server/app-dev-server.js.map +1 -1
  30. package/dist/server/dev-server.d.ts +2 -1
  31. package/dist/server/dev-server.d.ts.map +1 -1
  32. package/dist/server/dev-server.js +16 -14
  33. package/dist/server/dev-server.js.map +1 -1
  34. package/dist/server/prod-server.d.ts.map +1 -1
  35. package/dist/server/prod-server.js +26 -2
  36. package/dist/server/prod-server.js.map +1 -1
  37. package/dist/shims/cache.d.ts +1 -1
  38. package/dist/shims/cache.d.ts.map +1 -1
  39. package/dist/shims/cache.js +8 -3
  40. package/dist/shims/cache.js.map +1 -1
  41. package/dist/shims/metadata.d.ts +1 -0
  42. package/dist/shims/metadata.d.ts.map +1 -1
  43. package/dist/shims/metadata.js +5 -1
  44. package/dist/shims/metadata.js.map +1 -1
  45. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { loadEnv, parseAst } from "vite";
2
2
  import { pagesRouter, apiRouter, invalidateRouteCache, matchRoute, patternToNextFormat as pagesPatternToNextFormat } from "./routing/pages-router.js";
3
3
  import { appRouter, invalidateAppRouteCache } from "./routing/app-router.js";
4
+ import { createValidFileMatcher } from "./routing/file-matcher.js";
4
5
  import { createSSRHandler } from "./server/dev-server.js";
5
6
  import { handleApiRoute } from "./server/api-handler.js";
6
7
  import { generateRscEntry, generateSsrEntry, generateBrowserEntry, } from "./server/app-dev-server.js";
@@ -10,6 +11,7 @@ import { logRequest, now } from "./server/request-log.js";
10
11
  import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode } from "./server/middleware-codegen.js";
11
12
  import { normalizePath } from "./server/normalize-path.js";
12
13
  import { findInstrumentationFile, runInstrumentation } from "./server/instrumentation.js";
14
+ import { PHASE_PRODUCTION_BUILD, PHASE_DEVELOPMENT_SERVER } from "./shims/constants.js";
13
15
  import { validateDevRequest } from "./server/dev-origin-check.js";
14
16
  import { safeRegExp, isExternalUrl, proxyExternalRequest, parseCookies, matchHeaders, matchRedirect, matchRewrite, } from "./config/config-matchers.js";
15
17
  import { scanMetadataFiles } from "./server/metadata-routes.js";
@@ -493,6 +495,7 @@ export default function vinext(options = {}) {
493
495
  let hasAppDir = false;
494
496
  let hasPagesDir = false;
495
497
  let nextConfig;
498
+ let fileMatcher;
496
499
  let middlewarePath = null;
497
500
  let instrumentationPath = null;
498
501
  let hasCloudflarePlugin = false;
@@ -506,8 +509,8 @@ export default function vinext(options = {}) {
506
509
  * This is the entry point for `vite build --ssr`.
507
510
  */
508
511
  async function generateServerEntry() {
509
- const pageRoutes = await pagesRouter(pagesDir);
510
- const apiRoutes = await apiRouter(pagesDir);
512
+ const pageRoutes = await pagesRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
513
+ const apiRoutes = await apiRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
511
514
  // Generate import statements using absolute paths since virtual
512
515
  // modules don't have a real file location for relative resolution.
513
516
  const pageImports = pageRoutes.map((r, i) => {
@@ -527,16 +530,15 @@ export default function vinext(options = {}) {
527
530
  return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`;
528
531
  });
529
532
  // Check for _app and _document
530
- const hasApp = fs.existsSync(path.join(pagesDir, "_app.tsx")) || fs.existsSync(path.join(pagesDir, "_app.jsx")) || fs.existsSync(path.join(pagesDir, "_app.ts")) || fs.existsSync(path.join(pagesDir, "_app.js"));
531
- const hasDoc = fs.existsSync(path.join(pagesDir, "_document.tsx")) || fs.existsSync(path.join(pagesDir, "_document.jsx")) || fs.existsSync(path.join(pagesDir, "_document.ts")) || fs.existsSync(path.join(pagesDir, "_document.js"));
532
- // Use absolute paths for _app and _document too
533
- const appFileBase = path.join(pagesDir, "_app").replace(/\\/g, "/");
534
- const docFileBase = path.join(pagesDir, "_document").replace(/\\/g, "/");
533
+ const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
534
+ const docFilePath = findFileWithExts(pagesDir, "_document", fileMatcher);
535
+ const hasApp = appFilePath !== null;
536
+ const hasDoc = docFilePath !== null;
535
537
  const appImportCode = hasApp
536
- ? `import { default as AppComponent } from ${JSON.stringify(appFileBase)};`
538
+ ? `import { default as AppComponent } from ${JSON.stringify(appFilePath.replace(/\\/g, "/"))};`
537
539
  : `const AppComponent = null;`;
538
540
  const docImportCode = hasDoc
539
- ? `import { default as DocumentComponent } from ${JSON.stringify(docFileBase)};`
541
+ ? `import { default as DocumentComponent } from ${JSON.stringify(docFilePath.replace(/\\/g, "/"))};`
540
542
  : `const DocumentComponent = null;`;
541
543
  // Serialize i18n config for embedding in the server entry
542
544
  const i18nConfigJson = nextConfig?.i18n
@@ -1460,8 +1462,9 @@ ${middlewareExportCode}
1460
1462
  * __NEXT_DATA__ to determine which page to hydrate.
1461
1463
  */
1462
1464
  async function generateClientEntry() {
1463
- const pageRoutes = await pagesRouter(pagesDir);
1464
- const hasApp = fs.existsSync(path.join(pagesDir, "_app.tsx")) || fs.existsSync(path.join(pagesDir, "_app.jsx")) || fs.existsSync(path.join(pagesDir, "_app.ts")) || fs.existsSync(path.join(pagesDir, "_app.js"));
1465
+ const pageRoutes = await pagesRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
1466
+ const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
1467
+ const hasApp = appFilePath !== null;
1465
1468
  // Build a map of route pattern -> dynamic import.
1466
1469
  // Keys must use Next.js bracket format (e.g. "/user/[id]") to match
1467
1470
  // __NEXT_DATA__.page which is set via patternToNextFormat() during SSR.
@@ -1473,7 +1476,7 @@ ${middlewareExportCode}
1473
1476
  // lgtm[js/bad-code-sanitization]
1474
1477
  return ` ${JSON.stringify(nextFormatPattern)}: () => import(${JSON.stringify(absPath)})`;
1475
1478
  });
1476
- const appFileBase = path.join(pagesDir, "_app").replace(/\\/g, "/");
1479
+ const appFileBase = appFilePath?.replace(/\\/g, "/");
1477
1480
  return `
1478
1481
  import React from "react";
1479
1482
  import { hydrateRoot } from "react-dom/client";
@@ -1666,8 +1669,10 @@ hydrate();
1666
1669
  middlewarePath = findMiddlewareFile(root);
1667
1670
  instrumentationPath = findInstrumentationFile(root);
1668
1671
  // Load next.config.js if present (always from project root, not src/)
1669
- const rawConfig = await loadNextConfig(root);
1672
+ const phase = env?.command === "build" ? PHASE_PRODUCTION_BUILD : PHASE_DEVELOPMENT_SERVER;
1673
+ const rawConfig = await loadNextConfig(root, phase);
1670
1674
  nextConfig = await resolveNextConfig(rawConfig);
1675
+ fileMatcher = createValidFileMatcher(nextConfig.pageExtensions);
1671
1676
  // Merge env from next.config.js with NEXT_PUBLIC_* env vars
1672
1677
  const defines = getNextPublicEnvDefines();
1673
1678
  if (!config.define ||
@@ -2110,10 +2115,10 @@ hydrate();
2110
2115
  }
2111
2116
  // App Router virtual modules
2112
2117
  if (id === RESOLVED_RSC_ENTRY && hasAppDir) {
2113
- const routes = await appRouter(appDir);
2118
+ const routes = await appRouter(appDir, nextConfig?.pageExtensions, fileMatcher);
2114
2119
  const metaRoutes = scanMetadataFiles(appDir);
2115
2120
  // Check for global-error.tsx at app root
2116
- const globalErrorPath = findFileWithExts(appDir, "global-error");
2121
+ const globalErrorPath = findFileWithExts(appDir, "global-error", fileMatcher);
2117
2122
  return generateRscEntry(appDir, routes, middlewarePath, metaRoutes, globalErrorPath, nextConfig?.basePath, nextConfig?.trailingSlash, {
2118
2123
  redirects: nextConfig?.redirects,
2119
2124
  rewrites: nextConfig?.rewrites,
@@ -2216,15 +2221,14 @@ hydrate();
2216
2221
  hotUpdate(options) {
2217
2222
  if (!hasPagesDir || hasAppDir)
2218
2223
  return;
2219
- const ext = /\.(tsx?|jsx?|mdx)$/;
2220
- if (options.file.startsWith(pagesDir) && ext.test(options.file)) {
2224
+ if (options.file.startsWith(pagesDir) && fileMatcher.extensionRegex.test(options.file)) {
2221
2225
  options.server.environments.client.hot.send({ type: "full-reload" });
2222
2226
  return [];
2223
2227
  }
2224
2228
  },
2225
2229
  configureServer(server) {
2226
2230
  // Watch pages directory for file additions/removals to invalidate route cache.
2227
- const pageExtensions = /\.(tsx?|jsx?|mdx)$/;
2231
+ const pageExtensions = fileMatcher.extensionRegex;
2228
2232
  /**
2229
2233
  * Invalidate the virtual RSC entry module in Vite's module graph.
2230
2234
  *
@@ -2587,10 +2591,20 @@ hydrate();
2587
2591
  return;
2588
2592
  }
2589
2593
  }
2590
- // Apply middleware response headers
2594
+ // Apply middleware response headers. Unpack
2595
+ // x-middleware-request-* headers into req.headers so
2596
+ // config has/missing conditions and downstream handlers
2597
+ // see middleware-modified cookies and headers.
2591
2598
  if (result.responseHeaders) {
2599
+ const mwReqPrefix = "x-middleware-request-";
2592
2600
  for (const [key, value] of result.responseHeaders) {
2593
- res.appendHeader(key, value);
2601
+ if (key.startsWith(mwReqPrefix)) {
2602
+ const realName = key.slice(mwReqPrefix.length);
2603
+ req.headers[realName] = value;
2604
+ }
2605
+ else if (!key.startsWith("x-middleware-")) {
2606
+ res.appendHeader(key, value);
2607
+ }
2594
2608
  }
2595
2609
  }
2596
2610
  // Apply middleware rewrite (URL and optional status code)
@@ -2639,7 +2653,7 @@ hydrate();
2639
2653
  const resolvedPathname = resolvedUrl.split("?")[0];
2640
2654
  if (resolvedPathname.startsWith("/api/") ||
2641
2655
  resolvedPathname === "/api") {
2642
- const apiRoutes = await apiRouter(pagesDir);
2656
+ const apiRoutes = await apiRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
2643
2657
  const handled = await handleApiRoute(server, req, res, resolvedUrl, apiRoutes);
2644
2658
  if (handled)
2645
2659
  return;
@@ -2651,7 +2665,7 @@ hydrate();
2651
2665
  res.end("404 - API route not found");
2652
2666
  return;
2653
2667
  }
2654
- const routes = await pagesRouter(pagesDir);
2668
+ const routes = await pagesRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
2655
2669
  // Apply afterFiles rewrites — these run after initial route matching
2656
2670
  // If beforeFiles already rewrote the URL, afterFiles still run on the
2657
2671
  // *resolved* pathname. Next.js applies these when route matching succeeds
@@ -2666,7 +2680,7 @@ hydrate();
2666
2680
  await proxyExternalRewriteNode(req, res, resolvedUrl);
2667
2681
  return;
2668
2682
  }
2669
- const handler = createSSRHandler(server, routes, pagesDir, nextConfig?.i18n);
2683
+ const handler = createSSRHandler(server, routes, pagesDir, nextConfig?.i18n, fileMatcher);
2670
2684
  const mwStatus = req.__vinextRewriteStatus;
2671
2685
  // Try rendering the resolved URL
2672
2686
  const match = matchRoute(resolvedUrl.split("?")[0], routes);
@@ -2700,6 +2714,41 @@ hydrate();
2700
2714
  };
2701
2715
  },
2702
2716
  },
2717
+ // Strip server-only data-fetching exports (getServerSideProps, getStaticProps,
2718
+ // getStaticPaths) from page modules in the client bundle. These functions
2719
+ // often import server-only modules (database drivers, fs, etc.) that would
2720
+ // break or bloat the client bundle. Next.js does this via an SWC transform
2721
+ // (next-ssg-transform); we use Vite's parseAst + MagicString.
2722
+ //
2723
+ // Only applies to client builds (not SSR) and only to files under the
2724
+ // pages/ directory.
2725
+ {
2726
+ name: "vinext:strip-server-exports",
2727
+ transform: {
2728
+ // Only match page source files, not node_modules
2729
+ filter: { id: /\.(tsx?|jsx?|mjs)$/ },
2730
+ handler(code, id) {
2731
+ const ssr = this.environment?.name !== "client";
2732
+ if (ssr)
2733
+ return null;
2734
+ if (!hasPagesDir)
2735
+ return null;
2736
+ // Only transform files under the pages/ directory
2737
+ if (!id.startsWith(pagesDir))
2738
+ return null;
2739
+ // Skip API routes, _app, _document, _error
2740
+ const relativePath = id.slice(pagesDir.length);
2741
+ if (relativePath.startsWith("/api/") || relativePath === "/api")
2742
+ return null;
2743
+ if (/\/_(?:app|document|error)\b/.test(relativePath))
2744
+ return null;
2745
+ const result = stripServerExports(code);
2746
+ if (!result)
2747
+ return null;
2748
+ return { code: result, map: null };
2749
+ },
2750
+ },
2751
+ },
2703
2752
  // Local image import transform:
2704
2753
  // When a source file imports a local image (e.g., `import hero from './hero.jpg'`),
2705
2754
  // this plugin transforms the default import to a StaticImageData object with
@@ -3342,6 +3391,7 @@ hydrate();
3342
3391
  " Cache-Control: public, max-age=31536000, immutable",
3343
3392
  "",
3344
3393
  ].join("\n");
3394
+ fs.mkdirSync(clientDir, { recursive: true });
3345
3395
  fs.writeFileSync(headersPath, headersContent);
3346
3396
  }
3347
3397
  },
@@ -3400,6 +3450,101 @@ function extractConstraint(str, re) {
3400
3450
  * :param+ — matches one or more segments
3401
3451
  * (regex) — inline regex patterns in the source
3402
3452
  */
3453
+ /**
3454
+ * Strip server-only data-fetching exports from a page module's source code.
3455
+ * Returns the transformed code, or null if no changes were made.
3456
+ *
3457
+ * Handles:
3458
+ * - export (async) function getServerSideProps(...) { ... }
3459
+ * - export const getStaticProps = async (...) => { ... }
3460
+ * - export const getServerSideProps = someHelper;
3461
+ */
3462
+ /**
3463
+ * Skip past balanced brackets/parens/braces starting at `pos` (which should
3464
+ * point to the opening bracket). Returns the position AFTER the closing bracket.
3465
+ * Handles nested brackets, string literals, and comments.
3466
+ */
3467
+ /**
3468
+ * Strip server-only data-fetching exports (getServerSideProps,
3469
+ * getStaticProps, getStaticPaths) from page modules for the client
3470
+ * bundle. Uses Vite's parseAst (Rollup/acorn) for correct handling
3471
+ * of all export patterns including function expressions, arrow
3472
+ * functions with TS return types, and re-exports.
3473
+ *
3474
+ * Modeled after Next.js's SWC `next-ssg-transform`.
3475
+ */
3476
+ function stripServerExports(code) {
3477
+ const SERVER_EXPORTS = new Set(["getServerSideProps", "getStaticProps", "getStaticPaths"]);
3478
+ if (![...SERVER_EXPORTS].some(name => code.includes(name)))
3479
+ return null;
3480
+ let ast;
3481
+ try {
3482
+ ast = parseAst(code);
3483
+ }
3484
+ catch {
3485
+ // If parsing fails (shouldn't happen post-JSX/TS transform), bail out
3486
+ return null;
3487
+ }
3488
+ const s = new MagicString(code);
3489
+ let changed = false;
3490
+ for (const node of ast.body) {
3491
+ if (node.type !== "ExportNamedDeclaration")
3492
+ continue;
3493
+ // Case 1: export function name() {} / export async function name() {}
3494
+ // Case 2: export const/let/var name = ...
3495
+ if (node.declaration) {
3496
+ const decl = node.declaration;
3497
+ if (decl.type === "FunctionDeclaration" && SERVER_EXPORTS.has(decl.id?.name)) {
3498
+ s.overwrite(node.start, node.end, `export function ${decl.id.name}() { return { props: {} }; }`);
3499
+ changed = true;
3500
+ }
3501
+ else if (decl.type === "VariableDeclaration") {
3502
+ for (const declarator of decl.declarations) {
3503
+ if (declarator.id?.type === "Identifier" && SERVER_EXPORTS.has(declarator.id.name)) {
3504
+ s.overwrite(node.start, node.end, `export const ${declarator.id.name} = undefined;`);
3505
+ changed = true;
3506
+ }
3507
+ }
3508
+ }
3509
+ continue;
3510
+ }
3511
+ // Case 3: export { getServerSideProps } or export { getServerSideProps as gSSP }
3512
+ if (node.specifiers && node.specifiers.length > 0 && !node.source) {
3513
+ const kept = [];
3514
+ const stripped = [];
3515
+ for (const spec of node.specifiers) {
3516
+ // spec.local.name is the binding name, spec.exported.name is the export name
3517
+ const exportedName = spec.exported?.name ?? spec.exported?.value;
3518
+ if (SERVER_EXPORTS.has(exportedName)) {
3519
+ stripped.push(exportedName);
3520
+ }
3521
+ else {
3522
+ kept.push(spec);
3523
+ }
3524
+ }
3525
+ if (stripped.length > 0) {
3526
+ // Build replacement: keep non-server specifiers, add stubs for stripped ones
3527
+ const parts = [];
3528
+ if (kept.length > 0) {
3529
+ const keptStr = kept.map((sp) => {
3530
+ const local = sp.local.name;
3531
+ const exported = sp.exported?.name ?? sp.exported?.value;
3532
+ return local === exported ? local : `${local} as ${exported}`;
3533
+ }).join(", ");
3534
+ parts.push(`export { ${keptStr} };`);
3535
+ }
3536
+ for (const name of stripped) {
3537
+ parts.push(`export const ${name} = undefined;`);
3538
+ }
3539
+ s.overwrite(node.start, node.end, parts.join("\n"));
3540
+ changed = true;
3541
+ }
3542
+ }
3543
+ }
3544
+ if (!changed)
3545
+ return null;
3546
+ return s.toString();
3547
+ }
3403
3548
  export function matchConfigPattern(pathname, pattern) {
3404
3549
  // If the pattern contains regex groups like (\\d+) or (.*), use regex matching.
3405
3550
  // Also enter this branch when a catch-all parameter (:param* or :param+) is
@@ -3610,16 +3755,44 @@ function applyRewrites(pathname, rewrites, ctx) {
3610
3755
  function applyHeaders(pathname, res, headers, ctx) {
3611
3756
  const matched = matchHeaders(pathname, headers, ctx);
3612
3757
  for (const header of matched) {
3613
- res.setHeader(header.key, header.value);
3758
+ // Use append semantics for headers where multiple values must coexist
3759
+ // (Vary, Set-Cookie). Using setHeader() on these would destroy
3760
+ // existing values like "Vary: RSC, Accept".
3761
+ const lk = header.key.toLowerCase();
3762
+ if (lk === "set-cookie") {
3763
+ // Node.js res.getHeader("set-cookie") returns string[] when
3764
+ // multiple Set-Cookie headers have been set. Preserve the array.
3765
+ const existing = res.getHeader(lk);
3766
+ if (Array.isArray(existing)) {
3767
+ res.setHeader(header.key, [...existing, header.value]);
3768
+ }
3769
+ else if (existing) {
3770
+ res.setHeader(header.key, [String(existing), header.value]);
3771
+ }
3772
+ else {
3773
+ res.setHeader(header.key, header.value);
3774
+ }
3775
+ }
3776
+ else if (lk === "vary") {
3777
+ const existing = res.getHeader(lk);
3778
+ if (existing) {
3779
+ res.setHeader(header.key, existing + ", " + header.value);
3780
+ }
3781
+ else {
3782
+ res.setHeader(header.key, header.value);
3783
+ }
3784
+ }
3785
+ else {
3786
+ res.setHeader(header.key, header.value);
3787
+ }
3614
3788
  }
3615
3789
  }
3616
3790
  /**
3617
3791
  * Find a file by name (without extension) in a directory.
3618
- * Checks .tsx, .ts, .jsx, .js extensions.
3792
+ * Checks the configured page extensions.
3619
3793
  */
3620
- function findFileWithExts(dir, name) {
3621
- const extensions = [".tsx", ".ts", ".jsx", ".js"];
3622
- for (const ext of extensions) {
3794
+ function findFileWithExts(dir, name, matcher) {
3795
+ for (const ext of matcher.dottedExtensions) {
3623
3796
  const filePath = path.join(dir, name + ext);
3624
3797
  if (fs.existsSync(filePath))
3625
3798
  return filePath;
@@ -3664,4 +3837,5 @@ export { staticExportPages, staticExportApp } from "./build/static-export.js";
3664
3837
  export { clientManualChunks, clientOutputConfig, clientTreeshakeConfig, computeLazyChunks };
3665
3838
  export { resolvePostcssStringPlugins as _resolvePostcssStringPlugins };
3666
3839
  export { parseStaticObjectLiteral as _parseStaticObjectLiteral };
3840
+ export { stripServerExports as _stripServerExports };
3667
3841
  //# sourceMappingURL=index.js.map