@vercel/backends 0.1.0-canary.20260211174907.cdd2da6 → 0.1.1

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
@@ -97,9 +97,12 @@ declare const introspectApp: (args: {
97
97
  additionalDeps: string[];
98
98
  }>;
99
99
  //#endregion
100
+ //#region src/diagnostics.d.ts
101
+ declare const diagnostics: _vercel_build_utils0.Diagnostics;
102
+ //#endregion
100
103
  //#region src/index.d.ts
101
104
  declare const version = 2;
102
105
  declare const build: BuildV2;
103
106
  declare const prepareCache: PrepareCache;
104
107
  //#endregion
105
- export { type CervelBuildOptions, type CervelServeOptions, type PathOptions, build, build$1 as cervelBuild, serve as cervelServe, findEntrypoint, findEntrypointOrThrow, getBuildSummary, introspectApp, nodeFileTrace, prepareCache, srvxOptions, version };
108
+ export { type CervelBuildOptions, type CervelServeOptions, type PathOptions, build, build$1 as cervelBuild, serve as cervelServe, diagnostics, findEntrypoint, findEntrypointOrThrow, getBuildSummary, introspectApp, nodeFileTrace, prepareCache, srvxOptions, version };
package/dist/index.mjs CHANGED
@@ -1,20 +1,20 @@
1
1
  import { builtinModules, createRequire } from "node:module";
2
- import { delimiter, dirname, extname, join } from "path";
3
- import { FileBlob, FileFsRef, NodejsLambda, Span, debug, defaultCachePathGlob, download, execCommand, getEnvForPackageManager, getNodeBinPaths, getNodeVersion, glob, isBackendFramework, isBunVersion, isExperimentalBackendsWithoutIntrospectionEnabled, runNpmInstall, runPackageJsonScript, scanParentDirs } from "@vercel/build-utils";
2
+ import path, { delimiter, dirname, join } from "path";
3
+ import { FileBlob, FileFsRef, MANIFEST_VERSION, NodejsLambda, Span, createDiagnostics, debug, defaultCachePathGlob, download, execCommand, getEnvForPackageManager, getLambdaOptionsFromFunction, getNodeBinPaths, getNodeVersion, glob, isBackendFramework, isBunVersion, isExperimentalBackendsWithoutIntrospectionEnabled, runNpmInstall, runPackageJsonScript, scanParentDirs, writeProjectManifest } from "@vercel/build-utils";
4
+ import fs from "fs";
5
+ import yaml from "js-yaml";
6
+ import { parseSyml } from "@yarnpkg/parsers";
4
7
  import { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
5
8
  import { lstat, readFile, rm, stat } from "node:fs/promises";
6
- import { dirname as dirname$1, extname as extname$1, isAbsolute, join as join$1, relative } from "node:path";
9
+ import { basename, dirname as dirname$1, extname, isAbsolute, join as join$1, relative, resolve, sep } from "node:path";
7
10
  import { build as build$2 } from "rolldown";
8
11
  import { exports } from "resolve.exports";
9
12
  import { isNativeError } from "node:util/types";
10
- import { nodeFileTrace as nodeFileTrace$1, resolve } from "@vercel/nft";
13
+ import { nodeFileTrace as nodeFileTrace$1, resolve as resolve$1 } from "@vercel/nft";
11
14
  import { transform } from "oxc-transform";
12
- import { createRequire as createRequire$1 } from "module";
13
- import { spawn } from "child_process";
14
- import { existsSync as existsSync$1 } from "fs";
15
15
  import execa from "execa";
16
16
  import { readFile as readFile$1, writeFile } from "fs/promises";
17
- import { spawn as spawn$1 } from "node:child_process";
17
+ import { spawn } from "node:child_process";
18
18
  import { tmpdir } from "node:os";
19
19
  import { z } from "zod";
20
20
 
@@ -23,7 +23,7 @@ async function downloadInstallAndBundle(args) {
23
23
  const { entrypoint, files, workPath, meta, config, repoRootPath } = args;
24
24
  await download(files, workPath, meta);
25
25
  const entrypointFsDirname = join(workPath, dirname(entrypoint));
26
- const { cliType, lockfileVersion, packageJsonPackageManager, turboSupportsCorepackHome } = await scanParentDirs(entrypointFsDirname, true, repoRootPath);
26
+ const { cliType, lockfilePath, lockfileVersion, packageJsonPackageManager, turboSupportsCorepackHome } = await scanParentDirs(entrypointFsDirname, true, repoRootPath);
27
27
  const spawnEnv = getEnvForPackageManager({
28
28
  cliType,
29
29
  lockfileVersion,
@@ -43,10 +43,13 @@ async function downloadInstallAndBundle(args) {
43
43
  else await runNpmInstall(entrypointFsDirname, [], { env: spawnEnv }, meta, config.projectSettings?.createdAt);
44
44
  return {
45
45
  entrypointFsDirname,
46
- spawnEnv
46
+ spawnEnv,
47
+ cliType,
48
+ lockfilePath,
49
+ lockfileVersion
47
50
  };
48
51
  }
49
- async function maybeExecBuildCommand(args, { spawnEnv, entrypointFsDirname }) {
52
+ async function maybeExecBuildCommand(args, { spawnEnv }) {
50
53
  const projectBuildCommand = args.config.projectSettings?.buildCommand;
51
54
  if (projectBuildCommand) {
52
55
  const nodeBinPath = getNodeBinPaths({
@@ -61,8 +64,351 @@ async function maybeExecBuildCommand(args, { spawnEnv, entrypointFsDirname }) {
61
64
  cwd: args.workPath
62
65
  });
63
66
  }
64
- return runPackageJsonScript(entrypointFsDirname, ["build"], { env: spawnEnv }, args.config.projectSettings?.createdAt);
67
+ return runPackageJsonScript(args.workPath, ["build"], { env: spawnEnv }, args.config.projectSettings?.createdAt);
68
+ }
69
+
70
+ //#endregion
71
+ //#region src/diagnostics.ts
72
+ function classifySource(resolvedUrl) {
73
+ if (!resolvedUrl) return {};
74
+ if (resolvedUrl.startsWith("file:")) return {
75
+ source: "file",
76
+ sourceUrl: resolvedUrl.slice(5)
77
+ };
78
+ if (resolvedUrl.startsWith("git+") || resolvedUrl.startsWith("git://")) return {
79
+ source: "git",
80
+ sourceUrl: resolvedUrl.replace(/^git\+/, "")
81
+ };
82
+ try {
83
+ return {
84
+ source: "registry",
85
+ sourceUrl: new URL(resolvedUrl).origin
86
+ };
87
+ } catch {
88
+ return {};
89
+ }
90
+ }
91
+ function npmEntryScopes(entry) {
92
+ const scopes = [];
93
+ if (entry.dev) scopes.push("dev");
94
+ if (entry.peer) scopes.push("peer");
95
+ if (entry.optional) scopes.push("optional");
96
+ if (scopes.length === 0) scopes.push("prod");
97
+ return scopes;
98
+ }
99
+ function parseNpmLock(content, lockfileVersion) {
100
+ const lockMap = /* @__PURE__ */ new Map();
101
+ const parsed = JSON.parse(content);
102
+ if ((lockfileVersion ?? parsed.lockfileVersion ?? 1) >= 2) {
103
+ const packages = parsed.packages;
104
+ if (!packages) return lockMap;
105
+ for (const [key, entry] of Object.entries(packages)) {
106
+ if (key === "") continue;
107
+ if (!key.startsWith("node_modules/")) continue;
108
+ if (entry.link === true) continue;
109
+ const rest = key.slice(13);
110
+ const isScoped = rest.startsWith("@");
111
+ const slashCount = (rest.match(/\//g) ?? []).length;
112
+ if (isScoped ? slashCount !== 1 : slashCount !== 0) continue;
113
+ const resolved = entry.resolved;
114
+ if (resolved?.startsWith("file:")) continue;
115
+ const version$1 = entry.version ?? "";
116
+ const existing = lockMap.get(rest);
117
+ if (existing && !isHigherVersion(version$1, existing.resolved)) continue;
118
+ const { source, sourceUrl } = classifySource(resolved);
119
+ const lockEntry = {
120
+ resolved: version$1,
121
+ scopes: npmEntryScopes(entry)
122
+ };
123
+ if (source) lockEntry.source = source;
124
+ if (sourceUrl) lockEntry.sourceUrl = sourceUrl;
125
+ lockMap.set(rest, lockEntry);
126
+ }
127
+ } else {
128
+ const dependencies = parsed.dependencies;
129
+ if (!dependencies) return lockMap;
130
+ const walk = (deps) => {
131
+ for (const [name, entry] of Object.entries(deps)) {
132
+ const resolved = entry.resolved;
133
+ if (!resolved?.startsWith("file:")) {
134
+ const version$1 = entry.version ?? "";
135
+ const existing = lockMap.get(name);
136
+ if (!existing || isHigherVersion(version$1, existing.resolved)) {
137
+ const { source, sourceUrl } = classifySource(resolved);
138
+ const lockEntry = {
139
+ resolved: version$1,
140
+ scopes: npmEntryScopes(entry)
141
+ };
142
+ if (source) lockEntry.source = source;
143
+ if (sourceUrl) lockEntry.sourceUrl = sourceUrl;
144
+ lockMap.set(name, lockEntry);
145
+ }
146
+ }
147
+ const nested = entry.dependencies;
148
+ if (nested) walk(nested);
149
+ }
150
+ };
151
+ walk(dependencies);
152
+ }
153
+ return lockMap;
154
+ }
155
+ function isHigherVersion(a, b) {
156
+ const seg = (v) => v.split(/[.\-+]/).map((s) => parseInt(s, 10) || 0);
157
+ const pa = seg(a);
158
+ const pb = seg(b);
159
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
160
+ if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
161
+ if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
162
+ }
163
+ return false;
164
+ }
165
+ function extractPackageName(spec) {
166
+ const s = spec.replace(/\(.*\)$/, "");
167
+ if (s.startsWith("@")) {
168
+ const i$1 = s.indexOf("@", 1);
169
+ return i$1 === -1 ? null : s.slice(0, i$1);
170
+ }
171
+ const i = s.lastIndexOf("@");
172
+ return i <= 0 ? null : s.slice(0, i);
173
+ }
174
+ function parsePnpmV9Key(key) {
175
+ const name = extractPackageName(key);
176
+ if (!name) return null;
177
+ return {
178
+ name,
179
+ version: key.replace(/\(.*\)$/, "").slice(name.length + 1)
180
+ };
181
+ }
182
+ function parsePnpmV5Key(key) {
183
+ if (!key.startsWith("/")) return null;
184
+ const rest = key.slice(1);
185
+ if (rest.startsWith("@")) {
186
+ const firstSlash = rest.indexOf("/");
187
+ if (firstSlash === -1) return null;
188
+ const secondSlash = rest.indexOf("/", firstSlash + 1);
189
+ if (secondSlash === -1) return null;
190
+ return {
191
+ name: rest.slice(0, secondSlash),
192
+ version: rest.slice(secondSlash + 1).split("_")[0]
193
+ };
194
+ }
195
+ const slashIndex = rest.indexOf("/");
196
+ if (slashIndex === -1) return null;
197
+ return {
198
+ name: rest.slice(0, slashIndex),
199
+ version: rest.slice(slashIndex + 1).split("_")[0]
200
+ };
201
+ }
202
+ function parsePnpmV6Key(key) {
203
+ if (!key.startsWith("/")) return null;
204
+ const rest = key.slice(1);
205
+ let atIdx;
206
+ if (rest.startsWith("@")) atIdx = rest.indexOf("@", 1);
207
+ else atIdx = rest.indexOf("@");
208
+ if (atIdx === -1) return null;
209
+ return {
210
+ name: rest.slice(0, atIdx),
211
+ version: rest.slice(atIdx + 1).split("_")[0].replace(/\(.*\)$/, "")
212
+ };
213
+ }
214
+ function classifyPnpmResolution(resolution) {
215
+ if (!resolution) return {};
216
+ if (resolution.type === "directory" || resolution.directory) return { local: true };
217
+ if (typeof resolution.tarball === "string") return classifySource(resolution.tarball);
218
+ if (resolution.type === "git" || typeof resolution.repo === "string") return {
219
+ source: "git",
220
+ sourceUrl: resolution.repo ?? void 0
221
+ };
222
+ return {};
223
+ }
224
+ function parsePnpmLock(content, lockfileVersion) {
225
+ const lockMap = /* @__PURE__ */ new Map();
226
+ const docs = [];
227
+ yaml.safeLoadAll(content, (doc) => docs.push(doc));
228
+ const parsedYaml = docs[0];
229
+ if (!parsedYaml) return lockMap;
230
+ const lv = lockfileVersion ?? Number(parsedYaml.lockfileVersion ?? "0");
231
+ const packages = parsedYaml.packages;
232
+ if (!packages) return lockMap;
233
+ const parseKey = lv >= 9 ? parsePnpmV9Key : lv >= 6 ? parsePnpmV6Key : parsePnpmV5Key;
234
+ for (const [key, entry] of Object.entries(packages)) {
235
+ const keyParsed = parseKey(key);
236
+ if (!keyParsed) continue;
237
+ const { name, version: version$1 } = keyParsed;
238
+ const resolution = entry.resolution;
239
+ const { local, source, sourceUrl } = classifyPnpmResolution(resolution);
240
+ if (local) continue;
241
+ const existing = lockMap.get(name);
242
+ if (existing && !isHigherVersion(version$1, existing.resolved)) continue;
243
+ const lockEntry = {
244
+ resolved: version$1,
245
+ scopes: ["prod"]
246
+ };
247
+ if (source) lockEntry.source = source;
248
+ if (sourceUrl) lockEntry.sourceUrl = sourceUrl;
249
+ lockMap.set(name, lockEntry);
250
+ }
251
+ return lockMap;
252
+ }
253
+ function parseYarnLock(content, lockfileVersion) {
254
+ const lockMap = /* @__PURE__ */ new Map();
255
+ const isBerry = (lockfileVersion ?? 1) >= 2;
256
+ const parsed = parseSyml(content);
257
+ for (const [key, entry] of Object.entries(parsed)) {
258
+ if (key === "__metadata" || !entry) continue;
259
+ if (isBerry && entry.linkType === "soft") continue;
260
+ const version$1 = entry.version;
261
+ if (!version$1) continue;
262
+ let source;
263
+ let sourceUrl;
264
+ if (!isBerry && entry.resolved) {
265
+ if (entry.resolved.startsWith("file:")) continue;
266
+ const classified = classifySource(entry.resolved);
267
+ source = classified.source;
268
+ sourceUrl = classified.sourceUrl;
269
+ }
270
+ const specifiers = key.split(",").map((s) => s.trim().replace(/^"|"$/g, ""));
271
+ let name = null;
272
+ for (const spec of specifiers) {
273
+ name = extractPackageName(spec);
274
+ if (name) break;
275
+ }
276
+ if (!name) continue;
277
+ const existing = lockMap.get(name);
278
+ if (existing && !isHigherVersion(version$1, existing.resolved)) continue;
279
+ const lockEntry = {
280
+ resolved: version$1,
281
+ scopes: ["prod"]
282
+ };
283
+ if (source) lockEntry.source = source;
284
+ if (sourceUrl) lockEntry.sourceUrl = sourceUrl;
285
+ lockMap.set(name, lockEntry);
286
+ }
287
+ return lockMap;
288
+ }
289
+ function parseBunLock(content) {
290
+ const lockMap = /* @__PURE__ */ new Map();
291
+ const json = content.replace(/,(\s*[}\]])/g, "$1");
292
+ const packages = JSON.parse(json).packages;
293
+ if (!packages) return lockMap;
294
+ for (const [name, value] of Object.entries(packages)) {
295
+ if (!Array.isArray(value)) continue;
296
+ const ref = value[0];
297
+ if (!ref || typeof ref !== "string") continue;
298
+ const pkgName = extractPackageName(ref);
299
+ if (!pkgName) continue;
300
+ const version$1 = ref.slice(pkgName.length + 1);
301
+ if (!version$1) continue;
302
+ if (version$1.startsWith("file:") || version$1.startsWith("workspace:")) continue;
303
+ const existingBun = lockMap.get(name);
304
+ if (existingBun && !isHigherVersion(version$1, existingBun.resolved)) continue;
305
+ lockMap.set(name, {
306
+ resolved: version$1,
307
+ scopes: ["prod"]
308
+ });
309
+ }
310
+ return lockMap;
311
+ }
312
+ async function parseLockfile(cliType, lockfilePath, lockfileVersion) {
313
+ if (cliType === "bun" && lockfileVersion === 0) return /* @__PURE__ */ new Map();
314
+ if (cliType === "vlt") return /* @__PURE__ */ new Map();
315
+ const content = await fs.promises.readFile(lockfilePath, "utf-8");
316
+ switch (cliType) {
317
+ case "npm": return parseNpmLock(content, lockfileVersion);
318
+ case "pnpm": return parsePnpmLock(content, lockfileVersion);
319
+ case "yarn": return parseYarnLock(content, lockfileVersion);
320
+ case "bun": return parseBunLock(content);
321
+ default: return /* @__PURE__ */ new Map();
322
+ }
323
+ }
324
+ async function readPackageJson(startDir) {
325
+ let current = startDir;
326
+ for (;;) try {
327
+ const content = await fs.promises.readFile(path.join(current, "package.json"), "utf-8");
328
+ return JSON.parse(content);
329
+ } catch {
330
+ const parent = path.dirname(current);
331
+ if (parent === current) return null;
332
+ current = parent;
333
+ }
65
334
  }
335
+ function buildDirectMaps(pkgJson) {
336
+ const directScopes = /* @__PURE__ */ new Map();
337
+ const directRequested = /* @__PURE__ */ new Map();
338
+ const add = (deps, scope) => {
339
+ if (!deps || typeof deps !== "object") return;
340
+ for (const [name, specifier] of Object.entries(deps)) {
341
+ if (!directScopes.has(name)) directScopes.set(name, /* @__PURE__ */ new Set());
342
+ directScopes.get(name).add(scope);
343
+ if (!directRequested.has(name)) directRequested.set(name, specifier);
344
+ }
345
+ };
346
+ add(pkgJson.dependencies, "prod");
347
+ add(pkgJson.devDependencies, "dev");
348
+ add(pkgJson.peerDependencies, "peer");
349
+ add(pkgJson.optionalDependencies, "optional");
350
+ return {
351
+ directScopes,
352
+ directRequested
353
+ };
354
+ }
355
+ async function generateProjectManifest({ workPath, nodeVersion, cliType, lockfilePath, lockfileVersion }) {
356
+ try {
357
+ const pkgJson = await readPackageJson(workPath);
358
+ if (!pkgJson) return;
359
+ const { directScopes, directRequested } = buildDirectMaps(pkgJson);
360
+ const lockMap = lockfilePath ? await parseLockfile(cliType, lockfilePath, lockfileVersion) : /* @__PURE__ */ new Map();
361
+ const directDeps = [];
362
+ const transitiveDeps = [];
363
+ for (const [name, scopes] of directScopes) {
364
+ const lock = lockMap.get(name);
365
+ const dep = {
366
+ name,
367
+ type: "direct",
368
+ scopes: [...scopes].sort(),
369
+ requested: directRequested.get(name),
370
+ resolved: lock?.resolved ?? ""
371
+ };
372
+ if (lock?.source) dep.source = lock.source;
373
+ if (lock?.sourceUrl) dep.sourceUrl = lock.sourceUrl;
374
+ directDeps.push(dep);
375
+ }
376
+ for (const [name, lock] of lockMap) {
377
+ if (directScopes.has(name)) continue;
378
+ const dep = {
379
+ name,
380
+ type: "transitive",
381
+ scopes: lock.scopes,
382
+ resolved: lock.resolved
383
+ };
384
+ if (lock.source) dep.source = lock.source;
385
+ if (lock.sourceUrl) dep.sourceUrl = lock.sourceUrl;
386
+ transitiveDeps.push(dep);
387
+ }
388
+ const runtimeVersion = { resolved: String(nodeVersion.major) };
389
+ const enginesNode = pkgJson.engines?.node;
390
+ if (enginesNode) {
391
+ runtimeVersion.requested = enginesNode;
392
+ runtimeVersion.requestedSource = "package.json";
393
+ } else for (const filename of [".node-version", ".nvmrc"]) try {
394
+ const trimmed = (await fs.promises.readFile(path.join(workPath, filename), "utf-8")).trim();
395
+ if (trimmed) {
396
+ runtimeVersion.requested = trimmed;
397
+ runtimeVersion.requestedSource = filename;
398
+ break;
399
+ }
400
+ } catch {}
401
+ await writeProjectManifest({
402
+ version: MANIFEST_VERSION,
403
+ runtime: "node",
404
+ runtimeVersion,
405
+ dependencies: [...directDeps.sort((a, b) => a.name.localeCompare(b.name)), ...transitiveDeps.sort((a, b) => a.name.localeCompare(b.name))]
406
+ }, workPath, "node");
407
+ } catch (err) {
408
+ debug(`generateProjectManifest: ${err instanceof Error ? err.message : String(err)}`);
409
+ }
410
+ }
411
+ const diagnostics = createDiagnostics("node");
66
412
 
67
413
  //#endregion
68
414
  //#region src/cervel/plugin.ts
@@ -93,7 +439,7 @@ const plugin = (args) => {
93
439
  * Determine if a resolved module is CommonJS based on package.json exports
94
440
  */
95
441
  const isCommonJS = async (bareImport, resolvedPath, resolvedInfo) => {
96
- const ext = extname$1(resolvedPath);
442
+ const ext = extname(resolvedPath);
97
443
  if (ext === ".cjs") return true;
98
444
  if (ext === ".mjs") return false;
99
445
  if (ext === ".js" || ext === ".ts") {
@@ -231,7 +577,7 @@ const nodeFileTrace = async (args) => {
231
577
  ts: true,
232
578
  mixedModules: true,
233
579
  async resolve(id, parent, job, cjsResolve) {
234
- return resolve(id, parent, job, cjsResolve);
580
+ return resolve$1(id, parent, job, cjsResolve);
235
581
  },
236
582
  async readFile(fsPath) {
237
583
  try {
@@ -276,7 +622,7 @@ var __dirname = typeof __dirname !== 'undefined' ? __dirname : __dirname_(__file
276
622
  const rolldown$1 = async (args) => {
277
623
  const entrypointPath = join$1(args.workPath, args.entrypoint);
278
624
  const outputDir = join$1(args.workPath, args.out);
279
- const extension = extname$1(args.entrypoint);
625
+ const extension = extname(args.entrypoint);
280
626
  const extensionMap = {
281
627
  ".ts": {
282
628
  format: "auto",
@@ -407,9 +753,24 @@ const Colors = {
407
753
  };
408
754
 
409
755
  //#endregion
410
- //#region src/cervel/typescript.ts
411
- const require_$1 = createRequire$1(import.meta.url);
412
- const typescript$1 = (args) => {
756
+ //#region src/typescript.ts
757
+ /**
758
+ * Typecheck via the TypeScript compiler API (`createProgram`, `getPreEmitDiagnostics`),
759
+ * not by spawning the `tsc` binary.
760
+ *
761
+ * We only want to validate the deployment entrypoint and its import graph, not every
762
+ * file matched by `tsconfig` `include`. The CLI cannot combine `--project` with explicit
763
+ * root files (TS5042), so expressing 'project options + entry-only roots' in one `tsc`
764
+ * call requires a generated tsconfig on disk. Writing beside the user's config is
765
+ * invasive; a temp config elsewhere often breaks `node_modules` / `@types` resolution
766
+ * relative to the real project. The API lets us reuse `parseJsonConfigFileContent` (same
767
+ * options as `-p`) with explicit `rootNames`, no files written, and a compiler host whose
768
+ * current directory stays `workPath`.
769
+ *
770
+ * The `typescript` package is resolved with `require` from the user's app (peer dependency), not bundled.
771
+ */
772
+ const require_ = createRequire(import.meta.url);
773
+ const typescript = (args) => {
413
774
  const { span } = args;
414
775
  return span.child("vc.builder.backends.tsCompile").trace(async () => {
415
776
  const extension = extname(args.entrypoint);
@@ -418,79 +779,76 @@ const typescript$1 = (args) => {
418
779
  ".mts",
419
780
  ".cts"
420
781
  ].includes(extension)) return;
421
- const tscPath = resolveTscPath$1(args);
422
- if (!tscPath) {
782
+ const ts = resolveTypeScriptModule(args.workPath);
783
+ if (!ts) {
423
784
  console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck skipped ${Colors.gray("(TypeScript not found)")}`));
424
785
  return null;
425
786
  }
426
- return doTypeCheck$1(args, tscPath);
787
+ return doTypeCheck(args, ts);
427
788
  });
428
789
  };
429
- async function doTypeCheck$1(args, tscPath) {
430
- let stdout = "";
431
- let stderr = "";
432
- /**
433
- * This might be subject to change.
434
- * - if no tscPath, skip typecheck
435
- * - if tsconfig, provide the tsconfig path
436
- * - else provide the entrypoint path
437
- */
438
- const tscArgs = [
439
- tscPath,
440
- "--noEmit",
441
- "--pretty",
442
- "--allowJs",
443
- "--esModuleInterop",
444
- "--skipLibCheck"
445
- ];
446
- const tsconfig = await findNearestTsconfig$1(args.workPath);
447
- if (tsconfig) tscArgs.push("--project", tsconfig);
448
- else tscArgs.push(args.entrypoint);
449
- const child = spawn(process.execPath, tscArgs, {
450
- cwd: args.workPath,
451
- stdio: [
452
- "ignore",
453
- "pipe",
454
- "pipe"
455
- ]
456
- });
457
- child.stdout?.on("data", (data) => {
458
- stdout += data.toString();
459
- });
460
- child.stderr?.on("data", (data) => {
461
- stderr += data.toString();
462
- });
463
- await new Promise((resolve$1, reject) => {
464
- child.on("close", (code) => {
465
- if (code === 0) {
466
- console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck complete`));
467
- resolve$1();
468
- } else {
469
- const output = stdout || stderr;
470
- if (output) {
471
- console.error("\nTypeScript type check failed:\n");
472
- console.error(output);
473
- }
474
- reject(/* @__PURE__ */ new Error("TypeScript type check failed"));
475
- }
476
- });
477
- child.on("error", (err) => {
478
- reject(err);
479
- });
480
- });
790
+ async function doTypeCheck(args, ts) {
791
+ const entryAbsolute = resolve(args.workPath, args.entrypoint);
792
+ const tsconfig = await findNearestTsconfig(args.workPath);
793
+ const formatDiagnostics = process.stdout.isTTY ? ts.formatDiagnosticsWithColorAndContext : ts.formatDiagnostics;
794
+ const diagnosticHost = {
795
+ getNewLine: () => ts.sys.newLine,
796
+ getCanonicalFileName: (fileName) => ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
797
+ getCurrentDirectory: () => args.workPath
798
+ };
799
+ let options;
800
+ let parseDiagnostics = [];
801
+ if (tsconfig) {
802
+ const configRead = ts.readConfigFile(tsconfig, ts.sys.readFile);
803
+ if (configRead.error) {
804
+ const message = formatDiagnostics([configRead.error], diagnosticHost);
805
+ console.error("\nTypeScript type check failed:\n");
806
+ console.error(message);
807
+ throw new Error("TypeScript type check failed");
808
+ }
809
+ const parsed = ts.parseJsonConfigFileContent(configRead.config, ts.sys, dirname$1(tsconfig), void 0, tsconfig);
810
+ parseDiagnostics = parsed.errors;
811
+ options = {
812
+ ...parsed.options,
813
+ noEmit: true,
814
+ skipLibCheck: true,
815
+ allowJs: true,
816
+ esModuleInterop: true
817
+ };
818
+ } else options = {
819
+ noEmit: true,
820
+ skipLibCheck: true,
821
+ allowJs: true,
822
+ esModuleInterop: true,
823
+ target: ts.ScriptTarget.ES2022,
824
+ module: ts.ModuleKind.NodeNext,
825
+ moduleResolution: ts.ModuleResolutionKind.NodeNext
826
+ };
827
+ const compilerHost = ts.createCompilerHost(options);
828
+ compilerHost.getCurrentDirectory = () => args.workPath;
829
+ const program = ts.createProgram([entryAbsolute], options, compilerHost);
830
+ const errors = [...parseDiagnostics, ...ts.getPreEmitDiagnostics(program)].filter((d) => d.category === ts.DiagnosticCategory.Error);
831
+ if (errors.length === 0) {
832
+ console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck complete`));
833
+ return;
834
+ }
835
+ const output = formatDiagnostics(errors, diagnosticHost);
836
+ console.error("\nTypeScript type check failed:\n");
837
+ console.error(output);
838
+ throw new Error("TypeScript type check failed");
481
839
  }
482
- const resolveTscPath$1 = (args) => {
840
+ function resolveTypeScriptModule(workPath) {
483
841
  try {
484
- return require_$1.resolve("typescript/bin/tsc", { paths: [args.workPath] });
485
- } catch (e) {
842
+ return require_(require_.resolve("typescript", { paths: [workPath] }));
843
+ } catch (_e) {
486
844
  return null;
487
845
  }
488
- };
489
- const findNearestTsconfig$1 = async (workPath) => {
490
- const tsconfigPath = join(workPath, "tsconfig.json");
491
- if (existsSync$1(tsconfigPath)) return tsconfigPath;
846
+ }
847
+ const findNearestTsconfig = async (workPath) => {
848
+ const tsconfigPath = join$1(workPath, "tsconfig.json");
849
+ if (existsSync(tsconfigPath)) return tsconfigPath;
492
850
  if (workPath === "/") return;
493
- return findNearestTsconfig$1(join(workPath, ".."));
851
+ return findNearestTsconfig(join$1(workPath, ".."));
494
852
  };
495
853
 
496
854
  //#endregion
@@ -524,12 +882,24 @@ const entrypointExtensions = [
524
882
  const entrypoints = entrypointFilenames.flatMap((filename) => entrypointExtensions.map((extension) => `${filename}.${extension}`));
525
883
  const createFrameworkRegex = (framework) => new RegExp(`(?:from|require|import)\\s*(?:\\(\\s*)?["']${framework}["']\\s*(?:\\))?`, "g");
526
884
  const findEntrypoint = async (cwd) => {
527
- let framework;
885
+ let packageJsonObject = null;
528
886
  try {
529
887
  const packageJson = await readFile(join$1(cwd, "package.json"), "utf-8");
530
- const packageJsonObject = JSON.parse(packageJson);
531
- framework = frameworks.find((framework$1) => packageJsonObject.dependencies?.[framework$1]);
888
+ packageJsonObject = JSON.parse(packageJson);
532
889
  } catch (_) {}
890
+ if (packageJsonObject) {
891
+ const main = typeof packageJsonObject.main === "string" ? packageJsonObject.main.trim() : "";
892
+ if (main) {
893
+ const abs = resolve(cwd, main);
894
+ const rel = relative(cwd, abs);
895
+ if (!rel.startsWith("..") && rel !== "") try {
896
+ await readFile(abs, "utf-8");
897
+ return rel.split(sep).join("/");
898
+ } catch {}
899
+ }
900
+ }
901
+ let framework;
902
+ if (packageJsonObject) framework = frameworks.find((framework$1) => packageJsonObject.dependencies?.[framework$1]);
533
903
  if (!framework) for (const entrypoint of entrypoints) {
534
904
  const entrypointPath = join$1(cwd, entrypoint);
535
905
  try {
@@ -550,7 +920,7 @@ const findEntrypoint = async (cwd) => {
550
920
  };
551
921
  const findEntrypointOrThrow = async (cwd) => {
552
922
  const entrypoint = await findEntrypoint(cwd);
553
- if (!entrypoint) throw new Error(`No entrypoint found in "${cwd}". Expected one of: ${entrypoints.join(", ")}`);
923
+ if (!entrypoint) throw new Error(`No entrypoint found in "${cwd}". Set package.json "main" to a server file, or add one of: ${entrypoints.join(", ")}`);
554
924
  return entrypoint;
555
925
  };
556
926
 
@@ -564,7 +934,7 @@ const getBuildSummary = async (outputDir) => {
564
934
  const build$1 = async (args) => {
565
935
  const entrypoint = args.entrypoint || await findEntrypointOrThrow(args.workPath);
566
936
  const span = args.span ?? new Span({ name: "cervel-build" });
567
- const [, rolldownResult] = await Promise.all([typescript$1({
937
+ const [, rolldownResult] = await Promise.all([typescript({
568
938
  entrypoint,
569
939
  workPath: args.workPath,
570
940
  span
@@ -576,7 +946,7 @@ const build$1 = async (args) => {
576
946
  span
577
947
  })]);
578
948
  await writeFile(join$1(args.workPath, args.out, ".cervel.json"), JSON.stringify({ handler: rolldownResult.result.handler }, null, 2));
579
- console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Build complete`));
949
+ console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Build complete — Using ${Colors.bold(entrypoint)} as the root entrypoint.`));
580
950
  return { rolldownResult: rolldownResult.result };
581
951
  };
582
952
  const serve = async (args) => {
@@ -625,7 +995,7 @@ const srvxOptions = {
625
995
  //#endregion
626
996
  //#region src/rolldown/resolve-format.ts
627
997
  const resolveEntrypointAndFormat = async (args) => {
628
- const extension = extname$1(args.entrypoint);
998
+ const extension = extname(args.entrypoint);
629
999
  const extensionMap = {
630
1000
  ".ts": {
631
1001
  format: "auto",
@@ -673,13 +1043,174 @@ const resolveEntrypointAndFormat = async (args) => {
673
1043
  };
674
1044
  };
675
1045
 
1046
+ //#endregion
1047
+ //#region src/service-vc-init.ts
1048
+ async function applyServiceVcInit(args) {
1049
+ const { format, extension } = await resolveShimFormat(args);
1050
+ const handlerDir = dirname$1(args.handler);
1051
+ const vcInitName = `${basename(args.handler, extname(args.handler))}.__vc_service_vc_init${extension}`;
1052
+ const vcInitHandler = handlerDir === "." ? vcInitName : join$1(handlerDir, vcInitName);
1053
+ const handlerImportPath = `./${basename(args.handler)}`;
1054
+ const vcInitSource = format === "esm" ? createEsmServiceVcInit(handlerImportPath) : createCjsServiceVcInit(handlerImportPath);
1055
+ return {
1056
+ handler: vcInitHandler,
1057
+ files: {
1058
+ ...args.files,
1059
+ [vcInitHandler]: new FileBlob({
1060
+ data: vcInitSource,
1061
+ mode: 420
1062
+ })
1063
+ }
1064
+ };
1065
+ }
1066
+ async function resolveShimFormat(args) {
1067
+ const { format } = await resolveEntrypointAndFormat({
1068
+ entrypoint: args.handler,
1069
+ workPath: args.workPath
1070
+ });
1071
+ return {
1072
+ format,
1073
+ extension: extname(args.handler) || (format === "esm" ? ".mjs" : ".cjs")
1074
+ };
1075
+ }
1076
+ const sharedShimPrelude = String.raw`
1077
+ const PATCH_SYMBOL = Symbol.for('vc.service.route-prefix-strip.patch')
1078
+
1079
+ function normalizeServiceRoutePrefix(rawPrefix) {
1080
+ if (!rawPrefix) {
1081
+ return ''
1082
+ }
1083
+
1084
+ let prefix = String(rawPrefix).trim()
1085
+ if (!prefix) {
1086
+ return ''
1087
+ }
1088
+
1089
+ if (!prefix.startsWith('/')) {
1090
+ prefix = '/' + prefix
1091
+ }
1092
+
1093
+ if (prefix !== '/') {
1094
+ prefix = prefix.replace(/\/+$/, '')
1095
+ }
1096
+
1097
+ return prefix === '/' ? '' : prefix
1098
+ }
1099
+
1100
+ function getServiceRoutePrefix() {
1101
+ const enabled = String(
1102
+ process.env.VERCEL_SERVICE_ROUTE_PREFIX_STRIP || ''
1103
+ ).toLowerCase()
1104
+ if (enabled !== '1' && enabled !== 'true') {
1105
+ return ''
1106
+ }
1107
+
1108
+ return normalizeServiceRoutePrefix(process.env.VERCEL_SERVICE_ROUTE_PREFIX || '')
1109
+ }
1110
+
1111
+ function stripServiceRoutePrefix(requestUrl, prefix) {
1112
+ if (typeof requestUrl !== 'string' || requestUrl === '*') {
1113
+ return requestUrl
1114
+ }
1115
+
1116
+ const queryIndex = requestUrl.indexOf('?')
1117
+ const rawPath =
1118
+ queryIndex === -1 ? requestUrl : requestUrl.slice(0, queryIndex)
1119
+ const query = queryIndex === -1 ? '' : requestUrl.slice(queryIndex)
1120
+
1121
+ let path = rawPath || '/'
1122
+ if (!path.startsWith('/')) {
1123
+ path = '/' + path
1124
+ }
1125
+
1126
+ if (!prefix) {
1127
+ return path + query
1128
+ }
1129
+
1130
+ if (path === prefix) {
1131
+ return '/' + query
1132
+ }
1133
+
1134
+ if (path.startsWith(prefix + '/')) {
1135
+ return path.slice(prefix.length) + query
1136
+ }
1137
+
1138
+ return path + query
1139
+ }
1140
+
1141
+ function patchServerRequestUrl(ServerCtor) {
1142
+ const prefix = getServiceRoutePrefix()
1143
+ if (!prefix || globalThis[PATCH_SYMBOL]) {
1144
+ return
1145
+ }
1146
+
1147
+ globalThis[PATCH_SYMBOL] = true
1148
+
1149
+ const originalEmit = ServerCtor.prototype.emit
1150
+ ServerCtor.prototype.emit = function patchedEmit(event, request, ...args) {
1151
+ if (event === 'request' && request && typeof request.url === 'string') {
1152
+ request.url = stripServiceRoutePrefix(request.url, prefix)
1153
+ }
1154
+
1155
+ return originalEmit.call(this, event, request, ...args)
1156
+ }
1157
+ }
1158
+ `;
1159
+ function createEsmServiceVcInit(handlerImportPath) {
1160
+ return `
1161
+ import { Server } from 'node:http'
1162
+
1163
+ ${sharedShimPrelude}
1164
+
1165
+ // Patch the HTTP server before loading user code so apps that attach request
1166
+ // listeners during module evaluation see the stripped service-relative URL.
1167
+ patchServerRequestUrl(Server)
1168
+
1169
+ const originalModule = await import(${JSON.stringify(handlerImportPath)})
1170
+
1171
+ /**
1172
+ * Match the Node serverless loader behavior: TS/CJS/ESM interop can leave us
1173
+ * with nested \`.default\` wrappers, so peel off a few layers to recover the
1174
+ * actual user entrypoint shape.
1175
+ */
1176
+ function unwrapDefaultExport(value) {
1177
+ let current = value
1178
+ for (let i = 0; i < 5; i++) {
1179
+ if (current && typeof current === 'object' && 'default' in current && current.default) {
1180
+ current = current.default
1181
+ } else {
1182
+ break
1183
+ }
1184
+ }
1185
+ return current
1186
+ }
1187
+
1188
+ const entrypoint = unwrapDefaultExport(originalModule)
1189
+
1190
+ // Re-export the resolved entrypoint so the surrounding runtime still sees the
1191
+ // same handler shape after this service bootstrap runs.
1192
+ export default typeof entrypoint === 'undefined' ? originalModule : entrypoint
1193
+ `.trimStart();
1194
+ }
1195
+ function createCjsServiceVcInit(handlerImportPath) {
1196
+ return `
1197
+ const { Server } = require('node:http')
1198
+
1199
+ ${sharedShimPrelude}
1200
+
1201
+ patchServerRequestUrl(Server)
1202
+
1203
+ module.exports = require(${JSON.stringify(handlerImportPath)})
1204
+ `.trimStart();
1205
+ }
1206
+
676
1207
  //#endregion
677
1208
  //#region src/rolldown/nft.ts
678
1209
  const nft = async (args) => {
679
1210
  const nftSpan = args.span.child("vc.builder.backends.nft");
680
1211
  const runNft = async () => {
681
1212
  const ignorePatterns = [...args.ignoreNodeModules ? ["**/node_modules/**"] : [], ...args.ignore ? Array.isArray(args.ignore) ? args.ignore : [args.ignore] : []];
682
- const nftResult = await nodeFileTrace$1(Array.from(args.localBuildFiles), {
1213
+ const nftResult = await nodeFileTrace$1(Array.from(args.localBuildFiles).filter((p) => existsSync(p)), {
683
1214
  base: args.repoRootPath,
684
1215
  processCwd: args.workPath,
685
1216
  ts: true,
@@ -699,7 +1230,13 @@ const nft = async (args) => {
699
1230
  });
700
1231
  for (const file of nftResult.fileList) {
701
1232
  const absolutePath = join$1(args.repoRootPath, file);
702
- const stats = await lstat(absolutePath);
1233
+ let stats;
1234
+ try {
1235
+ stats = await lstat(absolutePath);
1236
+ } catch (error) {
1237
+ if (isNativeError(error) && "code" in error && error.code === "ENOENT") continue;
1238
+ throw error;
1239
+ }
703
1240
  const outputPath = file;
704
1241
  if (args.localBuildFiles.has(join$1(args.repoRootPath, outputPath))) continue;
705
1242
  if (stats.isSymbolicLink() || stats.isFile()) if (args.ignoreNodeModules) {
@@ -719,6 +1256,7 @@ const nft = async (args) => {
719
1256
  await nftSpan.trace(runNft);
720
1257
  };
721
1258
  const isTypeScriptFile = (fsPath) => {
1259
+ if (fsPath.endsWith(".d.ts") || fsPath.endsWith(".d.mts") || fsPath.endsWith(".d.cts")) return false;
722
1260
  return fsPath.endsWith(".ts") || fsPath.endsWith(".tsx") || fsPath.endsWith(".mts") || fsPath.endsWith(".cts");
723
1261
  };
724
1262
 
@@ -750,7 +1288,7 @@ const rolldown = async (args) => {
750
1288
  }
751
1289
  };
752
1290
  const isCommonJS = async (bareImport, resolvedPath, resolvedInfo) => {
753
- const ext = extname$1(resolvedPath);
1291
+ const ext = extname(resolvedPath);
754
1292
  if (ext === ".cjs") return true;
755
1293
  if (ext === ".mjs") return false;
756
1294
  if (ext === ".js" || ext === ".ts") {
@@ -812,8 +1350,12 @@ const rolldown = async (args) => {
812
1350
  id,
813
1351
  external: true
814
1352
  };
815
- if (resolved?.id && isLocalImport(resolved.id)) localBuildFiles.add(resolved.id);
816
- else if (!resolved) localBuildFiles.add(join$1(args.workPath, id));
1353
+ if (resolved?.id && isLocalImport(resolved.id)) {
1354
+ if (existsSync(resolved.id)) localBuildFiles.add(resolved.id);
1355
+ } else if (!resolved && !(isBareImport(id) && importer)) {
1356
+ const candidate = join$1(args.workPath, id);
1357
+ if (existsSync(candidate)) localBuildFiles.add(candidate);
1358
+ }
817
1359
  if (importer?.startsWith(CJS_SHIM_PREFIX) && isBareImport(id)) return {
818
1360
  id,
819
1361
  external: true
@@ -915,7 +1457,7 @@ module.exports = requireFromContext('${pkgName}');
915
1457
  ignore: args.config.excludeFiles
916
1458
  });
917
1459
  if (!handler) throw new Error(`Unable to resolve build handler for entrypoint: ${args.entrypoint}`);
918
- console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Build complete`));
1460
+ console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Build complete — Using ${Colors.bold(args.entrypoint)} as the root entrypoint.`));
919
1461
  return {
920
1462
  files,
921
1463
  handler,
@@ -962,6 +1504,7 @@ const introspection = async (args) => {
962
1504
  writeFileSync(filePath, value.data);
963
1505
  }
964
1506
  let introspectionData;
1507
+ let introspectionError;
965
1508
  await new Promise((resolvePromise) => {
966
1509
  try {
967
1510
  debug("Spawning introspection process");
@@ -969,7 +1512,7 @@ const introspection = async (args) => {
969
1512
  const tempFilePath = join$1(outputTempDir, "output.txt");
970
1513
  const writeStream = createWriteStream(tempFilePath);
971
1514
  let streamClosed = false;
972
- const child = spawn$1("node", [
1515
+ const child = spawn("node", [
973
1516
  "-r",
974
1517
  rolldownCjsLoaderPath,
975
1518
  "--import",
@@ -1004,11 +1547,11 @@ const introspection = async (args) => {
1004
1547
  const timeout = setTimeout(() => {
1005
1548
  debug("Introspection timeout, killing process with SIGTERM");
1006
1549
  child.kill("SIGTERM");
1007
- }, 8e3);
1550
+ }, 5e3);
1008
1551
  const timeout2 = setTimeout(() => {
1009
1552
  debug("Introspection timeout, killing process with SIGKILL");
1010
1553
  child.kill("SIGKILL");
1011
- }, 9e3);
1554
+ }, 6e3);
1012
1555
  const cleanup = () => {
1013
1556
  clearTimeout(timeout);
1014
1557
  clearTimeout(timeout2);
@@ -1023,7 +1566,8 @@ const introspection = async (args) => {
1023
1566
  };
1024
1567
  child.on("error", (err) => {
1025
1568
  cleanup();
1026
- debug(`Loader error: ${err.message}`);
1569
+ introspectionError = `Loader error: ${err.message}`;
1570
+ debug(introspectionError);
1027
1571
  if (!streamClosed) writeStream.end(() => {
1028
1572
  streamClosed = true;
1029
1573
  try {
@@ -1054,8 +1598,12 @@ const introspection = async (args) => {
1054
1598
  introspectionData = introspectionSchema.parse(JSON.parse(introspectionString));
1055
1599
  debug("Introspection data parsed successfully");
1056
1600
  }
1057
- } else debug(`Introspection markers not found.\nstdout:\n${stdoutBuffer}\nstderr:\n${stderrBuffer}`);
1601
+ } else {
1602
+ introspectionError = `Introspection markers not found. stderr: ${stderrBuffer}`;
1603
+ debug(`Introspection markers not found.\nstdout:\n${stdoutBuffer}\nstderr:\n${stderrBuffer}`);
1604
+ }
1058
1605
  } catch (error) {
1606
+ introspectionError = `Error parsing introspection data: ${error}. stderr: ${stderrBuffer}`;
1059
1607
  debug(`Error parsing introspection data: ${error}\nstdout:\n${stdoutBuffer}\nstderr:\n${stderrBuffer}`);
1060
1608
  } finally {
1061
1609
  try {
@@ -1072,6 +1620,7 @@ const introspection = async (args) => {
1072
1620
  else resolvePromise();
1073
1621
  });
1074
1622
  } catch (error) {
1623
+ introspectionError = `Introspection error: ${error}`;
1075
1624
  debug("Introspection error", error);
1076
1625
  resolvePromise();
1077
1626
  }
@@ -1079,7 +1628,8 @@ const introspection = async (args) => {
1079
1628
  if (!introspectionData) {
1080
1629
  introspectionSpan.setAttributes({
1081
1630
  "introspection.success": "false",
1082
- "introspection.routes": "0"
1631
+ "introspection.routes": "0",
1632
+ ...introspectionError && { "introspection.error": introspectionError.slice(0, 1024) }
1083
1633
  });
1084
1634
  return defaultResult$1;
1085
1635
  }
@@ -1102,6 +1652,20 @@ const introspection = async (args) => {
1102
1652
 
1103
1653
  //#endregion
1104
1654
  //#region src/build.ts
1655
+ /**
1656
+ * `outputDirectory` is usually the same project setting the static builder uses
1657
+ * (Vite/Webpack/etc. client output). We only reuse it for the Node lambda when we
1658
+ * find a known server entry file under that folder; otherwise we bundle from
1659
+ * source with rolldown. Errors here are swallowed so static-only output trees do
1660
+ * not fail the build.
1661
+ */
1662
+ async function findEntrypointInOutputDir(dir) {
1663
+ try {
1664
+ return await findEntrypoint(dir);
1665
+ } catch {
1666
+ return;
1667
+ }
1668
+ }
1105
1669
  const maybeDoBuildCommand = async (args, downloadResult) => {
1106
1670
  const buildCommandResult = await maybeExecBuildCommand(args, downloadResult);
1107
1671
  const outputSetting = args.config.outputDirectory;
@@ -1109,7 +1673,7 @@ const maybeDoBuildCommand = async (args, downloadResult) => {
1109
1673
  let entrypoint;
1110
1674
  if (buildCommandResult && outputSetting) if (outputSetting) {
1111
1675
  const _outputDir = join$1(args.workPath, outputSetting);
1112
- const _entrypoint = await findEntrypoint(_outputDir);
1676
+ const _entrypoint = await findEntrypointInOutputDir(_outputDir);
1113
1677
  if (_entrypoint) {
1114
1678
  outputDir = _outputDir;
1115
1679
  entrypoint = _entrypoint;
@@ -1121,7 +1685,7 @@ const maybeDoBuildCommand = async (args, downloadResult) => {
1121
1685
  ]) {
1122
1686
  const _outputDir = join$1(args.workPath, outputDirectory);
1123
1687
  if (existsSync(_outputDir)) {
1124
- const _entrypoint = await findEntrypoint(_outputDir);
1688
+ const _entrypoint = await findEntrypointInOutputDir(_outputDir);
1125
1689
  if (_entrypoint) {
1126
1690
  outputDir = _outputDir;
1127
1691
  entrypoint = _entrypoint;
@@ -1143,93 +1707,6 @@ const maybeDoBuildCommand = async (args, downloadResult) => {
1143
1707
  };
1144
1708
  };
1145
1709
 
1146
- //#endregion
1147
- //#region src/typescript.ts
1148
- const require_ = createRequire(import.meta.url);
1149
- const typescript = (args) => {
1150
- const { span } = args;
1151
- return span.child("vc.builder.backends.tsCompile").trace(async () => {
1152
- const extension = extname$1(args.entrypoint);
1153
- if (![
1154
- ".ts",
1155
- ".mts",
1156
- ".cts"
1157
- ].includes(extension)) return;
1158
- const tscPath = resolveTscPath(args);
1159
- if (!tscPath) {
1160
- console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck skipped ${Colors.gray("(TypeScript not found)")}`));
1161
- return null;
1162
- }
1163
- return doTypeCheck(args, tscPath);
1164
- });
1165
- };
1166
- async function doTypeCheck(args, tscPath) {
1167
- let stdout = "";
1168
- let stderr = "";
1169
- /**
1170
- * This might be subject to change.
1171
- * - if no tscPath, skip typecheck
1172
- * - if tsconfig, provide the tsconfig path
1173
- * - else provide the entrypoint path
1174
- */
1175
- const tscArgs = [
1176
- tscPath,
1177
- "--noEmit",
1178
- "--pretty",
1179
- "--allowJs",
1180
- "--esModuleInterop",
1181
- "--skipLibCheck"
1182
- ];
1183
- const tsconfig = await findNearestTsconfig(args.workPath);
1184
- if (tsconfig) tscArgs.push("--project", tsconfig);
1185
- else tscArgs.push(args.entrypoint);
1186
- const child = spawn$1(process.execPath, tscArgs, {
1187
- cwd: args.workPath,
1188
- stdio: [
1189
- "ignore",
1190
- "pipe",
1191
- "pipe"
1192
- ]
1193
- });
1194
- child.stdout?.on("data", (data) => {
1195
- stdout += data.toString();
1196
- });
1197
- child.stderr?.on("data", (data) => {
1198
- stderr += data.toString();
1199
- });
1200
- await new Promise((resolve$1, reject) => {
1201
- child.on("close", (code) => {
1202
- if (code === 0) {
1203
- console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck complete`));
1204
- resolve$1();
1205
- } else {
1206
- const output = stdout || stderr;
1207
- if (output) {
1208
- console.error("\nTypeScript type check failed:\n");
1209
- console.error(output);
1210
- }
1211
- reject(/* @__PURE__ */ new Error("TypeScript type check failed"));
1212
- }
1213
- });
1214
- child.on("error", (err) => {
1215
- reject(err);
1216
- });
1217
- });
1218
- }
1219
- const resolveTscPath = (args) => {
1220
- try {
1221
- return require_.resolve("typescript/bin/tsc", { paths: [args.workPath] });
1222
- } catch (e) {
1223
- return null;
1224
- }
1225
- };
1226
- const findNearestTsconfig = async (workPath) => {
1227
- const tsconfigPath = join$1(workPath, "tsconfig.json");
1228
- if (existsSync(tsconfigPath)) return tsconfigPath;
1229
- if (workPath === "/") return;
1230
- return findNearestTsconfig(join$1(workPath, ".."));
1231
- };
1232
-
1233
1710
  //#endregion
1234
1711
  //#region src/introspection/index.ts
1235
1712
  const require = createRequire(import.meta.url);
@@ -1259,7 +1736,7 @@ const introspectApp = async (args) => {
1259
1736
  await new Promise((resolvePromise) => {
1260
1737
  try {
1261
1738
  debug("Spawning introspection process");
1262
- const child = spawn$1("node", [
1739
+ const child = spawn("node", [
1263
1740
  "-r",
1264
1741
  cjsLoaderPath,
1265
1742
  "--import",
@@ -1419,6 +1896,11 @@ const getFramework = (args) => {
1419
1896
  //#endregion
1420
1897
  //#region src/index.ts
1421
1898
  const version = 2;
1899
+ /** Non-empty Build Command from project settings / vercel.json (not the default `build` script). */
1900
+ function hasExplicitBuildCommand(config) {
1901
+ const cmd = config.buildCommand ?? config.projectSettings?.buildCommand;
1902
+ return typeof cmd === "string" && cmd.trim().length > 0;
1903
+ }
1422
1904
  const build = async (args) => {
1423
1905
  const downloadResult = await downloadInstallAndBundle(args);
1424
1906
  const nodeVersion = await getNodeVersion(args.workPath, void 0, args.config, args.meta);
@@ -1441,13 +1923,21 @@ const build = async (args) => {
1441
1923
  ...args,
1442
1924
  span: buildSpan
1443
1925
  });
1444
- const introspectionPromise = introspection({
1926
+ const introspectionPromise = rolldownResult.framework.slug === "hono" ? introspection({
1445
1927
  ...args,
1446
1928
  span: buildSpan,
1447
1929
  files: rolldownResult.files,
1448
1930
  handler: rolldownResult.handler
1931
+ }) : Promise.resolve({
1932
+ routes: [],
1933
+ additionalFolders: [],
1934
+ additionalDeps: []
1449
1935
  });
1450
- const typescriptPromise = typescript({
1936
+ let typescriptPromise;
1937
+ if (hasExplicitBuildCommand(args.config)) {
1938
+ console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck skipped ${Colors.gray("(Build Command is configured)")}`));
1939
+ typescriptPromise = Promise.resolve();
1940
+ } else typescriptPromise = typescript({
1451
1941
  entrypoint,
1452
1942
  workPath: args.workPath,
1453
1943
  span: buildSpan
@@ -1466,33 +1956,79 @@ const build = async (args) => {
1466
1956
  conditions: isBun ? ["bun"] : void 0,
1467
1957
  span: buildSpan
1468
1958
  });
1959
+ try {
1960
+ await generateProjectManifest({
1961
+ workPath: args.workPath,
1962
+ nodeVersion,
1963
+ cliType: downloadResult.cliType,
1964
+ lockfilePath: downloadResult.lockfilePath,
1965
+ lockfileVersion: downloadResult.lockfileVersion
1966
+ });
1967
+ } catch (err) {
1968
+ debug(`Failed to write node manifest: ${err instanceof Error ? err.message : String(err)}`);
1969
+ }
1469
1970
  const baseDir = args.repoRootPath || args.workPath;
1470
1971
  const includeResults = await Promise.all(normalizeArray(args.config.includeFiles).map((pattern) => glob(pattern, baseDir)));
1471
1972
  for (const matched of includeResults) for (const [relPath, entry] of Object.entries(matched)) files[relPath] = entry;
1472
1973
  const introspectionResult = await introspectionPromise;
1473
1974
  await typescriptPromise;
1975
+ const functionConfigOverrides = await getLambdaOptionsFromFunction({
1976
+ sourceFile: entrypoint,
1977
+ config: args.config
1978
+ });
1979
+ const serviceRoutePrefix = normalizeServiceRoutePrefix(args.config?.routePrefix ?? args.service?.routePrefix);
1980
+ const shouldStripServiceRoutePrefix = !!serviceRoutePrefix && (typeof args.config?.serviceName === "string" || !!args.service);
1981
+ let lambdaFiles = files;
1982
+ let lambdaHandler = handler;
1983
+ if (shouldStripServiceRoutePrefix) {
1984
+ const shimmedLambda = await applyServiceVcInit({
1985
+ files,
1986
+ handler,
1987
+ workPath: nftWorkPath
1988
+ });
1989
+ lambdaFiles = shimmedLambda.files;
1990
+ lambdaHandler = shimmedLambda.handler;
1991
+ }
1474
1992
  const lambda = new NodejsLambda({
1475
1993
  runtime: nodeVersion.runtime,
1476
- handler,
1477
- files,
1994
+ handler: lambdaHandler,
1995
+ files: lambdaFiles,
1478
1996
  framework: rolldownResult.framework,
1479
1997
  shouldAddHelpers: false,
1480
1998
  shouldAddSourcemapSupport: true,
1481
1999
  awsLambdaHandler: "",
2000
+ ...functionConfigOverrides,
1482
2001
  shouldDisableAutomaticFetchInstrumentation: process.env.VERCEL_TRACING_DISABLE_AUTOMATIC_FETCH_INSTRUMENTATION === "1"
1483
2002
  });
2003
+ if (shouldStripServiceRoutePrefix && serviceRoutePrefix) lambda.environment = {
2004
+ ...lambda.environment,
2005
+ VERCEL_SERVICE_ROUTE_PREFIX: serviceRoutePrefix,
2006
+ VERCEL_SERVICE_ROUTE_PREFIX_STRIP: "1"
2007
+ };
2008
+ const serviceName = typeof args.config?.serviceName === "string" && args.config.serviceName !== "" ? args.config.serviceName : void 0;
2009
+ const internalServiceFunctionPath = typeof serviceName === "string" && serviceName !== "" ? `/_svc/${serviceName}/index` : void 0;
2010
+ const internalServiceOutputPath = internalServiceFunctionPath?.slice(1);
2011
+ const remapRouteDestination = (route) => {
2012
+ const prefixedRoute = maybePrefixServiceRouteSource(route, serviceRoutePrefix);
2013
+ if (!internalServiceFunctionPath || !route.dest) return prefixedRoute;
2014
+ return {
2015
+ ...prefixedRoute,
2016
+ dest: internalServiceFunctionPath
2017
+ };
2018
+ };
1484
2019
  const routes = [
1485
2020
  { handle: "filesystem" },
1486
- ...introspectionResult.routes,
2021
+ ...introspectionResult.routes.map(remapRouteDestination),
1487
2022
  {
1488
- src: "/(.*)",
1489
- dest: "/"
2023
+ src: getServiceCatchallSource(serviceRoutePrefix),
2024
+ dest: internalServiceFunctionPath ?? "/"
1490
2025
  }
1491
2026
  ];
1492
- const output = { index: lambda };
2027
+ const output = internalServiceOutputPath ? { [internalServiceOutputPath]: lambda } : { index: lambda };
1493
2028
  for (const route of routes) if (route.dest) {
1494
2029
  if (route.dest === "/") continue;
1495
- output[route.dest] = lambda;
2030
+ const outputPath = route.dest === internalServiceFunctionPath && internalServiceOutputPath ? internalServiceOutputPath : route.dest;
2031
+ output[outputPath] = lambda;
1496
2032
  }
1497
2033
  return {
1498
2034
  routes,
@@ -1504,6 +2040,33 @@ const prepareCache = ({ repoRootPath, workPath }) => {
1504
2040
  return glob(defaultCachePathGlob, repoRootPath || workPath);
1505
2041
  };
1506
2042
  const normalizeArray = (value) => Array.isArray(value) ? value : value ? [value] : [];
2043
+ const normalizeServiceRoutePrefix = (routePrefix) => {
2044
+ if (typeof routePrefix !== "string" || routePrefix === "" || routePrefix === ".") return;
2045
+ let normalized = routePrefix.startsWith("/") ? routePrefix : `/${routePrefix}`;
2046
+ if (normalized !== "/" && normalized.endsWith("/")) normalized = normalized.slice(0, -1);
2047
+ return normalized === "/" ? void 0 : normalized;
2048
+ };
2049
+ const maybePrefixServiceRouteSource = (route, routePrefix) => {
2050
+ if (!routePrefix || typeof route.dest !== "string" || !route.dest.startsWith("/")) return route;
2051
+ return {
2052
+ ...route,
2053
+ src: getPrefixedRouteSource(route.src, route.dest, routePrefix)
2054
+ };
2055
+ };
2056
+ const getPrefixedRouteSource = (routeSource, routePath, routePrefix) => {
2057
+ if (!routeSource) return routeSource;
2058
+ if (routePath === routePrefix || routePath.startsWith(`${routePrefix}/`)) return routeSource;
2059
+ const escapedRoutePrefix = toRegexSource(routePrefix);
2060
+ if (routeSource.startsWith("^(?:")) return `^(?:${escapedRoutePrefix}${routeSource.slice(4)}`;
2061
+ if (routeSource.startsWith("^")) return `^${escapedRoutePrefix}${routeSource.slice(1)}`;
2062
+ return `${escapedRoutePrefix}${routeSource}`;
2063
+ };
2064
+ const getServiceCatchallSource = (routePrefix) => {
2065
+ if (!routePrefix) return "/(.*)";
2066
+ return `^${escapeForRegex(routePrefix)}(?:/(.*))?$`;
2067
+ };
2068
+ const escapeForRegex = (value) => value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
2069
+ const toRegexSource = (value) => escapeForRegex(value).replaceAll("/", "\\/");
1507
2070
 
1508
2071
  //#endregion
1509
- export { build, build$1 as cervelBuild, serve as cervelServe, findEntrypoint, findEntrypointOrThrow, getBuildSummary, introspectApp, nodeFileTrace, prepareCache, srvxOptions, version };
2072
+ export { build, build$1 as cervelBuild, serve as cervelServe, diagnostics, findEntrypoint, findEntrypointOrThrow, getBuildSummary, introspectApp, nodeFileTrace, prepareCache, srvxOptions, version };
@@ -94,7 +94,7 @@ const nft = async (args) => {
94
94
  const nftSpan = args.span.child("vc.builder.backends.nft");
95
95
  const runNft = async () => {
96
96
  const ignorePatterns = [...args.ignoreNodeModules ? ["**/node_modules/**"] : [], ...args.ignore ? Array.isArray(args.ignore) ? args.ignore : [args.ignore] : []];
97
- const nftResult = await nodeFileTrace(Array.from(args.localBuildFiles), {
97
+ const nftResult = await nodeFileTrace(Array.from(args.localBuildFiles).filter((p) => existsSync(p)), {
98
98
  base: args.repoRootPath,
99
99
  processCwd: args.workPath,
100
100
  ts: true,
@@ -114,7 +114,13 @@ const nft = async (args) => {
114
114
  });
115
115
  for (const file of nftResult.fileList) {
116
116
  const absolutePath = join(args.repoRootPath, file);
117
- const stats = await lstat(absolutePath);
117
+ let stats;
118
+ try {
119
+ stats = await lstat(absolutePath);
120
+ } catch (error) {
121
+ if (isNativeError(error) && "code" in error && error.code === "ENOENT") continue;
122
+ throw error;
123
+ }
118
124
  const outputPath = file;
119
125
  if (args.localBuildFiles.has(join(args.repoRootPath, outputPath))) continue;
120
126
  if (stats.isSymbolicLink() || stats.isFile()) if (args.ignoreNodeModules) {
@@ -134,6 +140,7 @@ const nft = async (args) => {
134
140
  await nftSpan.trace(runNft);
135
141
  };
136
142
  const isTypeScriptFile = (fsPath) => {
143
+ if (fsPath.endsWith(".d.ts") || fsPath.endsWith(".d.mts") || fsPath.endsWith(".d.cts")) return false;
137
144
  return fsPath.endsWith(".ts") || fsPath.endsWith(".tsx") || fsPath.endsWith(".mts") || fsPath.endsWith(".cts");
138
145
  };
139
146
 
@@ -227,8 +234,12 @@ const rolldown = async (args) => {
227
234
  id,
228
235
  external: true
229
236
  };
230
- if (resolved?.id && isLocalImport(resolved.id)) localBuildFiles.add(resolved.id);
231
- else if (!resolved) localBuildFiles.add(join(args.workPath, id));
237
+ if (resolved?.id && isLocalImport(resolved.id)) {
238
+ if (existsSync(resolved.id)) localBuildFiles.add(resolved.id);
239
+ } else if (!resolved && !(isBareImport(id) && importer)) {
240
+ const candidate = join(args.workPath, id);
241
+ if (existsSync(candidate)) localBuildFiles.add(candidate);
242
+ }
232
243
  if (importer?.startsWith(CJS_SHIM_PREFIX) && isBareImport(id)) return {
233
244
  id,
234
245
  external: true
@@ -330,7 +341,7 @@ module.exports = requireFromContext('${pkgName}');
330
341
  ignore: args.config.excludeFiles
331
342
  });
332
343
  if (!handler) throw new Error(`Unable to resolve build handler for entrypoint: ${args.entrypoint}`);
333
- console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Build complete`));
344
+ console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Build complete — Using ${Colors.bold(args.entrypoint)} as the root entrypoint.`));
334
345
  return {
335
346
  files,
336
347
  handler,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/backends",
3
- "version": "0.1.0-canary.20260211174907.cdd2da6",
3
+ "version": "0.1.1",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./dist/index.mjs",
6
6
  "homepage": "https://vercel.com/docs",
@@ -24,7 +24,9 @@
24
24
  "dist"
25
25
  ],
26
26
  "dependencies": {
27
- "@vercel/nft": "1.3.0",
27
+ "@yarnpkg/parsers": "^3.0.0",
28
+ "js-yaml": "^3.13.1",
29
+ "@vercel/nft": "1.5.0",
28
30
  "execa": "3.2.0",
29
31
  "fs-extra": "11.1.0",
30
32
  "oxc-transform": "0.111.0",
@@ -34,13 +36,14 @@
34
36
  "srvx": "0.8.9",
35
37
  "tsx": "4.21.0",
36
38
  "zod": "3.22.4",
37
- "@vercel/build-utils": "13.4.0-canary.20260211174907.cdd2da6"
39
+ "@vercel/build-utils": "13.19.0"
38
40
  },
39
41
  "peerDependencies": {
40
42
  "typescript": "^4.0.0 || ^5.0.0"
41
43
  },
42
44
  "devDependencies": {
43
45
  "@types/express": "5.0.3",
46
+ "@types/js-yaml": "3.12.1",
44
47
  "@types/fs-extra": "11",
45
48
  "@types/jest": "27.5.1",
46
49
  "@types/node": "20.11.0",