@vercel/backends 0.3.0 → 0.4.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.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import * as _vercel_build_utils0 from "@vercel/build-utils";
3
2
  import { BuildOptions, BuildV2, Files, PrepareCache, Span } from "@vercel/build-utils";
4
3
  import { ParseArgsConfig } from "node:util";
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import { builtinModules, createRequire } from "node:module";
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";
2
+ import { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
4
+ import path, { delimiter, dirname as dirname$1, join as join$1 } from "path";
5
+ import { FileBlob, FileFsRef, MANIFEST_VERSION, NodejsLambda, Span, createDiagnostics, debug, defaultCachePathGlob, download, execCommand, getEnvForPackageManager, getInternalServiceCronPath, getLambdaOptionsFromFunction, getNodeBinPaths, getNodeVersion, getReportedServiceType, glob, isBackendFramework, isBunVersion, isExperimentalBackendsWithoutIntrospectionEnabled, isScheduleTriggeredService, runNpmInstall, runPackageJsonScript, scanParentDirs, writeProjectManifest } from "@vercel/build-utils";
4
6
  import fs from "fs";
5
7
  import yaml from "js-yaml";
6
8
  import { parseSyml } from "@yarnpkg/parsers";
7
- import { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
8
9
  import { lstat, readFile, rm, stat } from "node:fs/promises";
9
- import { basename, dirname as dirname$1, extname, isAbsolute, join as join$1, relative, resolve, sep } from "node:path";
10
10
  import { build as build$2 } from "rolldown";
11
11
  import { exports } from "resolve.exports";
12
12
  import { isNativeError } from "node:util/types";
@@ -14,6 +14,7 @@ import { nodeFileTrace as nodeFileTrace$1, resolve as resolve$1 } from "@vercel/
14
14
  import { transform } from "oxc-transform";
15
15
  import execa from "execa";
16
16
  import { readFile as readFile$1, writeFile } from "fs/promises";
17
+ import { fileURLToPath } from "node:url";
17
18
  import { spawn } from "node:child_process";
18
19
  import { tmpdir } from "node:os";
19
20
  import { z } from "zod";
@@ -22,7 +23,7 @@ import { z } from "zod";
22
23
  async function downloadInstallAndBundle(args) {
23
24
  const { entrypoint, files, workPath, meta, config, repoRootPath } = args;
24
25
  await download(files, workPath, meta);
25
- const entrypointFsDirname = join(workPath, dirname(entrypoint));
26
+ const entrypointFsDirname = join$1(workPath, dirname$1(entrypoint));
26
27
  const { cliType, lockfilePath, lockfileVersion, packageJsonPackageManager, turboSupportsCorepackHome } = await scanParentDirs(entrypointFsDirname, true, repoRootPath);
27
28
  const spawnEnv = getEnvForPackageManager({
28
29
  cliType,
@@ -352,7 +353,7 @@ function buildDirectMaps(pkgJson) {
352
353
  directRequested
353
354
  };
354
355
  }
355
- async function generateProjectManifest({ workPath, nodeVersion, cliType, lockfilePath, lockfileVersion, framework }) {
356
+ async function generateProjectManifest({ workPath, nodeVersion, cliType, lockfilePath, lockfileVersion, framework, serviceType }) {
356
357
  try {
357
358
  const pkgJson = await readPackageJson(workPath);
358
359
  if (!pkgJson) return;
@@ -402,6 +403,7 @@ async function generateProjectManifest({ workPath, nodeVersion, cliType, lockfil
402
403
  version: MANIFEST_VERSION,
403
404
  runtime: "node",
404
405
  ...framework ? { framework } : {},
406
+ ...serviceType ? { serviceType } : {},
405
407
  runtimeVersion,
406
408
  dependencies: [...directDeps.sort((a, b) => a.name.localeCompare(b.name)), ...transitiveDeps.sort((a, b) => a.name.localeCompare(b.name))]
407
409
  }, workPath, "node");
@@ -448,7 +450,7 @@ const plugin = (args) => {
448
450
  if (!pkgJsonPath) return true;
449
451
  const pkgJson = await getPackageJson(pkgJsonPath);
450
452
  if (!pkgJson) return true;
451
- const pkgDir = dirname$1(pkgJsonPath);
453
+ const pkgDir = dirname(pkgJsonPath);
452
454
  const relativePath = resolvedPath.startsWith(pkgDir) ? resolvedPath.slice(pkgDir.length + 1).replace(/\\/g, "/") : null;
453
455
  if (!relativePath) return pkgJson.type !== "module";
454
456
  const pkgName = pkgJson.name || "";
@@ -499,7 +501,7 @@ const plugin = (args) => {
499
501
  if (await isCommonJS(id, resolved.id, resolved)) {
500
502
  const importerPkgJsonPath = (await this.resolve(importer))?.packageJsonPath;
501
503
  if (importerPkgJsonPath) {
502
- const importerPkgDir = relative(args.repoRootPath, dirname$1(importerPkgJsonPath));
504
+ const importerPkgDir = relative(args.repoRootPath, dirname(importerPkgJsonPath));
503
505
  const shimId$1 = `${CJS_SHIM_PREFIX$1}${importerPkgDir.replace(/\//g, "_")}_${id.replace(/\//g, "_")}`;
504
506
  shimMeta.set(shimId$1, {
505
507
  pkgDir: importerPkgDir,
@@ -544,7 +546,7 @@ import { createRequire } from 'node:module';
544
546
  import { fileURLToPath } from 'node:url';
545
547
  import { dirname, join } from 'node:path';
546
548
 
547
- const requireFromContext = createRequire(join(dirname(fileURLToPath(import.meta.url)), '${join$1("..", pkgDir, "package.json")}'));
549
+ const requireFromContext = createRequire(join(dirname(fileURLToPath(import.meta.url)), '${join("..", pkgDir, "package.json")}'));
548
550
  module.exports = requireFromContext('${pkgName}');
549
551
  `.trim() };
550
552
  return { code: `module.exports = require('${pkgName}');` };
@@ -598,7 +600,7 @@ const nodeFileTrace = async (args) => {
598
600
  }
599
601
  debug("NFT traced files count:", result.fileList.size);
600
602
  for (const file of result.fileList) {
601
- const absolutePath = join$1(args.repoRootPath, file);
603
+ const absolutePath = join(args.repoRootPath, file);
602
604
  const stats = await lstat(absolutePath);
603
605
  const outputPath = file;
604
606
  if (stats.isSymbolicLink() || stats.isFile()) files[outputPath] = new FileFsRef({
@@ -621,8 +623,8 @@ var __filename = typeof __filename !== 'undefined' ? __filename : __fileURLToPat
621
623
  var __dirname = typeof __dirname !== 'undefined' ? __dirname : __dirname_(__filename);
622
624
  `.trim();
623
625
  const rolldown$1 = async (args) => {
624
- const entrypointPath = join$1(args.workPath, args.entrypoint);
625
- const outputDir = join$1(args.workPath, args.out);
626
+ const entrypointPath = join(args.workPath, args.entrypoint);
627
+ const outputDir = join(args.workPath, args.out);
626
628
  const extension = extname(args.entrypoint);
627
629
  const extensionMap = {
628
630
  ".ts": {
@@ -652,7 +654,7 @@ const rolldown$1 = async (args) => {
652
654
  };
653
655
  const extensionInfo = extensionMap[extension] || extensionMap[".js"];
654
656
  let resolvedFormat = extensionInfo.format === "auto" ? void 0 : extensionInfo.format;
655
- const packageJsonPath = join$1(args.workPath, "package.json");
657
+ const packageJsonPath = join(args.workPath, "package.json");
656
658
  const external = [];
657
659
  let pkg = {};
658
660
  if (existsSync(packageJsonPath)) {
@@ -807,7 +809,7 @@ async function doTypeCheck(args, ts) {
807
809
  console.error(message);
808
810
  throw new Error("TypeScript type check failed");
809
811
  }
810
- const parsed = ts.parseJsonConfigFileContent(configRead.config, ts.sys, dirname$1(tsconfig), void 0, tsconfig);
812
+ const parsed = ts.parseJsonConfigFileContent(configRead.config, ts.sys, dirname(tsconfig), void 0, tsconfig);
811
813
  parseDiagnostics = parsed.errors;
812
814
  options = {
813
815
  ...parsed.options,
@@ -846,10 +848,10 @@ function resolveTypeScriptModule(workPath) {
846
848
  }
847
849
  }
848
850
  const findNearestTsconfig = async (workPath) => {
849
- const tsconfigPath = join$1(workPath, "tsconfig.json");
851
+ const tsconfigPath = join(workPath, "tsconfig.json");
850
852
  if (existsSync(tsconfigPath)) return tsconfigPath;
851
853
  if (workPath === "/") return;
852
- return findNearestTsconfig(join$1(workPath, ".."));
854
+ return findNearestTsconfig(join(workPath, ".."));
853
855
  };
854
856
 
855
857
  //#endregion
@@ -885,7 +887,7 @@ const createFrameworkRegex = (framework) => new RegExp(`(?:from|require|import)\
885
887
  const findEntrypoint = async (cwd) => {
886
888
  let packageJsonObject = null;
887
889
  try {
888
- const packageJson = await readFile(join$1(cwd, "package.json"), "utf-8");
890
+ const packageJson = await readFile(join(cwd, "package.json"), "utf-8");
889
891
  packageJsonObject = JSON.parse(packageJson);
890
892
  } catch (_) {}
891
893
  if (packageJsonObject) {
@@ -902,7 +904,7 @@ const findEntrypoint = async (cwd) => {
902
904
  let framework;
903
905
  if (packageJsonObject) framework = frameworks.find((framework$1) => packageJsonObject.dependencies?.[framework$1]);
904
906
  if (!framework) for (const entrypoint of entrypoints) {
905
- const entrypointPath = join$1(cwd, entrypoint);
907
+ const entrypointPath = join(cwd, entrypoint);
906
908
  try {
907
909
  await readFile(entrypointPath, "utf-8");
908
910
  return entrypoint;
@@ -910,7 +912,7 @@ const findEntrypoint = async (cwd) => {
910
912
  }
911
913
  const regex = framework ? createFrameworkRegex(framework) : void 0;
912
914
  for (const entrypoint of entrypoints) {
913
- const entrypointPath = join$1(cwd, entrypoint);
915
+ const entrypointPath = join(cwd, entrypoint);
914
916
  try {
915
917
  const content = await readFile(entrypointPath, "utf-8");
916
918
  if (regex) {
@@ -929,7 +931,7 @@ const findEntrypointOrThrow = async (cwd) => {
929
931
  //#region src/cervel/index.ts
930
932
  const require$2 = createRequire(import.meta.url);
931
933
  const getBuildSummary = async (outputDir) => {
932
- const buildSummary = await readFile$1(join$1(outputDir, ".cervel.json"), "utf-8");
934
+ const buildSummary = await readFile$1(join(outputDir, ".cervel.json"), "utf-8");
933
935
  return JSON.parse(buildSummary);
934
936
  };
935
937
  const build$1 = async (args) => {
@@ -946,13 +948,13 @@ const build$1 = async (args) => {
946
948
  out: args.out,
947
949
  span
948
950
  })]);
949
- await writeFile(join$1(args.workPath, args.out, ".cervel.json"), JSON.stringify({ handler: rolldownResult.result.handler }, null, 2));
951
+ await writeFile(join(args.workPath, args.out, ".cervel.json"), JSON.stringify({ handler: rolldownResult.result.handler }, null, 2));
950
952
  console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Build complete — Using ${Colors.bold(entrypoint)} as the root entrypoint.`));
951
953
  return { rolldownResult: rolldownResult.result };
952
954
  };
953
955
  const serve = async (args) => {
954
956
  const entrypoint = await findEntrypointOrThrow(args.workPath);
955
- const srvxBin = join$1(require$2.resolve("srvx"), "..", "..", "..", "bin", "srvx.mjs");
957
+ const srvxBin = join(require$2.resolve("srvx"), "..", "..", "..", "bin", "srvx.mjs");
956
958
  const tsxBin = require$2.resolve("tsx");
957
959
  const restArgs = Object.entries(args.rest).filter(([, value]) => value !== void 0 && value !== false).map(([key, value]) => typeof value === "boolean" ? `--${key}` : `--${key}=${value}`);
958
960
  if (!args.rest.import) restArgs.push("--import", tsxBin);
@@ -1024,8 +1026,8 @@ const resolveEntrypointAndFormat = async (args) => {
1024
1026
  }
1025
1027
  };
1026
1028
  const extensionInfo = extensionMap[extension] || extensionMap[".js"];
1027
- let resolvedFormat = extensionInfo.format === "auto" ? void 0 : extensionInfo.format;
1028
- const packageJsonPath = join$1(args.workPath, "package.json");
1029
+ let resolvedFormat = extensionInfo.format === "auto" ? args.defaultFormat : extensionInfo.format;
1030
+ const packageJsonPath = join(args.workPath, "package.json");
1029
1031
  let pkg = {};
1030
1032
  if (existsSync(packageJsonPath)) {
1031
1033
  const source = await readFile(packageJsonPath, "utf8");
@@ -1047,10 +1049,10 @@ const resolveEntrypointAndFormat = async (args) => {
1047
1049
  //#endregion
1048
1050
  //#region src/service-vc-init.ts
1049
1051
  async function applyServiceVcInit(args) {
1050
- const { format, extension } = await resolveShimFormat(args);
1051
- const handlerDir = dirname$1(args.handler);
1052
+ const { format, extension } = await resolveShimFormat$1(args);
1053
+ const handlerDir = dirname(args.handler);
1052
1054
  const vcInitName = `${basename(args.handler, extname(args.handler))}.__vc_service_vc_init${extension}`;
1053
- const vcInitHandler = handlerDir === "." ? vcInitName : join$1(handlerDir, vcInitName);
1055
+ const vcInitHandler = handlerDir === "." ? vcInitName : join(handlerDir, vcInitName);
1054
1056
  const handlerImportPath = `./${basename(args.handler)}`;
1055
1057
  const vcInitSource = format === "esm" ? createEsmServiceVcInit(handlerImportPath) : createCjsServiceVcInit(handlerImportPath);
1056
1058
  return {
@@ -1064,7 +1066,7 @@ async function applyServiceVcInit(args) {
1064
1066
  }
1065
1067
  };
1066
1068
  }
1067
- async function resolveShimFormat(args) {
1069
+ async function resolveShimFormat$1(args) {
1068
1070
  const { format } = await resolveEntrypointAndFormat({
1069
1071
  entrypoint: args.handler,
1070
1072
  workPath: args.workPath
@@ -1205,6 +1207,96 @@ module.exports = require(${JSON.stringify(handlerImportPath)})
1205
1207
  `.trimStart();
1206
1208
  }
1207
1209
 
1210
+ //#endregion
1211
+ //#region src/cron-dispatch.ts
1212
+ const TEMPLATES_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
1213
+ const ESM_TEMPLATE = readFileSync(join(TEMPLATES_DIR, "vc_cron_dispatch.mjs"), "utf-8");
1214
+ const CJS_TEMPLATE = readFileSync(join(TEMPLATES_DIR, "vc_cron_dispatch.cjs"), "utf-8");
1215
+ const USER_MODULE_PLACEHOLDER = /['"]__VC_USER_MODULE_PATH__['"]/g;
1216
+ const ROUTES_PLACEHOLDER = /'__VC_ROUTES_JSON__'/g;
1217
+ /**
1218
+ * Wrap a cron service handler with a dispatcher shim that:
1219
+ * - looks up the inbound request path in a route table baked into the
1220
+ * shim at build time and invokes the named export on the user module
1221
+ * (or the default export when the table value is `"default"`)
1222
+ * - verifies `CRON_SECRET` via `Authorization: Bearer ...` when set
1223
+ *
1224
+ * The dispatcher source lives in templates/vc_cron_dispatch.{mjs,cjs};
1225
+ * this function picks the right template, swaps in the user module
1226
+ * import path and the cron route table, and writes the result into the
1227
+ * lambda files.
1228
+ *
1229
+ * The route table is embedded inline rather than passed via a lambda
1230
+ * env var because AWS Lambda rejects env var names that don't start
1231
+ * with a letter — `__VC_CRON_ROUTES` would fail at deploy time. The
1232
+ * Python builder works around the same constraint by writing the route
1233
+ * table into its trampoline source.
1234
+ */
1235
+ async function applyCronDispatch(args) {
1236
+ const { format, extension } = await resolveShimFormat(args);
1237
+ const handlerDir = posix.dirname(args.handler);
1238
+ const dispatchName = `${posix.basename(args.handler, extname(args.handler))}.__vc_cron_dispatch${extension}`;
1239
+ const dispatchHandler = handlerDir === "." ? dispatchName : posix.join(handlerDir, dispatchName);
1240
+ const handlerImportPath = `./${posix.basename(args.handler)}`;
1241
+ const routesJson = JSON.stringify(args.routes);
1242
+ if (routesJson.includes("\\") || routesJson.includes("'")) throw new Error(`cron route table contains characters that need JS-string escaping: ${routesJson}`);
1243
+ const dispatchSource = (format === "esm" ? ESM_TEMPLATE : CJS_TEMPLATE).replace(USER_MODULE_PLACEHOLDER, JSON.stringify(handlerImportPath)).replace(ROUTES_PLACEHOLDER, `'${routesJson}'`);
1244
+ return {
1245
+ handler: dispatchHandler,
1246
+ files: {
1247
+ ...args.files,
1248
+ [dispatchHandler]: new FileBlob({
1249
+ data: dispatchSource,
1250
+ mode: 420
1251
+ })
1252
+ }
1253
+ };
1254
+ }
1255
+ async function resolveShimFormat(args) {
1256
+ const { format } = await resolveEntrypointAndFormat({
1257
+ entrypoint: args.handler,
1258
+ workPath: args.workPath,
1259
+ defaultFormat: "cjs"
1260
+ });
1261
+ return {
1262
+ format,
1263
+ extension: extname(args.handler) || (format === "esm" ? ".mjs" : ".cjs")
1264
+ };
1265
+ }
1266
+
1267
+ //#endregion
1268
+ //#region src/crons.ts
1269
+ const DYNAMIC_SCHEDULE = "<dynamic>";
1270
+ /** Build the JSON route table embedded in `__VC_CRON_ROUTES`. */
1271
+ function buildCronRouteTable(crons) {
1272
+ const table = {};
1273
+ for (const cron of crons) table[cron.path] = "default";
1274
+ return table;
1275
+ }
1276
+ /**
1277
+ * Compute cron entries for a JS/TS cron service build.
1278
+ *
1279
+ * Mirrors `packages/python/src/crons.ts` for static schedules. Returns
1280
+ * `undefined` when the service is not schedule-triggered. Throws on
1281
+ * `<dynamic>` schedules — that path is reserved for a follow-up.
1282
+ *
1283
+ * v1 always invokes the user module's default export, so this returns
1284
+ * plain `Cron[]` (no handler-name field). When `handlerFunction` or
1285
+ * `<dynamic>` support lands, this will need to grow a per-path handler
1286
+ * name back.
1287
+ */
1288
+ function getServiceCrons(opts) {
1289
+ const { service, entrypoint } = opts;
1290
+ if (!service || !isScheduleTriggeredService(service)) return;
1291
+ if (!service.name || typeof service.schedule !== "string") return;
1292
+ if (!entrypoint) throw new Error("Cron service is missing an entrypoint");
1293
+ if (service.schedule === DYNAMIC_SCHEDULE) throw new Error("Dynamic cron schedules (\"<dynamic>\") are not yet supported for JavaScript/TypeScript services. Use a static cron expression in vercel.json.");
1294
+ return [{
1295
+ path: getInternalServiceCronPath(service.name, entrypoint, "cron"),
1296
+ schedule: service.schedule
1297
+ }];
1298
+ }
1299
+
1208
1300
  //#endregion
1209
1301
  //#region src/rolldown/nft.ts
1210
1302
  const nft = async (args) => {
@@ -1230,7 +1322,7 @@ const nft = async (args) => {
1230
1322
  }
1231
1323
  });
1232
1324
  for (const file of nftResult.fileList) {
1233
- const absolutePath = join$1(args.repoRootPath, file);
1325
+ const absolutePath = join(args.repoRootPath, file);
1234
1326
  let stats;
1235
1327
  try {
1236
1328
  stats = await lstat(absolutePath);
@@ -1239,7 +1331,7 @@ const nft = async (args) => {
1239
1331
  throw error;
1240
1332
  }
1241
1333
  const outputPath = file;
1242
- if (args.localBuildFiles.has(join$1(args.repoRootPath, outputPath))) continue;
1334
+ if (args.localBuildFiles.has(join(args.repoRootPath, outputPath))) continue;
1243
1335
  if (stats.isSymbolicLink() || stats.isFile()) if (args.ignoreNodeModules) {
1244
1336
  if ((stats.isSymbolicLink() ? await stat(absolutePath) : stats).isFile()) {
1245
1337
  const content = await readFile(absolutePath, "utf-8");
@@ -1297,7 +1389,7 @@ const rolldown = async (args) => {
1297
1389
  if (!pkgJsonPath) return true;
1298
1390
  const pkgJson = await getPackageJson(pkgJsonPath);
1299
1391
  if (!pkgJson) return true;
1300
- const pkgDir = dirname$1(pkgJsonPath);
1392
+ const pkgDir = dirname(pkgJsonPath);
1301
1393
  const relativePath = resolvedPath.slice(pkgDir.length + 1).replace(/\\/g, "/");
1302
1394
  const pkgName = pkgJson.name || "";
1303
1395
  const subpath = bareImport.startsWith(pkgName) ? `.${bareImport.slice(pkgName.length)}` || "." : ".";
@@ -1354,7 +1446,7 @@ const rolldown = async (args) => {
1354
1446
  if (resolved?.id && isLocalImport(resolved.id)) {
1355
1447
  if (existsSync(resolved.id)) localBuildFiles.add(resolved.id);
1356
1448
  } else if (!resolved && !(isBareImport(id) && importer)) {
1357
- const candidate = join$1(args.workPath, id);
1449
+ const candidate = join(args.workPath, id);
1358
1450
  if (existsSync(candidate)) localBuildFiles.add(candidate);
1359
1451
  }
1360
1452
  if (importer?.startsWith(CJS_SHIM_PREFIX) && isBareImport(id)) return {
@@ -1371,7 +1463,7 @@ const rolldown = async (args) => {
1371
1463
  if (resolved ? await isCommonJS(id, resolved.id, resolved) : false) {
1372
1464
  const importerPkgJsonPath = (await this.resolve(importer))?.packageJsonPath;
1373
1465
  if (importerPkgJsonPath) {
1374
- const importerPkgDir = relative(args.repoRootPath, dirname$1(importerPkgJsonPath));
1466
+ const importerPkgDir = relative(args.repoRootPath, dirname(importerPkgJsonPath));
1375
1467
  const shimId$1 = `${CJS_SHIM_PREFIX}${importerPkgDir.replace(/\//g, "_")}_${id.replace(/\//g, "_")}`;
1376
1468
  shimMeta.set(shimId$1, {
1377
1469
  pkgDir: importerPkgDir,
@@ -1412,7 +1504,7 @@ import { createRequire } from 'node:module';
1412
1504
  import { fileURLToPath } from 'node:url';
1413
1505
  import { dirname, join } from 'node:path';
1414
1506
 
1415
- const requireFromContext = createRequire(join(dirname(fileURLToPath(import.meta.url)), '${pkgDir ? join$1("..", pkgDir, "package.json") : "../package.json"}'));
1507
+ const requireFromContext = createRequire(join(dirname(fileURLToPath(import.meta.url)), '${pkgDir ? join("..", pkgDir, "package.json") : "../package.json"}'));
1416
1508
  module.exports = requireFromContext('${pkgName}');
1417
1509
  `.trim() };
1418
1510
  }
@@ -1495,13 +1587,13 @@ const introspection = async (args) => {
1495
1587
  const runIntrospection = async () => {
1496
1588
  const rolldownEsmLoaderPath = `file://${require$1.resolve("@vercel/backends/rolldown/esm")}`;
1497
1589
  const rolldownCjsLoaderPath = require$1.resolve("@vercel/backends/rolldown/cjs-hooks");
1498
- const handlerPath = join$1(args.workPath, args.entrypoint);
1590
+ const handlerPath = join(args.workPath, args.entrypoint);
1499
1591
  const files = args.files;
1500
- const tmpDir = mkdtempSync(join$1(tmpdir(), "vercel-introspection-"));
1592
+ const tmpDir = mkdtempSync(join(tmpdir(), "vercel-introspection-"));
1501
1593
  for (const [key, value] of Object.entries(files)) {
1502
1594
  if (!(value instanceof FileBlob) || typeof value.data !== "string") continue;
1503
- const filePath = join$1(tmpDir, key);
1504
- mkdirSync(dirname$1(filePath), { recursive: true });
1595
+ const filePath = join(tmpDir, key);
1596
+ mkdirSync(dirname(filePath), { recursive: true });
1505
1597
  writeFileSync(filePath, value.data);
1506
1598
  }
1507
1599
  let introspectionData;
@@ -1509,8 +1601,8 @@ const introspection = async (args) => {
1509
1601
  await new Promise((resolvePromise) => {
1510
1602
  try {
1511
1603
  debug("Spawning introspection process");
1512
- const outputTempDir = mkdtempSync(join$1(tmpdir(), "introspection-output-"));
1513
- const tempFilePath = join$1(outputTempDir, "output.txt");
1604
+ const outputTempDir = mkdtempSync(join(tmpdir(), "introspection-output-"));
1605
+ const tempFilePath = join(outputTempDir, "output.txt");
1514
1606
  const writeStream = createWriteStream(tempFilePath);
1515
1607
  let streamClosed = false;
1516
1608
  const child = spawn("node", [
@@ -1673,7 +1765,7 @@ const maybeDoBuildCommand = async (args, downloadResult) => {
1673
1765
  let outputDir;
1674
1766
  let entrypoint;
1675
1767
  if (buildCommandResult && outputSetting) if (outputSetting) {
1676
- const _outputDir = join$1(args.workPath, outputSetting);
1768
+ const _outputDir = join(args.workPath, outputSetting);
1677
1769
  const _entrypoint = await findEntrypointInOutputDir(_outputDir);
1678
1770
  if (_entrypoint) {
1679
1771
  outputDir = _outputDir;
@@ -1684,7 +1776,7 @@ const maybeDoBuildCommand = async (args, downloadResult) => {
1684
1776
  "build",
1685
1777
  "output"
1686
1778
  ]) {
1687
- const _outputDir = join$1(args.workPath, outputDirectory);
1779
+ const _outputDir = join(args.workPath, outputDirectory);
1688
1780
  if (existsSync(_outputDir)) {
1689
1781
  const _entrypoint = await findEntrypointInOutputDir(_outputDir);
1690
1782
  if (_entrypoint) {
@@ -1698,7 +1790,7 @@ const maybeDoBuildCommand = async (args, downloadResult) => {
1698
1790
  let files;
1699
1791
  if (outputDir && entrypoint) {
1700
1792
  files = await glob("**", outputDir);
1701
- for (const file of Object.keys(files)) localBuildFiles.add(join$1(outputDir, file));
1793
+ for (const file of Object.keys(files)) localBuildFiles.add(join(outputDir, file));
1702
1794
  }
1703
1795
  return {
1704
1796
  localBuildFiles,
@@ -1717,7 +1809,7 @@ const introspectApp = async (args) => {
1717
1809
  if (isExperimentalBackendsWithoutIntrospectionEnabled()) return defaultResult(args);
1718
1810
  const cjsLoaderPath = require.resolve("@vercel/backends/introspection/loaders/cjs");
1719
1811
  const rolldownEsmLoaderPath = `file://${require.resolve("@vercel/backends/introspection/loaders/rolldown-esm")}`;
1720
- const handlerPath = join$1(args.dir, args.handler);
1812
+ const handlerPath = join(args.dir, args.handler);
1721
1813
  const introspectionSchema$1 = z.object({
1722
1814
  frameworkSlug: z.string().optional(),
1723
1815
  routes: z.array(z.object({
@@ -1755,8 +1847,8 @@ const introspectApp = async (args) => {
1755
1847
  ...args.env
1756
1848
  }
1757
1849
  });
1758
- const tempDir = mkdtempSync(join$1(tmpdir(), "introspection-"));
1759
- const tempFilePath = join$1(tempDir, "output.txt");
1850
+ const tempDir = mkdtempSync(join(tmpdir(), "introspection-"));
1851
+ const tempFilePath = join(tempDir, "output.txt");
1760
1852
  const writeStream = createWriteStream(tempFilePath);
1761
1853
  let streamClosed = false;
1762
1854
  child.stdout?.pipe(writeStream);
@@ -1872,9 +1964,9 @@ const getFramework = (args) => {
1872
1964
  if (args.framework) {
1873
1965
  const frameworkLibPath = require.resolve(`${args.framework}`, { paths: [args.dir] });
1874
1966
  const findNearestPackageJson = (dir) => {
1875
- const packageJsonPath = join$1(dir, "package.json");
1967
+ const packageJsonPath = join(dir, "package.json");
1876
1968
  if (existsSync(packageJsonPath)) return packageJsonPath;
1877
- const parentDir = dirname$1(dir);
1969
+ const parentDir = dirname(dir);
1878
1970
  if (parentDir === dir) return;
1879
1971
  return findNearestPackageJson(parentDir);
1880
1972
  };
@@ -1903,6 +1995,7 @@ function hasExplicitBuildCommand(config) {
1903
1995
  return typeof cmd === "string" && cmd.trim().length > 0;
1904
1996
  }
1905
1997
  const build = async (args) => {
1998
+ if (typeof args.config?.handlerFunction === "string") throw new Error(`Named-function entrypoints are not supported for JavaScript services (got "${args.entrypoint}:${args.config.handlerFunction}"). Put each handler in its own file with a default export.`);
1906
1999
  const downloadResult = await downloadInstallAndBundle(args);
1907
2000
  const nodeVersion = await getNodeVersion(args.workPath, void 0, args.config, args.meta);
1908
2001
  const isBun = isBunVersion(nodeVersion);
@@ -1911,9 +2004,15 @@ const build = async (args) => {
1911
2004
  span.setAttributes({ "builder.name": builderName });
1912
2005
  const buildSpan = span.child("vc.builder.backends.build");
1913
2006
  return buildSpan.trace(async () => {
1914
- const entrypoint = await findEntrypointOrThrow(args.workPath);
2007
+ const explicit = args.entrypoint && args.entrypoint !== "package.json" ? args.entrypoint : null;
2008
+ const entrypoint = explicit && existsSync(join(args.workPath, explicit)) ? explicit : await findEntrypointOrThrow(args.workPath);
1915
2009
  debug("Entrypoint", entrypoint);
1916
2010
  args.entrypoint = entrypoint;
2011
+ const cronEntries = getServiceCrons({
2012
+ service: args.service,
2013
+ entrypoint
2014
+ });
2015
+ const isCronService = cronEntries !== void 0;
1917
2016
  const userBuildResult = await maybeDoBuildCommand(args, downloadResult);
1918
2017
  const functionConfig = args.config.functions?.[entrypoint];
1919
2018
  if (functionConfig) {
@@ -1922,9 +2021,10 @@ const build = async (args) => {
1922
2021
  }
1923
2022
  const rolldownResult = await rolldown({
1924
2023
  ...args,
1925
- span: buildSpan
2024
+ span: buildSpan,
2025
+ defaultFormat: isCronService ? "cjs" : void 0
1926
2026
  });
1927
- const introspectionPromise = rolldownResult.framework.slug === "hono" ? introspection({
2027
+ const introspectionPromise = !isCronService && rolldownResult.framework.slug === "hono" ? introspection({
1928
2028
  ...args,
1929
2029
  span: buildSpan,
1930
2030
  files: rolldownResult.files,
@@ -1964,7 +2064,8 @@ const build = async (args) => {
1964
2064
  cliType: downloadResult.cliType,
1965
2065
  lockfilePath: downloadResult.lockfilePath,
1966
2066
  lockfileVersion: downloadResult.lockfileVersion,
1967
- framework: rolldownResult.framework.slug || void 0
2067
+ framework: rolldownResult.framework.slug || void 0,
2068
+ serviceType: args.service ? getReportedServiceType(args.service) : void 0
1968
2069
  });
1969
2070
  } catch (err) {
1970
2071
  debug(`Failed to write node manifest: ${err instanceof Error ? err.message : String(err)}`);
@@ -1982,7 +2083,16 @@ const build = async (args) => {
1982
2083
  const shouldStripServiceRoutePrefix = !!serviceRoutePrefix && (typeof args.config?.serviceName === "string" || !!args.service);
1983
2084
  let lambdaFiles = files;
1984
2085
  let lambdaHandler = handler;
1985
- if (shouldStripServiceRoutePrefix) {
2086
+ if (isCronService && cronEntries) {
2087
+ const dispatched = await applyCronDispatch({
2088
+ files,
2089
+ handler,
2090
+ workPath: nftWorkPath,
2091
+ routes: buildCronRouteTable(cronEntries)
2092
+ });
2093
+ lambdaFiles = dispatched.files;
2094
+ lambdaHandler = dispatched.handler;
2095
+ } else if (shouldStripServiceRoutePrefix) {
1986
2096
  const shimmedLambda = await applyServiceVcInit({
1987
2097
  files,
1988
2098
  handler,
@@ -2018,7 +2128,7 @@ const build = async (args) => {
2018
2128
  dest: internalServiceFunctionPath
2019
2129
  };
2020
2130
  };
2021
- const routes = [
2131
+ const routes = isCronService ? [{ handle: "filesystem" }] : [
2022
2132
  { handle: "filesystem" },
2023
2133
  ...introspectionResult.routes.map(remapRouteDestination),
2024
2134
  {
@@ -2034,7 +2144,8 @@ const build = async (args) => {
2034
2144
  }
2035
2145
  return {
2036
2146
  routes,
2037
- output
2147
+ output,
2148
+ ...cronEntries ? { crons: cronEntries } : {}
2038
2149
  };
2039
2150
  });
2040
2151
  };
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  //#region src/introspection/loaders/rolldown-hooks.d.ts
3
2
  declare function resolve(specifier: string, context: {
4
3
  parentURL?: string;
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  //#region src/rolldown/hooks.d.ts
3
2
  interface ResolveContext {
4
3
  parentURL?: string;
@@ -1,8 +1,9 @@
1
1
  import { BuildOptions, Files, Span } from "@vercel/build-utils";
2
2
 
3
3
  //#region src/rolldown/index.d.ts
4
- declare const rolldown: (args: Pick<BuildOptions, 'entrypoint' | 'workPath' | 'repoRootPath' | 'config'> & {
4
+ declare const rolldown: (args: Pick<BuildOptions, "entrypoint" | "workPath" | "repoRootPath" | "config"> & {
5
5
  span?: Span;
6
+ defaultFormat?: "esm" | "cjs";
6
7
  }) => Promise<{
7
8
  files: Files;
8
9
  handler: string;
@@ -68,7 +68,7 @@ const resolveEntrypointAndFormat = async (args) => {
68
68
  }
69
69
  };
70
70
  const extensionInfo = extensionMap[extension] || extensionMap[".js"];
71
- let resolvedFormat = extensionInfo.format === "auto" ? void 0 : extensionInfo.format;
71
+ let resolvedFormat = extensionInfo.format === "auto" ? args.defaultFormat : extensionInfo.format;
72
72
  const packageJsonPath = join(args.workPath, "package.json");
73
73
  let pkg = {};
74
74
  if (existsSync(packageJsonPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/backends",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./dist/index.mjs",
6
6
  "homepage": "https://vercel.com/docs",
@@ -21,7 +21,8 @@
21
21
  "directory": "packages/backends"
22
22
  },
23
23
  "files": [
24
- "dist"
24
+ "dist",
25
+ "templates"
25
26
  ],
26
27
  "dependencies": {
27
28
  "@yarnpkg/parsers": "^3.0.0",
@@ -36,19 +37,14 @@
36
37
  "srvx": "0.8.9",
37
38
  "tsx": "4.21.0",
38
39
  "zod": "3.22.4",
39
- "@vercel/build-utils": "13.21.0"
40
- },
41
- "peerDependencies": {
42
- "typescript": "^4.0.0 || ^5.0.0"
40
+ "@vercel/build-utils": "13.22.0"
43
41
  },
44
42
  "devDependencies": {
45
43
  "@types/express": "5.0.3",
46
- "@types/js-yaml": "3.12.1",
47
44
  "@types/fs-extra": "11",
48
- "@types/jest": "27.5.1",
45
+ "@types/js-yaml": "3.12.1",
49
46
  "@types/node": "20.11.0",
50
47
  "hono": "4.10.1",
51
- "jest-junit": "16.0.0",
52
48
  "tsdown": "0.16.3",
53
49
  "vite": "^5.1.6",
54
50
  "vitest": "^2.0.1"
@@ -0,0 +1,119 @@
1
+ // Runtime cron dispatcher (CJS). Generated into a Vercel cron service
2
+ // lambda by `applyCronDispatch` in @vercel/backends. JS analog of
3
+ // vercel-runtime/src/vercel_runtime/crons.py — pre-resolves the route
4
+ // table at module load and dispatches inbound requests to the matching
5
+ // handler on the user's module.
6
+ //
7
+ // `__VC_USER_MODULE_PATH__` is replaced at build time with the relative
8
+ // path to the user's cron entrypoint.
9
+ const { timingSafeEqual: __vc_timingSafeEqual } = require('node:crypto');
10
+ const __vc_user_module = require('__VC_USER_MODULE_PATH__');
11
+
12
+ function jsonResponse(res, status, body) {
13
+ res.statusCode = status;
14
+ res.setHeader('content-type', 'application/json');
15
+ res.end(JSON.stringify(body));
16
+ }
17
+
18
+ function unwrapDefault(value) {
19
+ let current = value;
20
+ for (let i = 0; i < 5; i++) {
21
+ if (
22
+ current &&
23
+ typeof current === 'object' &&
24
+ 'default' in current &&
25
+ current.default
26
+ ) {
27
+ current = current.default;
28
+ } else {
29
+ break;
30
+ }
31
+ }
32
+ return current;
33
+ }
34
+
35
+ function resolveCronHandler(userModule, name) {
36
+ if (name === 'default') {
37
+ const unwrapped = unwrapDefault(userModule);
38
+ if (typeof unwrapped === 'function') return unwrapped;
39
+ if (typeof userModule === 'function') return userModule;
40
+ return undefined;
41
+ }
42
+ const fn = userModule != null ? userModule[name] : undefined;
43
+ return typeof fn === 'function' ? fn : undefined;
44
+ }
45
+
46
+ function safeBearerEqual(authHeader, secret) {
47
+ if (typeof authHeader !== 'string') return false;
48
+ const expected = 'Bearer ' + secret;
49
+ const a = Buffer.from(authHeader);
50
+ const b = Buffer.from(expected);
51
+ if (a.length !== b.length) return false;
52
+ return __vc_timingSafeEqual(a, b);
53
+ }
54
+
55
+ // Pre-resolve every route at module load. A bad route table fails the
56
+ // lambda at boot rather than at first request.
57
+ //
58
+ // `__VC_ROUTES_JSON__` is replaced at build time with the JSON route
59
+ // table. Embedded inline here (instead of read from env) because AWS
60
+ // Lambda env var names must start with a letter, so `__VC_CRON_ROUTES`
61
+ // would fail at deploy time. The Python builder works around the same
62
+ // constraint by writing the route table into its trampoline source.
63
+ const __vc_routes_parsed = JSON.parse('__VC_ROUTES_JSON__');
64
+ const RESOLVED_HANDLERS = new Map();
65
+ for (const __vc_path in __vc_routes_parsed) {
66
+ const __vc_name = __vc_routes_parsed[__vc_path];
67
+ const __vc_fn = resolveCronHandler(__vc_user_module, __vc_name);
68
+ if (typeof __vc_fn !== 'function') {
69
+ throw new Error(
70
+ 'cron handler "' +
71
+ __vc_name +
72
+ '" is not a function in the user module (route: ' +
73
+ __vc_path +
74
+ ')'
75
+ );
76
+ }
77
+ RESOLVED_HANDLERS.set(__vc_path, __vc_fn);
78
+ }
79
+
80
+ async function vcCronDispatch(req, res) {
81
+ // Drain any inbound request body so the underlying stream completes
82
+ // independently of whether the user's cron handler reads it. Cron
83
+ // handlers take no arguments — body bytes are never used. Idempotent
84
+ // and a no-op for non-streaming runtimes.
85
+ if (typeof req.resume === 'function') req.resume();
86
+
87
+ const method = (req.method || 'GET').toUpperCase();
88
+ if (method !== 'GET' && method !== 'POST') {
89
+ jsonResponse(res, 405, { error: 'method not allowed' });
90
+ return;
91
+ }
92
+ const secret = process.env.CRON_SECRET;
93
+ if (secret) {
94
+ const headers = req.headers || {};
95
+ const authorization = headers.authorization || headers.Authorization;
96
+ if (!safeBearerEqual(authorization, secret)) {
97
+ jsonResponse(res, 401, { error: 'unauthorized' });
98
+ return;
99
+ }
100
+ }
101
+ const rawUrl = typeof req.url === 'string' ? req.url : '/';
102
+ const path = rawUrl.split('?')[0];
103
+ const fn = RESOLVED_HANDLERS.get(path);
104
+ if (!fn) {
105
+ jsonResponse(res, 404, { error: 'no cron handler for path: ' + path });
106
+ return;
107
+ }
108
+ try {
109
+ await fn();
110
+ jsonResponse(res, 200, { ok: true });
111
+ } catch (err) {
112
+ console.error(err);
113
+ jsonResponse(res, 500, { error: 'internal' });
114
+ }
115
+ }
116
+
117
+ module.exports = function (req, res) {
118
+ return vcCronDispatch(req, res);
119
+ };
@@ -0,0 +1,119 @@
1
+ // Runtime cron dispatcher (ESM). Generated into a Vercel cron service
2
+ // lambda by `applyCronDispatch` in @vercel/backends. JS analog of
3
+ // vercel-runtime/src/vercel_runtime/crons.py — pre-resolves the route
4
+ // table at module load and dispatches inbound requests to the matching
5
+ // handler on the user's module.
6
+ //
7
+ // `__VC_USER_MODULE_PATH__` is replaced at build time with the relative
8
+ // path to the user's cron entrypoint.
9
+ import { timingSafeEqual as __vc_timingSafeEqual } from 'node:crypto';
10
+ import * as __vc_user_module from '__VC_USER_MODULE_PATH__';
11
+
12
+ function jsonResponse(res, status, body) {
13
+ res.statusCode = status;
14
+ res.setHeader('content-type', 'application/json');
15
+ res.end(JSON.stringify(body));
16
+ }
17
+
18
+ function unwrapDefault(value) {
19
+ let current = value;
20
+ for (let i = 0; i < 5; i++) {
21
+ if (
22
+ current &&
23
+ typeof current === 'object' &&
24
+ 'default' in current &&
25
+ current.default
26
+ ) {
27
+ current = current.default;
28
+ } else {
29
+ break;
30
+ }
31
+ }
32
+ return current;
33
+ }
34
+
35
+ function resolveCronHandler(userModule, name) {
36
+ if (name === 'default') {
37
+ const unwrapped = unwrapDefault(userModule);
38
+ if (typeof unwrapped === 'function') return unwrapped;
39
+ if (typeof userModule === 'function') return userModule;
40
+ return undefined;
41
+ }
42
+ const fn = userModule != null ? userModule[name] : undefined;
43
+ return typeof fn === 'function' ? fn : undefined;
44
+ }
45
+
46
+ function safeBearerEqual(authHeader, secret) {
47
+ if (typeof authHeader !== 'string') return false;
48
+ const expected = 'Bearer ' + secret;
49
+ const a = Buffer.from(authHeader);
50
+ const b = Buffer.from(expected);
51
+ if (a.length !== b.length) return false;
52
+ return __vc_timingSafeEqual(a, b);
53
+ }
54
+
55
+ // Pre-resolve every route at module load. A bad route table fails the
56
+ // lambda at boot rather than at first request.
57
+ //
58
+ // `__VC_ROUTES_JSON__` is replaced at build time with the JSON route
59
+ // table. Embedded inline here (instead of read from env) because AWS
60
+ // Lambda env var names must start with a letter, so `__VC_CRON_ROUTES`
61
+ // would fail at deploy time. The Python builder works around the same
62
+ // constraint by writing the route table into its trampoline source.
63
+ const __vc_routes_parsed = JSON.parse('__VC_ROUTES_JSON__');
64
+ const RESOLVED_HANDLERS = new Map();
65
+ for (const __vc_path in __vc_routes_parsed) {
66
+ const __vc_name = __vc_routes_parsed[__vc_path];
67
+ const __vc_fn = resolveCronHandler(__vc_user_module, __vc_name);
68
+ if (typeof __vc_fn !== 'function') {
69
+ throw new Error(
70
+ 'cron handler "' +
71
+ __vc_name +
72
+ '" is not a function in the user module (route: ' +
73
+ __vc_path +
74
+ ')'
75
+ );
76
+ }
77
+ RESOLVED_HANDLERS.set(__vc_path, __vc_fn);
78
+ }
79
+
80
+ async function vcCronDispatch(req, res) {
81
+ // Drain any inbound request body so the underlying stream completes
82
+ // independently of whether the user's cron handler reads it. Cron
83
+ // handlers take no arguments — body bytes are never used. Idempotent
84
+ // and a no-op for non-streaming runtimes.
85
+ if (typeof req.resume === 'function') req.resume();
86
+
87
+ const method = (req.method || 'GET').toUpperCase();
88
+ if (method !== 'GET' && method !== 'POST') {
89
+ jsonResponse(res, 405, { error: 'method not allowed' });
90
+ return;
91
+ }
92
+ const secret = process.env.CRON_SECRET;
93
+ if (secret) {
94
+ const headers = req.headers || {};
95
+ const authorization = headers.authorization || headers.Authorization;
96
+ if (!safeBearerEqual(authorization, secret)) {
97
+ jsonResponse(res, 401, { error: 'unauthorized' });
98
+ return;
99
+ }
100
+ }
101
+ const rawUrl = typeof req.url === 'string' ? req.url : '/';
102
+ const path = rawUrl.split('?')[0];
103
+ const fn = RESOLVED_HANDLERS.get(path);
104
+ if (!fn) {
105
+ jsonResponse(res, 404, { error: 'no cron handler for path: ' + path });
106
+ return;
107
+ }
108
+ try {
109
+ await fn();
110
+ jsonResponse(res, 200, { ok: true });
111
+ } catch (err) {
112
+ console.error(err);
113
+ jsonResponse(res, 500, { error: 'internal' });
114
+ }
115
+ }
116
+
117
+ export default function (req, res) {
118
+ return vcCronDispatch(req, res);
119
+ }