vinext 0.0.10 → 0.0.11

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 (37) hide show
  1. package/dist/cli.js +4 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config/config-matchers.d.ts +8 -0
  4. package/dist/config/config-matchers.d.ts.map +1 -1
  5. package/dist/config/config-matchers.js +163 -42
  6. package/dist/config/config-matchers.js.map +1 -1
  7. package/dist/deploy.d.ts.map +1 -1
  8. package/dist/deploy.js +6 -4
  9. package/dist/deploy.js.map +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +78 -48
  12. package/dist/index.js.map +1 -1
  13. package/dist/server/app-dev-server.d.ts.map +1 -1
  14. package/dist/server/app-dev-server.js +13 -4
  15. package/dist/server/app-dev-server.js.map +1 -1
  16. package/dist/server/dev-server.d.ts.map +1 -1
  17. package/dist/server/dev-server.js +13 -2
  18. package/dist/server/dev-server.js.map +1 -1
  19. package/dist/server/middleware-codegen.d.ts.map +1 -1
  20. package/dist/server/middleware-codegen.js +13 -7
  21. package/dist/server/middleware-codegen.js.map +1 -1
  22. package/dist/server/middleware.d.ts.map +1 -1
  23. package/dist/server/middleware.js +25 -12
  24. package/dist/server/middleware.js.map +1 -1
  25. package/dist/server/prod-server.d.ts.map +1 -1
  26. package/dist/server/prod-server.js +6 -1
  27. package/dist/server/prod-server.js.map +1 -1
  28. package/dist/shims/fetch-cache.d.ts.map +1 -1
  29. package/dist/shims/fetch-cache.js +6 -0
  30. package/dist/shims/fetch-cache.js.map +1 -1
  31. package/dist/shims/head.d.ts.map +1 -1
  32. package/dist/shims/head.js +4 -1
  33. package/dist/shims/head.js.map +1 -1
  34. package/dist/shims/headers.d.ts.map +1 -1
  35. package/dist/shims/headers.js +4 -2
  36. package/dist/shims/headers.js.map +1 -1
  37. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormaliz
10
10
  import { normalizePath } from "./server/normalize-path.js";
11
11
  import { findInstrumentationFile, runInstrumentation } from "./server/instrumentation.js";
12
12
  import { validateDevRequest } from "./server/dev-origin-check.js";
13
- import { safeRegExp, isExternalUrl, proxyExternalRequest } from "./config/config-matchers.js";
13
+ import { safeRegExp, escapeHeaderSource, isExternalUrl, proxyExternalRequest } from "./config/config-matchers.js";
14
14
  import { scanMetadataFiles } from "./server/metadata-routes.js";
15
15
  import tsconfigPaths from "vite-tsconfig-paths";
16
16
  import MagicString from "magic-string";
@@ -1115,7 +1115,7 @@ export async function renderPage(request, url, manifest) {
1115
1115
  if (result && result.props) pageProps = result.props;
1116
1116
  if (result && result.redirect) {
1117
1117
  var gsspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
1118
- return new Response(null, { status: gsspStatus, headers: { Location: result.redirect.destination } });
1118
+ return new Response(null, { status: gsspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
1119
1119
  }
1120
1120
  if (result && result.notFound) {
1121
1121
  return new Response("404", { status: 404 });
@@ -1174,7 +1174,7 @@ export async function renderPage(request, url, manifest) {
1174
1174
  if (result && result.props) pageProps = result.props;
1175
1175
  if (result && result.redirect) {
1176
1176
  var gspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
1177
- return new Response(null, { status: gspStatus, headers: { Location: result.redirect.destination } });
1177
+ return new Response(null, { status: gspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
1178
1178
  }
1179
1179
  if (result && result.notFound) {
1180
1180
  return new Response("404", { status: 404 });
@@ -1395,6 +1395,9 @@ ${middlewareExportCode}
1395
1395
  const loaderEntries = pageRoutes.map((r) => {
1396
1396
  const absPath = r.filePath.replace(/\\/g, "/");
1397
1397
  const nextFormatPattern = pagesPatternToNextFormat(r.pattern);
1398
+ // JSON.stringify safely escapes quotes, backslashes, and special chars in
1399
+ // both the route pattern and the absolute file path.
1400
+ // lgtm[js/bad-code-sanitization]
1398
1401
  return ` ${JSON.stringify(nextFormatPattern)}: () => import(${JSON.stringify(absPath)})`;
1399
1402
  });
1400
1403
  const appFileBase = path.join(pagesDir, "_app").replace(/\\/g, "/");
@@ -2990,6 +2993,29 @@ function getNextPublicEnvDefines() {
2990
2993
  }
2991
2994
  return defines;
2992
2995
  }
2996
+ /**
2997
+ * If the current position in `str` starts with a parenthesized group, consume
2998
+ * it and advance `re.lastIndex` past the closing `)`. Returns the group
2999
+ * contents or null if no group is present.
3000
+ */
3001
+ function extractConstraint(str, re) {
3002
+ if (str[re.lastIndex] !== "(")
3003
+ return null;
3004
+ const start = re.lastIndex + 1;
3005
+ let depth = 1;
3006
+ let i = start;
3007
+ while (i < str.length && depth > 0) {
3008
+ if (str[i] === "(")
3009
+ depth++;
3010
+ else if (str[i] === ")")
3011
+ depth--;
3012
+ i++;
3013
+ }
3014
+ if (depth !== 0)
3015
+ return null;
3016
+ re.lastIndex = i;
3017
+ return str.slice(start, i - 1);
3018
+ }
2993
3019
  /**
2994
3020
  * Match a Next.js route pattern (e.g. "/blog/:slug", "/docs/:path*") against a pathname.
2995
3021
  * Returns matched params or null.
@@ -3016,28 +3042,44 @@ export function matchConfigPattern(pathname, pattern) {
3016
3042
  // :param* -> (.*)
3017
3043
  // :param+ -> (.+)
3018
3044
  const paramNames = [];
3019
- const regexStr = pattern
3020
- .replace(/\./g, "\\.")
3021
- // :param* with optional constraint
3022
- .replace(/:(\w+)\*(?:\(([^)]+)\))?/g, (_m, name, constraint) => {
3023
- paramNames.push(name);
3024
- return constraint ? `(${constraint})` : "(.*)";
3025
- })
3026
- // :param+ with optional constraint
3027
- .replace(/:(\w+)\+(?:\(([^)]+)\))?/g, (_m, name, constraint) => {
3028
- paramNames.push(name);
3029
- return constraint ? `(${constraint})` : "(.+)";
3030
- })
3031
- // :param(constraint) named param with inline regex constraint
3032
- .replace(/:(\w+)\(([^)]+)\)/g, (_m, name, constraint) => {
3033
- paramNames.push(name);
3034
- return `(${constraint})`;
3035
- })
3036
- // :param plain named param
3037
- .replace(/:(\w+)/g, (_m, name) => {
3038
- paramNames.push(name);
3039
- return "([^/]+)";
3040
- });
3045
+ // Single-pass conversion with procedural suffix handling. The tokenizer
3046
+ // matches only simple, non-overlapping tokens; quantifier/constraint
3047
+ // suffixes after :param are consumed procedurally to avoid polynomial
3048
+ // backtracking in the regex engine.
3049
+ let regexStr = "";
3050
+ const tokenRe = /:(\w+)|[.]|[^:.]+/g; // lgtm[js/redos] — alternatives are non-overlapping (`:` and `.` excluded from `[^:.]+`)
3051
+ let tok;
3052
+ while ((tok = tokenRe.exec(pattern)) !== null) {
3053
+ if (tok[1] !== undefined) {
3054
+ const name = tok[1];
3055
+ const rest = pattern.slice(tokenRe.lastIndex);
3056
+ // Check for quantifier (* or +) with optional constraint
3057
+ if (rest.startsWith("*") || rest.startsWith("+")) {
3058
+ const quantifier = rest[0];
3059
+ tokenRe.lastIndex += 1;
3060
+ const constraint = extractConstraint(pattern, tokenRe);
3061
+ paramNames.push(name);
3062
+ if (constraint !== null) {
3063
+ regexStr += `(${constraint})`;
3064
+ }
3065
+ else {
3066
+ regexStr += quantifier === "*" ? "(.*)" : "(.+)";
3067
+ }
3068
+ }
3069
+ else {
3070
+ // Check for inline constraint without quantifier
3071
+ const constraint = extractConstraint(pattern, tokenRe);
3072
+ paramNames.push(name);
3073
+ regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
3074
+ }
3075
+ }
3076
+ else if (tok[0] === ".") {
3077
+ regexStr += "\\.";
3078
+ }
3079
+ else {
3080
+ regexStr += tok[0];
3081
+ }
3082
+ }
3041
3083
  const re = safeRegExp("^" + regexStr + "$");
3042
3084
  if (!re)
3043
3085
  return null;
@@ -3067,7 +3109,12 @@ export function matchConfigPattern(pathname, pattern) {
3067
3109
  if (isPlus && (!rest || rest === "/"))
3068
3110
  return null;
3069
3111
  // For :path* zero segments is fine
3070
- return { [paramName]: rest.startsWith("/") ? rest.slice(1) : rest };
3112
+ let restValue = rest.startsWith("/") ? rest.slice(1) : rest;
3113
+ try {
3114
+ restValue = decodeURIComponent(restValue);
3115
+ }
3116
+ catch { /* malformed percent-encoding */ }
3117
+ return { [paramName]: restValue };
3071
3118
  }
3072
3119
  // Simple segment-based matching for exact patterns and :param
3073
3120
  const parts = pattern.split("/");
@@ -3086,14 +3133,14 @@ export function matchConfigPattern(pathname, pattern) {
3086
3133
  return params;
3087
3134
  }
3088
3135
  /**
3089
- * Sanitize a redirect/rewrite destination by collapsing leading // to /
3090
- * for non-external URLs, preventing unintended protocol-relative redirects.
3136
+ * Sanitize a redirect/rewrite destination by collapsing leading slashes and
3137
+ * backslashes to a single "/" for non-external URLs. Browsers interpret "\"
3138
+ * as "/" in URL contexts, so "\/evil.com" becomes "//evil.com" (protocol-relative).
3091
3139
  */
3092
3140
  function sanitizeDestinationLocal(dest) {
3093
3141
  if (dest.startsWith("http://") || dest.startsWith("https://"))
3094
3142
  return dest;
3095
- if (dest.startsWith("//"))
3096
- dest = dest.replace(/^\/\/+/, "/");
3143
+ dest = dest.replace(/^[\\/]+/, "/");
3097
3144
  return dest;
3098
3145
  }
3099
3146
  /**
@@ -3204,24 +3251,7 @@ function applyRewrites(pathname, rewrites) {
3204
3251
  */
3205
3252
  function applyHeaders(pathname, res, headers) {
3206
3253
  for (const rule of headers) {
3207
- // Escape regex metacharacters in the source, then convert Next.js patterns.
3208
- // Strategy: extract regex groups first, process the rest, then restore groups.
3209
- const groups = [];
3210
- const withPlaceholders = rule.source.replace(/\(([^)]+)\)/g, (_m, inner) => {
3211
- groups.push(inner);
3212
- return `___GROUP_${groups.length - 1}___`;
3213
- });
3214
- const escaped = withPlaceholders
3215
- // Escape dots and other metacharacters
3216
- .replace(/\./g, "\\.")
3217
- .replace(/\+/g, "\\+")
3218
- .replace(/\?/g, "\\?")
3219
- // Convert glob * to .*
3220
- .replace(/\*/g, ".*")
3221
- // Convert :param to [^/]+
3222
- .replace(/:\w+/g, "[^/]+")
3223
- // Restore regex groups (contents are untouched)
3224
- .replace(/___GROUP_(\d+)___/g, (_m, idx) => `(${groups[Number(idx)]})`);
3254
+ const escaped = escapeHeaderSource(rule.source);
3225
3255
  const sourceRegex = safeRegExp("^" + escaped + "$");
3226
3256
  if (sourceRegex && sourceRegex.test(pathname)) {
3227
3257
  for (const header of rule.headers) {