@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 +0 -1
- package/dist/index.mjs +166 -55
- package/dist/introspection/loaders/rolldown-hooks.d.mts +0 -1
- package/dist/rolldown/hooks.d.mts +0 -1
- package/dist/rolldown/index.d.mts +2 -1
- package/dist/rolldown/index.mjs +1 -1
- package/package.json +5 -9
- package/templates/vc_cron_dispatch.cjs +119 -0
- package/templates/vc_cron_dispatch.mjs +119 -0
package/dist/index.d.mts
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { builtinModules, createRequire } from "node:module";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
625
|
-
const outputDir = join
|
|
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
|
|
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
|
|
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
|
|
851
|
+
const tsconfigPath = join(workPath, "tsconfig.json");
|
|
850
852
|
if (existsSync(tsconfigPath)) return tsconfigPath;
|
|
851
853
|
if (workPath === "/") return;
|
|
852
|
-
return findNearestTsconfig(join
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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" ?
|
|
1028
|
-
const packageJsonPath = join
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1590
|
+
const handlerPath = join(args.workPath, args.entrypoint);
|
|
1499
1591
|
const files = args.files;
|
|
1500
|
-
const tmpDir = mkdtempSync(join
|
|
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
|
|
1504
|
-
mkdirSync(dirname
|
|
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
|
|
1513
|
-
const tempFilePath = join
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1759
|
-
const tempFilePath = join
|
|
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
|
|
1967
|
+
const packageJsonPath = join(dir, "package.json");
|
|
1876
1968
|
if (existsSync(packageJsonPath)) return packageJsonPath;
|
|
1877
|
-
const parentDir = dirname
|
|
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
|
|
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 (
|
|
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,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,
|
|
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;
|
package/dist/rolldown/index.mjs
CHANGED
|
@@ -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" ?
|
|
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
|
+
"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.
|
|
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/
|
|
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
|
+
}
|