@vercel/fs-detectors 7.6.2 → 7.7.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.
@@ -27,6 +27,13 @@ export interface ProxyConfig {
27
27
  export interface Options {
28
28
  tag?: string;
29
29
  functions?: BuilderFunctions;
30
+ /**
31
+ * Source files built by a builder that detection does not create itself
32
+ * (e.g. schedule entrypoints resolved by the CLI). Detection neither builds
33
+ * them as `api/` functions nor serves them as static files, and `functions`
34
+ * patterns that match them count as used.
35
+ */
36
+ reservedFunctionFiles?: string[];
30
37
  ignoreBuildScript?: boolean;
31
38
  projectSettings?: ProjectSettings;
32
39
  cleanUrls?: boolean;
@@ -119,6 +119,80 @@ var require_constants = __commonJS({
119
119
  }
120
120
  });
121
121
 
122
+ // ../../internals/runtimes/dist/entrypoint.js
123
+ var require_entrypoint = __commonJS({
124
+ "../../internals/runtimes/dist/entrypoint.js"(exports, module2) {
125
+ "use strict";
126
+ var __defProp2 = Object.defineProperty;
127
+ var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
128
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
129
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
130
+ var __export2 = (target, all) => {
131
+ for (var name in all)
132
+ __defProp2(target, name, { get: all[name], enumerable: true });
133
+ };
134
+ var __copyProps2 = (to, from, except, desc) => {
135
+ if (from && typeof from === "object" || typeof from === "function") {
136
+ for (let key of __getOwnPropNames2(from))
137
+ if (!__hasOwnProp2.call(to, key) && key !== except)
138
+ __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
139
+ }
140
+ return to;
141
+ };
142
+ var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
143
+ var entrypoint_exports = {};
144
+ __export2(entrypoint_exports, {
145
+ SCHEDULE_ENTRYPOINT_FORMS_HINT: () => SCHEDULE_ENTRYPOINT_FORMS_HINT,
146
+ isDockerfileEntrypoint: () => isDockerfileEntrypoint,
147
+ parsePyModuleAttrEntrypoint: () => parsePyModuleAttrEntrypoint2,
148
+ parseScheduleEntrypoint: () => parseScheduleEntrypoint,
149
+ stripTrailingSlash: () => stripTrailingSlash
150
+ });
151
+ module2.exports = __toCommonJS2(entrypoint_exports);
152
+ var import_path2 = require("path");
153
+ var import_constants2 = require_constants();
154
+ var PYTHON_MODULE_ATTR_RE = /^([A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)*):([A-Za-z_][\w]*)$/;
155
+ var NODE_FILE_ENTRYPOINT_RE = /^(?!\/)(?!.*(?:^|\/)\.\.?(?:\/|$))(?:[\w@+.-]+\/)*[\w@+.-]+\.[cm]?[jt]s$/;
156
+ var NODE_DECLARATION_ENTRYPOINT_RE = /\.d\.[cm]?ts$/;
157
+ function parsePyModuleAttrEntrypoint2(entrypoint) {
158
+ const match = PYTHON_MODULE_ATTR_RE.exec(entrypoint);
159
+ if (!match)
160
+ return null;
161
+ const [, modulePart, attrName] = match;
162
+ if (!modulePart || !attrName)
163
+ return null;
164
+ return {
165
+ attrName,
166
+ filePath: modulePart.replace(/\./g, "/") + ".py",
167
+ moduleName: modulePart
168
+ };
169
+ }
170
+ var SCHEDULE_ENTRYPOINT_FORMS_HINT = 'Use a source file path like "jobs/nightly.ts" or a Python "module.path:function" reference.';
171
+ function parseScheduleEntrypoint(entrypoint) {
172
+ const python = parsePyModuleAttrEntrypoint2(entrypoint);
173
+ if (python) {
174
+ return { runtime: "python", ...python };
175
+ }
176
+ if (NODE_FILE_ENTRYPOINT_RE.test(entrypoint) && !NODE_DECLARATION_ENTRYPOINT_RE.test(entrypoint)) {
177
+ return { runtime: "node", filePath: entrypoint };
178
+ }
179
+ return null;
180
+ }
181
+ var CONTAINER_ENTRYPOINT_BASENAMES = new Set(
182
+ import_constants2.CONTAINER_ENTRYPOINT_CANDIDATES.map((name) => name.toLowerCase())
183
+ );
184
+ function isDockerfileEntrypoint(entrypoint) {
185
+ return CONTAINER_ENTRYPOINT_BASENAMES.has(
186
+ import_path2.posix.basename(entrypoint).toLowerCase()
187
+ );
188
+ }
189
+ function stripTrailingSlash(p) {
190
+ const stripped = p.replace(/\/+$/, "");
191
+ return stripped === "" ? "." : stripped;
192
+ }
193
+ }
194
+ });
195
+
122
196
  // ../../internals/runtimes/dist/framework.js
123
197
  var require_framework = __commonJS({
124
198
  "../../internals/runtimes/dist/framework.js"(exports, module2) {
@@ -147,6 +221,7 @@ var require_framework = __commonJS({
147
221
  frameworksBySlug: () => frameworksBySlug,
148
222
  inferBuilderRuntimeFromFramework: () => inferBuilderRuntimeFromFramework2,
149
223
  inferRuntimeFromFramework: () => inferRuntimeFromFramework,
224
+ inferServerRuntimeFromFramework: () => inferServerRuntimeFromFramework,
150
225
  isBFFFramework: () => isBFFFramework,
151
226
  isFrontendFramework: () => isFrontendFramework
152
227
  });
@@ -177,6 +252,14 @@ var require_framework = __commonJS({
177
252
  middleman: "ruby",
178
253
  zola: "rust"
179
254
  };
255
+ function inferServerRuntimeFromFramework(framework) {
256
+ if (!framework || framework === "services")
257
+ return void 0;
258
+ const builder = frameworksBySlug.get(framework)?.useRuntime?.use;
259
+ if (!builder)
260
+ return void 0;
261
+ return import_constants2.BUILDER_RUNTIMES[builder] ?? "node";
262
+ }
180
263
  function inferBuilderRuntimeFromFramework2(framework) {
181
264
  if (!framework || framework === "container" || framework === "services")
182
265
  return void 0;
@@ -229,6 +312,7 @@ __export(detect_builders_exports, {
229
312
  });
230
313
  module.exports = __toCommonJS(detect_builders_exports);
231
314
  var import_minimatch = __toESM(require("minimatch"));
315
+ var import_entrypoint = __toESM(require_entrypoint());
232
316
  var import_semver = require("semver");
233
317
  var import_path = require("path");
234
318
  var import_frameworks = require("@vercel/frameworks");
@@ -381,6 +465,13 @@ async function detectBuilders(files, pkg, options = {}) {
381
465
  usedFunctions.add(key);
382
466
  };
383
467
  const absolutePathCache = /* @__PURE__ */ new Map();
468
+ const proxyFile = options.proxy ? parseProxyEntrypoint(options.proxy.entrypoint).file : null;
469
+ const reservedFunctionFiles = options.reservedFunctionFiles ?? [];
470
+ const claimedFiles = /* @__PURE__ */ new Set([
471
+ ...proxyFile !== null ? [proxyFile] : [],
472
+ ...reservedFunctionFiles
473
+ ]);
474
+ const hasReservedFunctions = reservedFunctionFiles.length > 0;
384
475
  const { buildCommand, outputDirectory } = projectSettings;
385
476
  const frameworkConfig = import_framework.frameworksBySlug.get(framework || "");
386
477
  const rustStaticOutput = isRustStaticOutput(framework, projectSettings);
@@ -407,7 +498,7 @@ async function detectBuilders(files, pkg, options = {}) {
407
498
  const apiRoutes = [];
408
499
  const dynamicRoutes = [];
409
500
  for (const fileName of sortedFiles) {
410
- if (fileName === options.proxy?.entrypoint) {
501
+ if (claimedFiles.has(fileName)) {
411
502
  continue;
412
503
  }
413
504
  const apiBuilder = await maybeGetApiBuilder(fileName, apiMatches, options);
@@ -471,7 +562,7 @@ async function detectBuilders(files, pkg, options = {}) {
471
562
  options
472
563
  );
473
564
  } else {
474
- if (pkg && !makeFrontendStatic && !apiBuilders.length && !options.ignoreBuildScript) {
565
+ if (pkg && !makeFrontendStatic && !apiBuilders.length && !hasReservedFunctions && !options.ignoreBuildScript) {
475
566
  errors.push(getMissingBuildScriptError());
476
567
  return {
477
568
  errors,
@@ -492,12 +583,10 @@ async function detectBuilders(files, pkg, options = {}) {
492
583
  outputDirectory: usedOutputDirectory
493
584
  }
494
585
  };
495
- } else if (apiBuilders.length && hasNoneApiFiles) {
586
+ } else if ((apiBuilders.length || hasReservedFunctions) && hasNoneApiFiles) {
496
587
  frontendBuilder = {
497
588
  use: "@vercel/static",
498
- src: getStaticFilesPattern(
499
- options.proxy ? [options.proxy.entrypoint] : []
500
- ),
589
+ src: getStaticFilesPattern([...claimedFiles]),
501
590
  config: {
502
591
  zeroConfig: true
503
592
  }
@@ -523,6 +612,7 @@ async function detectBuilders(files, pkg, options = {}) {
523
612
  const unusedFunctionError = checkUnusedFunctions(
524
613
  frontendBuilder,
525
614
  usedFunctions,
615
+ claimedFiles,
526
616
  options
527
617
  );
528
618
  if (unusedFunctionError) {
@@ -659,14 +749,25 @@ function getFunction(fileName, { functions = {} }) {
659
749
  const func = keys.find((key) => key === fileName || (0, import_minimatch.default)(fileName, key));
660
750
  return func ? { fnPattern: func, func: functions[func] } : { fnPattern: null, func: null };
661
751
  }
752
+ function parseProxyEntrypoint(entrypoint) {
753
+ if (process.env.VERCEL_PYTHON_PROXY) {
754
+ const parsed = (0, import_entrypoint.parsePyModuleAttrEntrypoint)(entrypoint);
755
+ if (parsed)
756
+ return { file: parsed.filePath, variable: parsed.attrName };
757
+ }
758
+ return { file: entrypoint };
759
+ }
662
760
  function getProxyBuilder(proxy, tag, functions = {}) {
663
- const { fnPattern, func } = getFunction(proxy.entrypoint, { functions });
761
+ const { file, variable } = parseProxyEntrypoint(proxy.entrypoint);
762
+ const isPython = (0, import_path.extname)(file) === ".py";
763
+ const { fnPattern, func } = getFunction(file, { functions });
664
764
  const runtime = func?.runtime;
665
765
  const config = {
666
766
  zeroConfig: true,
667
767
  middleware: true,
668
- ...!runtime ? { middlewareRuntime: "nodejs" } : {},
669
- ...proxy.matcher ? { middlewareMatcher: proxy.matcher } : {}
768
+ ...!runtime && !isPython ? { middlewareRuntime: "nodejs" } : {},
769
+ ...proxy.matcher ? { middlewareMatcher: proxy.matcher } : {},
770
+ ...variable ? { handlerFunction: variable } : {}
670
771
  };
671
772
  if (fnPattern && func) {
672
773
  config.functions = { [fnPattern]: func };
@@ -677,9 +778,10 @@ function getProxyBuilder(proxy, tag, functions = {}) {
677
778
  config.excludeFiles = func.excludeFiles;
678
779
  }
679
780
  }
781
+ const defaultRuntime = isPython ? `@vercel/python${tag ? `@${tag}` : ""}` : `@vercel/node${tag ? `@${tag}` : ""}`;
680
782
  return {
681
- src: proxy.entrypoint,
682
- use: runtime || `@vercel/node${tag ? `@${tag}` : ""}`,
783
+ src: file,
784
+ use: runtime || defaultRuntime,
683
785
  config
684
786
  };
685
787
  }
@@ -702,21 +804,29 @@ function validateProxyConfig(proxy) {
702
804
  if (typeof proxy !== "object" || typeof proxy.entrypoint !== "string" || !proxy.entrypoint) {
703
805
  return {
704
806
  code: "invalid_proxy",
705
- message: "The `proxy` property must contain an `entrypoint` string that references a `.js` or `.ts` file."
807
+ message: "The `proxy` property must contain an `entrypoint` string."
706
808
  };
707
809
  }
708
- const entrypoint = proxy.entrypoint;
709
- const segments = entrypoint.split("/");
710
- if (entrypoint.startsWith("/") || entrypoint.includes("\\") || segments.includes(".") || segments.includes("..") || /[?#\u0000-\u001f]/.test(entrypoint)) {
810
+ const { file, variable } = parseProxyEntrypoint(proxy.entrypoint);
811
+ const isPythonForm = (0, import_path.extname)(file) === ".py";
812
+ const isJsForm = /\.(?:js|ts)$/.test(file) && !file.endsWith(".d.ts");
813
+ const segments = file.split("/");
814
+ if (file.startsWith("/") || file.includes("\\") || segments.includes(".") || segments.includes("..") || /[?#\u0000-\u001f]/.test(file)) {
711
815
  return {
712
816
  code: "invalid_proxy_entrypoint",
713
817
  message: "The `proxy.entrypoint` path must be relative to the project root and cannot contain traversal, query, fragment, or control characters."
714
818
  };
715
819
  }
716
- if (!/\.(?:js|ts)$/.test(entrypoint) || entrypoint.endsWith(".d.ts")) {
820
+ if (!isPythonForm && !isJsForm) {
821
+ return {
822
+ code: "invalid_proxy_entrypoint",
823
+ message: "The `proxy.entrypoint` must be a `.js` or `.ts` file path, or a Python `module:attr`."
824
+ };
825
+ }
826
+ if (isPythonForm && !variable) {
717
827
  return {
718
828
  code: "invalid_proxy_entrypoint",
719
- message: "The `proxy.entrypoint` path must end in `.js` or `.ts` and reference an executable file."
829
+ message: "Python proxy entrypoints must use `module:attr` syntax, not a plain `.py` path."
720
830
  };
721
831
  }
722
832
  if (proxy.matcher !== void 0) {
@@ -741,10 +851,11 @@ function validateProxy(options, files, framework) {
741
851
  if (configError) {
742
852
  return configError;
743
853
  }
744
- if (!files.includes(proxy.entrypoint)) {
854
+ const { file } = parseProxyEntrypoint(proxy.entrypoint);
855
+ if (!files.includes(file)) {
745
856
  return {
746
857
  code: "proxy_entrypoint_not_found",
747
- message: `The proxy entrypoint \`${proxy.entrypoint}\` does not exist. Set \`proxy.entrypoint\` to an existing \`.js\` or \`.ts\` file.`
858
+ message: `The proxy entrypoint \`${proxy.entrypoint}\` does not exist.`
748
859
  };
749
860
  }
750
861
  if (framework === "nextjs" || framework === "astro") {
@@ -754,7 +865,8 @@ function validateProxy(options, files, framework) {
754
865
  };
755
866
  }
756
867
  const appRuntime = (0, import_framework.inferBuilderRuntimeFromFramework)(framework);
757
- const proxyRuntime = import_constants.ENTRYPOINT_EXTENSIONS[(0, import_path.extname)(proxy.entrypoint)];
868
+ const { file: proxyFile } = parseProxyEntrypoint(proxy.entrypoint);
869
+ const proxyRuntime = import_constants.ENTRYPOINT_EXTENSIONS[(0, import_path.extname)(proxyFile)];
758
870
  if (appRuntime !== void 0 && proxyRuntime !== void 0 && appRuntime !== proxyRuntime) {
759
871
  return {
760
872
  code: "proxy_runtime_mismatch",
@@ -922,10 +1034,18 @@ function validateFunctions({ functions = {} }) {
922
1034
  }
923
1035
  return null;
924
1036
  }
925
- function checkUnusedFunctions(frontendBuilder, usedFunctions, options) {
1037
+ function checkUnusedFunctions(frontendBuilder, usedFunctions, claimedFiles, options) {
926
1038
  const unusedFunctions = new Set(
927
1039
  Object.keys(options.functions || {}).filter((key) => !usedFunctions.has(key))
928
1040
  );
1041
+ for (const fnKey of unusedFunctions) {
1042
+ for (const filePath of claimedFiles) {
1043
+ if (filePath === fnKey || (0, import_minimatch.default)(filePath, fnKey)) {
1044
+ unusedFunctions.delete(fnKey);
1045
+ break;
1046
+ }
1047
+ }
1048
+ }
929
1049
  if (!unusedFunctions.size) {
930
1050
  return null;
931
1051
  }
@@ -248,7 +248,7 @@ async function resolveConfiguredServiceV2(name, config, fs) {
248
248
  }
249
249
  };
250
250
  }
251
- const buildpackRuntime = flaggedBuildpackRuntime && !entrypointFile && !(0, import_framework.isFrontendFramework)(framework) && await (0, import_buildpack_runtimes.hasBuildpackProjectMarker)(
251
+ const buildpackRuntime = flaggedBuildpackRuntime && (!entrypointFile || flaggedBuildpackRuntime === "ruby" && entrypointFile === "config.ru") && !(0, import_framework.isFrontendFramework)(framework) && await (0, import_buildpack_runtimes.hasBuildpackProjectMarker)(
252
252
  flaggedBuildpackRuntime,
253
253
  (marker) => (0, import_utils.hasFile)(serviceFs, marker)
254
254
  ) ? flaggedBuildpackRuntime : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/fs-detectors",
3
- "version": "7.6.2",
3
+ "version": "7.7.0",
4
4
  "description": "Vercel filesystem detectors",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -20,11 +20,11 @@
20
20
  "minimatch": "3.1.2",
21
21
  "semver": "6.3.1",
22
22
  "smol-toml": "1.5.2",
23
- "@vercel/build-utils": "14.10.2",
24
23
  "@vercel/error-utils": "2.2.1",
24
+ "@vercel/build-utils": "14.11.0",
25
25
  "@vercel/frameworks": "3.34.0",
26
- "@vercel/python-analysis": "0.14.0",
27
- "@vercel/routing-utils": "6.6.0"
26
+ "@vercel/routing-utils": "6.6.0",
27
+ "@vercel/python-analysis": "0.14.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/glob": "7.2.0",
@@ -33,7 +33,7 @@
33
33
  "@types/node": "20.11.0",
34
34
  "@types/semver": "7.3.10",
35
35
  "vitest": "4.1.10",
36
- "@vercel-internals/runtimes": "1.1.1"
36
+ "@vercel-internals/runtimes": "1.1.2"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "node build.mjs",