@vercel/fs-detectors 6.13.2 → 6.14.0

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.
@@ -4,10 +4,6 @@ import type { PackageJson, Builder, BuilderFunctions, ExperimentalServices, Expe
4
4
  * Pattern for finding all supported middleware files.
5
5
  */
6
6
  export declare const REGEX_MIDDLEWARE_FILES = "middleware.[jt]s";
7
- /**
8
- * Pattern for files that the Vercel platform cares about separately from frameworks.
9
- * These files are excluded from static file serving.
10
- */
11
7
  export declare const REGEX_VERCEL_PLATFORM_FILES: string;
12
8
  /**
13
9
  * Pattern for non-Vercel platform files.
@@ -19,6 +15,10 @@ export interface ErrorResponse {
19
15
  action?: string;
20
16
  link?: string;
21
17
  }
18
+ export interface ProxyConfig {
19
+ entrypoint: string;
20
+ matcher?: string | string[];
21
+ }
22
22
  export interface Options {
23
23
  tag?: string;
24
24
  functions?: BuilderFunctions;
@@ -32,6 +32,7 @@ export interface Options {
32
32
  featHandleMiss?: boolean;
33
33
  bunVersion?: string;
34
34
  workPath?: string;
35
+ proxy?: ProxyConfig;
35
36
  }
36
37
  export declare function sortFiles(fileA: string, fileB: string): number;
37
38
  export declare function detectApiExtensions(builders: Builder[]): Set<string>;
@@ -51,3 +52,10 @@ export declare function detectBuilders(files: string[], pkg?: PackageJson | unde
51
52
  experimentalServicesV2?: Services;
52
53
  useImplicitEnvInjection?: boolean;
53
54
  }>;
55
+ export declare function getProxyBuilder(proxy: ProxyConfig, tag?: string, functions?: BuilderFunctions): Builder;
56
+ /**
57
+ * Validates a `proxy` config value in isolation, without knowledge of the
58
+ * project's files or framework. Also used by the CLI to validate
59
+ * `vercel.json` before builders are detected.
60
+ */
61
+ export declare function validateProxyConfig(proxy: ProxyConfig): ErrorResponse | null;
@@ -35,7 +35,9 @@ __export(detect_builders_exports, {
35
35
  detectApiExtensions: () => detectApiExtensions,
36
36
  detectBuilders: () => detectBuilders,
37
37
  detectOutputDirectory: () => detectOutputDirectory,
38
- sortFiles: () => sortFiles
38
+ getProxyBuilder: () => getProxyBuilder,
39
+ sortFiles: () => sortFiles,
40
+ validateProxyConfig: () => validateProxyConfig
39
41
  });
40
42
  module.exports = __toCommonJS(detect_builders_exports);
41
43
  var import_minimatch = __toESM(require("minimatch"));
@@ -46,7 +48,7 @@ var import_is_official_runtime = require("./is-official-runtime");
46
48
  var import_build_utils = require("@vercel/build-utils");
47
49
  var import_get_services_builders = require("./services/get-services-builders");
48
50
  const REGEX_MIDDLEWARE_FILES = "middleware.[jt]s";
49
- const REGEX_VERCEL_PLATFORM_FILES = [
51
+ const VERCEL_PLATFORM_FILES = [
50
52
  "api/**",
51
53
  "node_modules/**",
52
54
  REGEX_MIDDLEWARE_FILES,
@@ -58,8 +60,18 @@ const REGEX_VERCEL_PLATFORM_FILES = [
58
60
  "bun.lockb",
59
61
  ".gitignore",
60
62
  "README.md"
61
- ].join(",");
62
- const REGEX_NON_VERCEL_PLATFORM_FILES = `!{${REGEX_VERCEL_PLATFORM_FILES}}`;
63
+ ];
64
+ const REGEX_VERCEL_PLATFORM_FILES = VERCEL_PLATFORM_FILES.join(",");
65
+ function escapeMinimatchPath(path) {
66
+ return path.replace(/([\\,*?[\]{}()!+@])/g, "\\$1");
67
+ }
68
+ function getStaticFilesPattern(additionalExclusions = []) {
69
+ return `!{${[
70
+ ...VERCEL_PLATFORM_FILES,
71
+ ...additionalExclusions.map(escapeMinimatchPath)
72
+ ].join(",")}}`;
73
+ }
74
+ const REGEX_NON_VERCEL_PLATFORM_FILES = getStaticFilesPattern();
63
75
  const slugToFramework = new Map(
64
76
  import_frameworks.frameworkList.map((f) => [f.slug, f])
65
77
  );
@@ -115,6 +127,18 @@ async function detectBuilders(files, pkg, options = {}) {
115
127
  };
116
128
  }
117
129
  const { framework } = projectSettings;
130
+ const proxyError = validateProxy(options, files, framework);
131
+ if (proxyError) {
132
+ return {
133
+ builders: null,
134
+ errors: [proxyError],
135
+ warnings: [],
136
+ defaultRoutes: null,
137
+ redirectRoutes: null,
138
+ rewriteRoutes: null,
139
+ errorRoutes: null
140
+ };
141
+ }
118
142
  const servicesConfig = services ?? experimentalServicesV2;
119
143
  const configuredServices = servicesConfig ?? experimentalServicesV1;
120
144
  const configuredServicesType = servicesConfig ? services ? "services" : "experimentalServicesV2" : "experimentalServices";
@@ -131,6 +155,12 @@ async function detectBuilders(files, pkg, options = {}) {
131
155
  ...(0, import_get_services_builders.warnIgnoredDirectories)(files, configuredServices)
132
156
  );
133
157
  }
158
+ if (!result.errors && options.proxy) {
159
+ result.builders = [
160
+ getProxyBuilder(options.proxy, options.tag, options.functions),
161
+ ...result.builders ?? []
162
+ ];
163
+ }
134
164
  return result;
135
165
  }
136
166
  const errors = [];
@@ -181,6 +211,9 @@ async function detectBuilders(files, pkg, options = {}) {
181
211
  const apiRoutes = [];
182
212
  const dynamicRoutes = [];
183
213
  for (const fileName of sortedFiles) {
214
+ if (fileName === options.proxy?.entrypoint) {
215
+ continue;
216
+ }
184
217
  const apiBuilder = await maybeGetApiBuilder(fileName, apiMatches, options);
185
218
  if (apiBuilder) {
186
219
  const { routeError, apiRoute, isDynamic } = getApiRoute(
@@ -223,6 +256,15 @@ async function detectBuilders(files, pkg, options = {}) {
223
256
  fallbackEntrypoint = fileName;
224
257
  }
225
258
  }
259
+ if (options.proxy) {
260
+ const proxyBuilder = getProxyBuilder(
261
+ options.proxy,
262
+ options.tag,
263
+ options.functions
264
+ );
265
+ addToUsedFunctions(proxyBuilder);
266
+ apiBuilders.unshift(proxyBuilder);
267
+ }
226
268
  if (!makeFrontendStatic && (hasBuildScript(pkg) || buildCommand || framework)) {
227
269
  frontendBuilder = detectFrontBuilder(
228
270
  pkg,
@@ -256,13 +298,31 @@ async function detectBuilders(files, pkg, options = {}) {
256
298
  } else if (apiBuilders.length && hasNoneApiFiles) {
257
299
  frontendBuilder = {
258
300
  use: "@vercel/static",
259
- src: REGEX_NON_VERCEL_PLATFORM_FILES,
301
+ src: getStaticFilesPattern(
302
+ options.proxy ? [options.proxy.entrypoint] : []
303
+ ),
260
304
  config: {
261
305
  zeroConfig: true
262
306
  }
263
307
  };
264
308
  }
265
309
  }
310
+ if (options.proxy && frontendBuilder && (0, import_is_official_runtime.isOfficialRuntime)("next", frontendBuilder.use)) {
311
+ return {
312
+ builders: null,
313
+ errors: [
314
+ {
315
+ code: "proxy_framework_conflict",
316
+ message: "The `proxy` property cannot be used with Next.js because the framework builds its own routing middleware."
317
+ }
318
+ ],
319
+ warnings,
320
+ defaultRoutes: null,
321
+ redirectRoutes: null,
322
+ rewriteRoutes: null,
323
+ errorRoutes: null
324
+ };
325
+ }
266
326
  const unusedFunctionError = checkUnusedFunctions(
267
327
  frontendBuilder,
268
328
  usedFunctions,
@@ -329,7 +389,7 @@ async function detectBuilders(files, pkg, options = {}) {
329
389
  };
330
390
  }
331
391
  async function maybeGetApiBuilder(fileName, apiMatches, options) {
332
- const middleware = fileName === "middleware.js" || fileName === "middleware.ts";
392
+ const middleware = !options.proxy && (fileName === "middleware.js" || fileName === "middleware.ts");
333
393
  if (middleware && options.projectSettings?.framework === "nextjs") {
334
394
  return null;
335
395
  }
@@ -402,6 +462,30 @@ function getFunction(fileName, { functions = {} }) {
402
462
  const func = keys.find((key) => key === fileName || (0, import_minimatch.default)(fileName, key));
403
463
  return func ? { fnPattern: func, func: functions[func] } : { fnPattern: null, func: null };
404
464
  }
465
+ function getProxyBuilder(proxy, tag, functions = {}) {
466
+ const { fnPattern, func } = getFunction(proxy.entrypoint, { functions });
467
+ const runtime = func?.runtime;
468
+ const config = {
469
+ zeroConfig: true,
470
+ middleware: true,
471
+ ...!runtime ? { middlewareRuntime: "nodejs" } : {},
472
+ ...proxy.matcher ? { middlewareMatcher: proxy.matcher } : {}
473
+ };
474
+ if (fnPattern && func) {
475
+ config.functions = { [fnPattern]: func };
476
+ if (func.includeFiles) {
477
+ config.includeFiles = func.includeFiles;
478
+ }
479
+ if (func.excludeFiles) {
480
+ config.excludeFiles = func.excludeFiles;
481
+ }
482
+ }
483
+ return {
484
+ src: proxy.entrypoint,
485
+ use: runtime || `@vercel/node${tag ? `@${tag}` : ""}`,
486
+ config
487
+ };
488
+ }
405
489
  function getApiMatches() {
406
490
  const config = { zeroConfig: true };
407
491
  return [
@@ -417,6 +501,63 @@ function getApiMatches() {
417
501
  { src: "api/**/*.rs", use: `@vercel/rust`, config }
418
502
  ];
419
503
  }
504
+ function validateProxyConfig(proxy) {
505
+ if (typeof proxy !== "object" || typeof proxy.entrypoint !== "string" || !proxy.entrypoint) {
506
+ return {
507
+ code: "invalid_proxy",
508
+ message: "The `proxy` property must contain an `entrypoint` string that references a `.js` or `.ts` file."
509
+ };
510
+ }
511
+ const entrypoint = proxy.entrypoint;
512
+ const segments = entrypoint.split("/");
513
+ if (entrypoint.startsWith("/") || entrypoint.includes("\\") || segments.includes(".") || segments.includes("..") || /[?#\u0000-\u001f]/.test(entrypoint)) {
514
+ return {
515
+ code: "invalid_proxy_entrypoint",
516
+ message: "The `proxy.entrypoint` path must be relative to the project root and cannot contain traversal, query, fragment, or control characters."
517
+ };
518
+ }
519
+ if (!/\.(?:js|ts)$/.test(entrypoint) || entrypoint.endsWith(".d.ts")) {
520
+ return {
521
+ code: "invalid_proxy_entrypoint",
522
+ message: "The `proxy.entrypoint` path must end in `.js` or `.ts` and reference an executable file."
523
+ };
524
+ }
525
+ if (proxy.matcher !== void 0) {
526
+ const matchers = Array.isArray(proxy.matcher) ? proxy.matcher : [proxy.matcher];
527
+ if (matchers.length === 0 || matchers.some(
528
+ (matcher) => typeof matcher !== "string" || !matcher.startsWith("/")
529
+ )) {
530
+ return {
531
+ code: "invalid_proxy_matcher",
532
+ message: "The `proxy.matcher` value must be a path matcher starting with `/`, or an array of path matchers starting with `/`."
533
+ };
534
+ }
535
+ }
536
+ return null;
537
+ }
538
+ function validateProxy(options, files, framework) {
539
+ const { proxy } = options;
540
+ if (!proxy) {
541
+ return null;
542
+ }
543
+ const configError = validateProxyConfig(proxy);
544
+ if (configError) {
545
+ return configError;
546
+ }
547
+ if (!files.includes(proxy.entrypoint)) {
548
+ return {
549
+ code: "proxy_entrypoint_not_found",
550
+ message: `The proxy entrypoint \`${proxy.entrypoint}\` does not exist. Set \`proxy.entrypoint\` to an existing \`.js\` or \`.ts\` file.`
551
+ };
552
+ }
553
+ if (framework === "nextjs" || framework === "astro") {
554
+ return {
555
+ code: "proxy_framework_conflict",
556
+ message: `The \`proxy\` property cannot be used with ${framework === "nextjs" ? "Next.js" : "Astro"} because the framework builds its own routing middleware.`
557
+ };
558
+ }
559
+ return null;
560
+ }
420
561
  function hasBuildScript(pkg) {
421
562
  const { scripts = {} } = pkg || {};
422
563
  return Boolean(scripts && scripts["build"]);
@@ -870,5 +1011,7 @@ function sortFilesBySegmentCount(fileA, fileB) {
870
1011
  detectApiExtensions,
871
1012
  detectBuilders,
872
1013
  detectOutputDirectory,
873
- sortFiles
1014
+ getProxyBuilder,
1015
+ sortFiles,
1016
+ validateProxyConfig
874
1017
  });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { detectBuilders, detectOutputDirectory, detectApiDirectory, detectApiExtensions, type Options as DetectBuildersOptions, } from './detect-builders';
1
+ export { detectBuilders, detectOutputDirectory, detectApiDirectory, detectApiExtensions, getProxyBuilder, validateProxyConfig, type Options as DetectBuildersOptions, type ProxyConfig, } from './detect-builders';
2
2
  export { detectServices, generateServicesRoutes, } from './services/detect-services';
3
3
  export { resolveAllConfiguredServicesV2, resolveConfiguredServiceV2, validateServiceConfigV2, } from './services/resolve-v2';
4
4
  export { isExperimentalService, isExperimentalServiceV2, } from '@vercel/build-utils';
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ __export(src_exports, {
46
46
  getInternalServiceWorkerPath: () => import_utils.getInternalServiceWorkerPath,
47
47
  getInternalServiceWorkerPathPrefix: () => import_utils.getInternalServiceWorkerPathPrefix,
48
48
  getProjectPaths: () => import_get_project_paths.getProjectPaths,
49
+ getProxyBuilder: () => import_detect_builders.getProxyBuilder,
49
50
  getServicesBuilders: () => import_get_services_builders.getServicesBuilders,
50
51
  getWorkspacePackagePaths: () => import_get_workspace_package_paths.getWorkspacePackagePaths,
51
52
  getWorkspaces: () => import_get_workspaces.getWorkspaces,
@@ -59,6 +60,7 @@ __export(src_exports, {
59
60
  packageManagers: () => import_package_managers.packageManagers,
60
61
  resolveAllConfiguredServicesV2: () => import_resolve_v2.resolveAllConfiguredServicesV2,
61
62
  resolveConfiguredServiceV2: () => import_resolve_v2.resolveConfiguredServiceV2,
63
+ validateProxyConfig: () => import_detect_builders.validateProxyConfig,
62
64
  validateServiceConfigV2: () => import_resolve_v2.validateServiceConfigV2,
63
65
  workspaceManagers: () => import_workspace_managers.workspaceManagers
64
66
  });
@@ -113,6 +115,7 @@ var import_detect_instrumentation = require("./detect-instrumentation");
113
115
  getInternalServiceWorkerPath,
114
116
  getInternalServiceWorkerPathPrefix,
115
117
  getProjectPaths,
118
+ getProxyBuilder,
116
119
  getServicesBuilders,
117
120
  getWorkspacePackagePaths,
118
121
  getWorkspaces,
@@ -126,6 +129,7 @@ var import_detect_instrumentation = require("./detect-instrumentation");
126
129
  packageManagers,
127
130
  resolveAllConfiguredServicesV2,
128
131
  resolveConfiguredServiceV2,
132
+ validateProxyConfig,
129
133
  validateServiceConfigV2,
130
134
  workspaceManagers,
131
135
  ...require("./monorepos/get-monorepo-default-settings")
@@ -177,7 +177,7 @@ async function readVercelConfig(fs) {
177
177
  };
178
178
  }
179
179
  }
180
- const hasVercelToml = process.env.VERCEL_TOML_CONFIG_ENABLED === "1" && await fs.hasPath("vercel.toml");
180
+ const hasVercelToml = await fs.hasPath("vercel.toml");
181
181
  if (hasVercelToml) {
182
182
  try {
183
183
  const { parse: tomlParse } = await import("smol-toml");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/fs-detectors",
3
- "version": "6.13.2",
3
+ "version": "6.14.0",
4
4
  "description": "Vercel filesystem detectors",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -20,10 +20,10 @@
20
20
  "minimatch": "3.1.2",
21
21
  "semver": "6.3.1",
22
22
  "smol-toml": "1.5.2",
23
- "@vercel/routing-utils": "6.4.0",
23
+ "@vercel/build-utils": "13.36.0",
24
+ "@vercel/frameworks": "3.30.7",
24
25
  "@vercel/error-utils": "2.2.0",
25
- "@vercel/build-utils": "13.34.0",
26
- "@vercel/frameworks": "3.30.7"
26
+ "@vercel/routing-utils": "6.4.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/glob": "7.2.0",