@vercel/backends 0.8.29 → 0.8.32

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.
package/dist/index.d.mts CHANGED
@@ -3,8 +3,15 @@ import { BuildOptions, BuildV2, DetectEntrypointFn, Files, PrepareCache, ShouldS
3
3
  import { ParseArgsConfig } from "node:util";
4
4
 
5
5
  //#region src/find-entrypoint.d.ts
6
- declare const findEntrypoint: (cwd: string) => Promise<string | undefined>;
7
- declare const findEntrypointOrThrow: (cwd: string) => Promise<string>;
6
+ type FindEntrypointOptions = {
7
+ /**
8
+ * Project setting `outputDirectory`. When set, the search is exclusive to
9
+ * that directory, matching the wrapper builders.
10
+ */
11
+ outputDirectory?: string;
12
+ };
13
+ declare const findEntrypoint: (cwd: string, options?: FindEntrypointOptions) => Promise<string | undefined>;
14
+ declare const findEntrypointOrThrow: (cwd: string, options?: FindEntrypointOptions) => Promise<string>;
8
15
  /**
9
16
  * Normalized entrypoint detector for Node services. Wraps {@link findEntrypoint}
10
17
  * and returns the result in the shared {@link DetectedEntrypoint} shape consumed
package/dist/index.mjs CHANGED
@@ -75,15 +75,64 @@ const diagnostics = createDiagnostics("node");
75
75
 
76
76
  //#endregion
77
77
  //#region src/find-entrypoint.ts
78
- const frameworks = [
79
- "express",
80
- "hono",
81
- "elysia",
82
- "fastify",
83
- "@nestjs/core",
84
- "h3"
78
+ /**
79
+ * Entrypoint detection modeled on the framework wrapper builders
80
+ * (`@vercel/express`, `@vercel/hono`, ... — see
81
+ * `packages/build-utils/src/generate-node-builder-functions.ts`), but
82
+ * deliberately a *permissive union* with this builder's historical behavior
83
+ * so that neither wrapper migrations nor existing `@vercel/backends` flag
84
+ * users start failing:
85
+ *
86
+ * 1. If `outputDirectory` is configured, search there first (wrapper
87
+ * preference) — but fall through to the source tree instead of erroring
88
+ * when nothing is found (historical backends behavior: `outputDirectory`
89
+ * never blocked detection).
90
+ * 2. Search well-known filenames in the project root, requiring the file to
91
+ * import the detected framework (wrapper behavior).
92
+ * 3. Fall back to `package.json#main` when it points at an existing file —
93
+ * without requiring a framework import (historical backends behavior;
94
+ * the wrappers additionally gate `main` on the import, but relaxing the
95
+ * gate only accepts more projects, never fewer).
96
+ *
97
+ * When no framework dependency is recognized, the framework-import gate is
98
+ * skipped and the first existing well-known file (or `main`) wins.
99
+ */
100
+ const entrypointExtensions = [
101
+ "js",
102
+ "cjs",
103
+ "mjs",
104
+ "ts",
105
+ "cts",
106
+ "mts"
107
+ ];
108
+ /**
109
+ * Wrapper builders search app/index/server (+src). Historical backends also
110
+ * searched main/src/main for every framework; keep them appended so
111
+ * existing users don't lose their entrypoint.
112
+ */
113
+ const DEFAULT_FILENAMES = [
114
+ "app",
115
+ "index",
116
+ "server",
117
+ "src/app",
118
+ "src/index",
119
+ "src/server",
120
+ "main",
121
+ "src/main"
85
122
  ];
86
- const entrypointFilenames = [
123
+ /** NestJS conventionally uses `src/main.ts`; its wrapper prefers `src/` first. */
124
+ const NESTJS_FILENAMES = [
125
+ "src/main",
126
+ "src/app",
127
+ "src/index",
128
+ "src/server",
129
+ "main",
130
+ "app",
131
+ "index",
132
+ "server"
133
+ ];
134
+ /** Used when no framework dependency is recognized (historical backends order). */
135
+ const GENERIC_FILENAMES = [
87
136
  "app",
88
137
  "index",
89
138
  "server",
@@ -93,62 +142,129 @@ const entrypointFilenames = [
93
142
  "src/server",
94
143
  "src/main"
95
144
  ];
96
- const entrypointExtensions = [
97
- "js",
98
- "cjs",
99
- "mjs",
100
- "ts",
101
- "cts",
102
- "mts"
145
+ /**
146
+ * `@nestjs/core` is listed first: Nest apps commonly also depend on `express`
147
+ * (via `@nestjs/platform-express`), and their entrypoints import
148
+ * `@nestjs/core`, not `express`.
149
+ */
150
+ const FRAMEWORKS = [
151
+ {
152
+ dependency: "@nestjs/core",
153
+ name: "nestjs",
154
+ filenames: NESTJS_FILENAMES
155
+ },
156
+ {
157
+ dependency: "express",
158
+ name: "express",
159
+ filenames: DEFAULT_FILENAMES
160
+ },
161
+ {
162
+ dependency: "hono",
163
+ name: "hono",
164
+ filenames: DEFAULT_FILENAMES
165
+ },
166
+ {
167
+ dependency: "elysia",
168
+ name: "elysia",
169
+ filenames: DEFAULT_FILENAMES
170
+ },
171
+ {
172
+ dependency: "fastify",
173
+ name: "fastify",
174
+ filenames: DEFAULT_FILENAMES
175
+ },
176
+ {
177
+ dependency: "koa",
178
+ name: "koa",
179
+ filenames: DEFAULT_FILENAMES
180
+ },
181
+ {
182
+ dependency: "h3",
183
+ name: "h3",
184
+ filenames: DEFAULT_FILENAMES
185
+ }
103
186
  ];
104
- const entrypoints = entrypointFilenames.flatMap((filename) => entrypointExtensions.map((extension) => `${filename}.${extension}`));
105
- const createFrameworkRegex = (framework) => new RegExp(`(?:from|require|import)\\s*(?:\\(\\s*)?["']${framework}["']\\s*(?:\\))?`, "g");
106
- const findEntrypoint = async (cwd) => {
187
+ const toCandidates = (filenames) => filenames.flatMap((filename) => entrypointExtensions.map((extension) => `${filename}.${extension}`));
188
+ const entrypointsForMessage = (filenames) => filenames.map((filename) => `- ${filename}.{${entrypointExtensions.join(",")}}`).join("\n");
189
+ const createFrameworkRegex = (dependency) => new RegExp(`(?:from|require|import)\\s*(?:\\(\\s*)?["']${dependency}["']\\s*(?:\\))?`, "g");
190
+ const pluralize = (word, count) => count === 1 ? word : `${word}s`;
191
+ /** Returns true/false for readable files, null when unreadable/missing. */
192
+ const matchesFramework = async (absolutePath, spec) => {
193
+ try {
194
+ return (await readFile(absolutePath, "utf-8")).match(createFrameworkRegex(spec.dependency)) !== null;
195
+ } catch {
196
+ return null;
197
+ }
198
+ };
199
+ const searchDirectory = async (dir, framework, filenames) => {
200
+ const existing = toCandidates(filenames).filter((candidate) => existsSync(join(dir, candidate)));
201
+ if (!framework) return {
202
+ entrypoint: existing[0],
203
+ entrypointsNotMatchingRegex: []
204
+ };
205
+ const matching = [];
206
+ const entrypointsNotMatchingRegex = [];
207
+ for (const candidate of existing) {
208
+ const matches = await matchesFramework(join(dir, candidate), framework);
209
+ if (matches === true) matching.push(candidate);
210
+ else if (matches === false) entrypointsNotMatchingRegex.push(candidate);
211
+ }
212
+ const entrypoint = matching[0];
213
+ if (matching.length > 1) console.warn(`Multiple entrypoints found: ${matching.join(", ")}. Using ${entrypoint}.`);
214
+ return {
215
+ entrypoint,
216
+ entrypointsNotMatchingRegex
217
+ };
218
+ };
219
+ const findMainEntrypoint = async (cwd, packageJsonObject) => {
220
+ const main = packageJsonObject && typeof packageJsonObject.main === "string" ? packageJsonObject.main.trim() : "";
221
+ if (!main) return void 0;
222
+ const abs = resolve(cwd, main);
223
+ const rel = relative(cwd, abs);
224
+ if (rel.startsWith("..") || rel === "") return void 0;
225
+ if (!existsSync(abs)) return void 0;
226
+ try {
227
+ await readFile(abs, "utf-8");
228
+ } catch {
229
+ return;
230
+ }
231
+ return rel.split(sep).join("/");
232
+ };
233
+ const resolveEntrypoint = async (cwd, options) => {
107
234
  let packageJsonObject = null;
108
235
  try {
109
236
  const packageJson = await readFile(join(cwd, "package.json"), "utf-8");
110
237
  packageJsonObject = JSON.parse(packageJson);
111
238
  } catch (_) {}
112
- if (packageJsonObject) {
113
- const main = typeof packageJsonObject.main === "string" ? packageJsonObject.main.trim() : "";
114
- if (main) {
115
- const abs = resolve(cwd, main);
116
- const rel = relative(cwd, abs);
117
- if (!rel.startsWith("..") && rel !== "") try {
118
- await readFile(abs, "utf-8");
119
- return rel.split(sep).join("/");
120
- } catch {}
121
- }
122
- }
123
- let framework;
124
- if (packageJsonObject) framework = frameworks.find((framework$1) => packageJsonObject.dependencies?.[framework$1]);
125
- if (!framework) for (const entrypoint of entrypoints) {
126
- const entrypointPath = join(cwd, entrypoint);
127
- try {
128
- await readFile(entrypointPath, "utf-8");
129
- return entrypoint;
130
- } catch (_) {}
131
- }
132
- const regex = framework ? createFrameworkRegex(framework) : void 0;
133
- for (const entrypoint of entrypoints) {
134
- const entrypointPath = join(cwd, entrypoint);
135
- try {
136
- const content = await readFile(entrypointPath, "utf-8");
137
- if (regex) {
138
- if (regex.test(content)) return entrypoint;
139
- }
140
- } catch (_) {}
239
+ const framework = packageJsonObject ? FRAMEWORKS.find((spec) => packageJsonObject.dependencies?.[spec.dependency]) : void 0;
240
+ const filenames = framework ? framework.filenames : GENERIC_FILENAMES;
241
+ const searchedForMessage = entrypointsForMessage(filenames);
242
+ const noMatchingImportError = (candidates) => /* @__PURE__ */ new Error(`No entrypoint found which imports ${framework.name}. Found possible ${pluralize("entrypoint", candidates.length)}: ${candidates.join(", ")}`);
243
+ const dir = options?.outputDirectory?.replace(/^\/+|\/+$/g, "");
244
+ if (dir) {
245
+ const { entrypoint: outputDirEntrypoint } = await searchDirectory(join(cwd, dir), framework, filenames);
246
+ if (outputDirEntrypoint) return { entrypoint: join(dir, outputDirEntrypoint).split(sep).join("/") };
141
247
  }
248
+ const { entrypoint, entrypointsNotMatchingRegex } = await searchDirectory(cwd, framework, filenames);
249
+ if (entrypoint) return { entrypoint };
250
+ const mainEntrypoint = await findMainEntrypoint(cwd, packageJsonObject);
251
+ if (mainEntrypoint) return { entrypoint: mainEntrypoint };
252
+ if (framework && entrypointsNotMatchingRegex.length > 0) return { error: noMatchingImportError(entrypointsNotMatchingRegex) };
253
+ if (framework) return { error: /* @__PURE__ */ new Error(`No entrypoint found. Searched for:\n${searchedForMessage}`) };
254
+ return { error: /* @__PURE__ */ new Error(`No entrypoint found in "${cwd}". Set package.json "main" to a server file, or add one of: ${toCandidates(filenames).join(", ")}`) };
142
255
  };
143
- const findEntrypointOrThrow = async (cwd) => {
144
- const entrypoint = await findEntrypoint(cwd);
145
- if (!entrypoint) throw new Error(`No entrypoint found in "${cwd}". Set package.json "main" to a server file, or add one of: ${entrypoints.join(", ")}`);
146
- return entrypoint;
256
+ const findEntrypoint = async (cwd, options) => {
257
+ return (await resolveEntrypoint(cwd, options)).entrypoint;
147
258
  };
148
- const findEntrypointWithHintOrThrow = async (workPath, configured) => {
259
+ const findEntrypointOrThrow = async (cwd, options) => {
260
+ const result = await resolveEntrypoint(cwd, options);
261
+ if (result.error) throw result.error;
262
+ return result.entrypoint;
263
+ };
264
+ const findEntrypointWithHintOrThrow = async (workPath, configured, options) => {
149
265
  const explicit = configured && configured !== "package.json" ? configured : null;
150
266
  if (explicit && existsSync(join(workPath, explicit))) return explicit;
151
- return findEntrypointOrThrow(workPath);
267
+ return findEntrypointOrThrow(workPath, options);
152
268
  };
153
269
  /**
154
270
  * Normalized entrypoint detector for Node services. Wraps {@link findEntrypoint}
@@ -807,22 +923,12 @@ const Colors = {
807
923
 
808
924
  //#endregion
809
925
  //#region src/typescript.ts
810
- /**
811
- * Typecheck via the TypeScript compiler API (`createProgram`, `getPreEmitDiagnostics`),
812
- * not by spawning the `tsc` binary.
813
- *
814
- * We only want to validate the deployment entrypoint and its import graph, not every
815
- * file matched by `tsconfig` `include`. The CLI cannot combine `--project` with explicit
816
- * root files (TS5042), so expressing 'project options + entry-only roots' in one `tsc`
817
- * call requires a generated tsconfig on disk. Writing beside the user's config is
818
- * invasive; a temp config elsewhere often breaks `node_modules` / `@types` resolution
819
- * relative to the real project. The API lets us reuse `parseJsonConfigFileContent` (same
820
- * options as `-p`) with explicit `rootNames`, no files written, and a compiler host whose
821
- * current directory stays `workPath`.
822
- *
823
- * The `typescript` package is resolved with `require` from the user's app (peer dependency), not bundled.
824
- */
825
926
  const require_$1 = createRequire(import.meta.url);
927
+ const IGNORED_DIAGNOSTIC_CODES = new Set([
928
+ 6059,
929
+ 18002,
930
+ 18003
931
+ ]);
826
932
  const typescript = (args) => {
827
933
  const { span } = args;
828
934
  return span.child("vc.builder.backends.tsCompile").trace(async () => {
@@ -832,7 +938,7 @@ const typescript = (args) => {
832
938
  ".mts",
833
939
  ".cts"
834
940
  ].includes(extension)) return;
835
- const ts = resolveTypeScriptModule(args.workPath);
941
+ const ts = resolveTypeScriptModule(dirname(resolve(args.workPath, args.entrypoint)));
836
942
  if (!ts) {
837
943
  console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck skipped ${Colors.gray("(TypeScript not found)")}`));
838
944
  return null;
@@ -842,66 +948,95 @@ const typescript = (args) => {
842
948
  };
843
949
  async function doTypeCheck(args, ts) {
844
950
  const entryAbsolute = resolve(args.workPath, args.entrypoint);
845
- const tsconfig = await findNearestTsconfig(args.workPath);
951
+ const tsconfig = findNearestTsconfig(dirname(entryAbsolute));
846
952
  const formatDiagnostics = process.stdout.isTTY ? ts.formatDiagnosticsWithColorAndContext : ts.formatDiagnostics;
847
953
  const diagnosticHost = {
848
954
  getNewLine: () => ts.sys.newLine,
849
955
  getCanonicalFileName: (fileName) => ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
850
956
  getCurrentDirectory: () => args.workPath
851
957
  };
958
+ const filterIgnored = (diagnostics$1) => diagnostics$1.filter((d) => !IGNORED_DIAGNOSTIC_CODES.has(d.code));
959
+ const fail = (diagnostics$1) => {
960
+ const message = formatDiagnostics(diagnostics$1, diagnosticHost);
961
+ console.error("\nTypeScript type check failed:\n");
962
+ console.error(message);
963
+ throw new Error("TypeScript type check failed");
964
+ };
852
965
  let options;
853
- let parseDiagnostics = [];
966
+ const rootNames = [entryAbsolute];
854
967
  if (tsconfig) {
855
968
  const configRead = ts.readConfigFile(tsconfig, ts.sys.readFile);
856
- if (configRead.error) {
857
- const message = formatDiagnostics([configRead.error], diagnosticHost);
858
- console.error("\nTypeScript type check failed:\n");
859
- console.error(message);
860
- throw new Error("TypeScript type check failed");
861
- }
862
- const parsed = ts.parseJsonConfigFileContent(configRead.config, ts.sys, dirname(tsconfig), void 0, tsconfig);
863
- parseDiagnostics = parsed.errors;
969
+ if (configRead.error) fail([configRead.error]);
970
+ const config = configRead.config ?? {};
971
+ const parsed = ts.parseJsonConfigFileContent(config, ts.sys, dirname(tsconfig), void 0, tsconfig);
972
+ const parseErrors = filterIgnored(parsed.errors).filter((d) => d.category === ts.DiagnosticCategory.Error);
973
+ if (parseErrors.length > 0) if (parsed.options.noEmitOnError) fail(parseErrors);
974
+ else console.error(formatDiagnostics(parseErrors, diagnosticHost));
864
975
  options = {
865
976
  ...parsed.options,
866
977
  noEmit: true,
867
978
  skipLibCheck: true,
868
- allowJs: true,
869
- esModuleInterop: true
979
+ allowJs: true
870
980
  };
981
+ delete options.out;
982
+ delete options.outFile;
983
+ delete options.composite;
984
+ delete options.declarationDir;
985
+ delete options.declarationMap;
986
+ delete options.emitDeclarationOnly;
987
+ delete options.tsBuildInfoFile;
988
+ delete options.incremental;
989
+ if (options.target === void 0) options.target = defaultScriptTarget(ts, args.nodeVersionMajor);
990
+ if (options.esModuleInterop === void 0) options.esModuleInterop = true;
991
+ if (options.module === void 0 && options.moduleResolution === void 0) {
992
+ options.module = ts.ModuleKind.NodeNext;
993
+ options.moduleResolution = ts.ModuleResolutionKind.NodeNext;
994
+ options.strict = false;
995
+ }
996
+ for (const fileName of parsed.fileNames) if (/\.d\.(ts|mts|cts)$/.test(fileName) && fileName !== entryAbsolute) rootNames.push(fileName);
871
997
  } else options = {
872
998
  noEmit: true,
873
999
  skipLibCheck: true,
874
1000
  allowJs: true,
875
1001
  esModuleInterop: true,
876
- target: ts.ScriptTarget.ES2022,
1002
+ target: defaultScriptTarget(ts, args.nodeVersionMajor),
877
1003
  module: ts.ModuleKind.NodeNext,
878
- moduleResolution: ts.ModuleResolutionKind.NodeNext
1004
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
1005
+ strict: false
879
1006
  };
880
1007
  const compilerHost = ts.createCompilerHost(options);
881
1008
  compilerHost.getCurrentDirectory = () => args.workPath;
882
- const program = ts.createProgram([entryAbsolute], options, compilerHost);
883
- const errors = [...parseDiagnostics, ...ts.getPreEmitDiagnostics(program)].filter((d) => d.category === ts.DiagnosticCategory.Error);
1009
+ const program = ts.createProgram(rootNames, options, compilerHost);
1010
+ const errors = filterIgnored(ts.getPreEmitDiagnostics(program)).filter((d) => d.category === ts.DiagnosticCategory.Error);
884
1011
  if (errors.length === 0) {
885
1012
  console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck complete`));
886
1013
  return;
887
1014
  }
888
- const output = formatDiagnostics(errors, diagnosticHost);
889
- console.error("\nTypeScript type check failed:\n");
890
- console.error(output);
891
- throw new Error("TypeScript type check failed");
1015
+ fail(errors);
1016
+ }
1017
+ function defaultScriptTarget(ts, nodeVersionMajor = 16) {
1018
+ if (nodeVersionMajor >= 16) return ts.ScriptTarget.ES2021;
1019
+ if (nodeVersionMajor >= 14) return ts.ScriptTarget.ES2020;
1020
+ return ts.ScriptTarget.ES2019;
892
1021
  }
893
- function resolveTypeScriptModule(workPath) {
1022
+ function resolveTypeScriptModule(startDir) {
894
1023
  try {
895
- return require_$1(require_$1.resolve("typescript", { paths: [workPath] }));
1024
+ const ts = require_$1(require_$1.resolve("typescript", { paths: [startDir] }));
1025
+ console.log(`Using TypeScript ${ts.version} (local user-provided)`);
1026
+ return ts;
896
1027
  } catch (_e) {
897
1028
  return null;
898
1029
  }
899
1030
  }
900
- const findNearestTsconfig = async (workPath) => {
901
- const tsconfigPath = join(workPath, "tsconfig.json");
902
- if (existsSync(tsconfigPath)) return tsconfigPath;
903
- if (workPath === "/") return;
904
- return findNearestTsconfig(join(workPath, ".."));
1031
+ const findNearestTsconfig = (startDir) => {
1032
+ let dir = resolve(startDir);
1033
+ for (;;) {
1034
+ const tsconfigPath = join(dir, "tsconfig.json");
1035
+ if (existsSync(tsconfigPath)) return tsconfigPath;
1036
+ const parent = dirname(dir);
1037
+ if (parent === dir) return;
1038
+ dir = parent;
1039
+ }
905
1040
  };
906
1041
 
907
1042
  //#endregion
@@ -2136,7 +2271,8 @@ const build = async (args) => {
2136
2271
  span.setAttributes({ "builder.name": builderName });
2137
2272
  const buildSpan = span.child("vc.builder.backends.build");
2138
2273
  return buildSpan.trace(async () => {
2139
- const entrypoint = await findEntrypointWithHintOrThrow(args.workPath, args.entrypoint);
2274
+ const userBuildResult = await maybeDoBuildCommand(args, downloadResult);
2275
+ const entrypoint = await findEntrypointWithHintOrThrow(args.workPath, args.entrypoint, { outputDirectory: getOutputDirectorySetting(args.config) });
2140
2276
  debug("Entrypoint", entrypoint);
2141
2277
  args.entrypoint = entrypoint;
2142
2278
  const serviceName = typeof args.config?.serviceName === "string" && args.config.serviceName !== "" ? args.config.serviceName : void 0;
@@ -2152,7 +2288,6 @@ const build = async (args) => {
2152
2288
  entrypoint
2153
2289
  });
2154
2290
  const isCronService = cronEntries !== void 0;
2155
- const userBuildResult = await maybeDoBuildCommand(args, downloadResult);
2156
2291
  const preDeployCommand = args.config.preDeployCommand;
2157
2292
  if (args.registerPreDeploy && typeof preDeployCommand === "string") {
2158
2293
  const nodeBinPath = getNodeBinPaths({
@@ -2199,7 +2334,8 @@ const build = async (args) => {
2199
2334
  } else typescriptPromise = typescript({
2200
2335
  entrypoint,
2201
2336
  workPath: args.workPath,
2202
- span: buildSpan
2337
+ span: buildSpan,
2338
+ nodeVersionMajor: nodeVersion.major
2203
2339
  });
2204
2340
  const localBuildFiles = userBuildResult?.localBuildFiles.size > 0 ? userBuildResult?.localBuildFiles : rolldownResult.localBuildFiles;
2205
2341
  const files = userBuildResult?.files || rolldownResult.files;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/backends",
3
- "version": "0.8.29",
3
+ "version": "0.8.32",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./dist/index.mjs",
6
6
  "homepage": "https://vercel.com/docs",
@@ -37,8 +37,8 @@
37
37
  "ts-morph": "12.0.0",
38
38
  "tsx": "4.21.0",
39
39
  "zod": "3.22.4",
40
- "@vercel/build-utils": "13.36.2",
41
- "@vercel/static-config": "3.4.0"
40
+ "@vercel/build-utils": "14.0.1",
41
+ "@vercel/static-config": "3.4.1"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/express": "5.0.3",