@vercel/backends 0.7.1 → 0.8.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.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  import { builtinModules, createRequire } from "node:module";
2
- import { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
2
  import { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
4
3
  import { delimiter as delimiter$1, dirname as dirname$1, join as join$1 } from "path";
5
4
  import { FileBlob, FileFsRef, NodejsLambda, Span, createDiagnostics, debug, defaultCachePathGlob, download, execCommand, generateProjectManifest, getEnvForPackageManager, getInternalServiceCronPath, getLambdaOptionsFromFunction, getNodeBinPaths, getNodeVersion, getReportedServiceType, glob, isBackendFramework, isBunVersion, isExperimentalBackendsWithoutIntrospectionEnabled, isScheduleTriggeredService, runNpmInstall, runPackageJsonScript, scanParentDirs } from "@vercel/build-utils";
5
+ import { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import { lstat, readFile, rm, stat } from "node:fs/promises";
7
+ import { fileURLToPath } from "node:url";
7
8
  import { build as build$2 } from "rolldown";
8
9
  import { exports } from "resolve.exports";
9
10
  import { isNativeError } from "node:util/types";
@@ -11,7 +12,6 @@ import { nodeFileTrace as nodeFileTrace$1, resolve as resolve$1 } from "@vercel/
11
12
  import { transform } from "oxc-transform";
12
13
  import execa from "execa";
13
14
  import { readFile as readFile$1, writeFile } from "fs/promises";
14
- import { fileURLToPath } from "node:url";
15
15
  import { spawn } from "node:child_process";
16
16
  import { tmpdir } from "node:os";
17
17
  import { z } from "zod";
@@ -70,222 +70,104 @@ async function maybeExecBuildCommand(args, { spawnEnv }) {
70
70
  const diagnostics = createDiagnostics("node");
71
71
 
72
72
  //#endregion
73
- //#region src/cervel/plugin.ts
74
- const CJS_SHIM_PREFIX$1 = "\0cjs-shim:";
75
- const plugin = (args) => {
76
- const packageJsonCache = /* @__PURE__ */ new Map();
77
- const shimMeta = /* @__PURE__ */ new Map();
78
- const { tracedPaths } = args.context;
79
- const isBareImport = (id) => {
80
- return !id.startsWith(".") && !id.startsWith("/") && !/^[a-z][a-z0-9+.-]*:/i.test(id);
81
- };
82
- /**
83
- * Read and cache package.json contents
84
- */
85
- const getPackageJson = async (pkgPath) => {
86
- if (packageJsonCache.has(pkgPath)) return packageJsonCache.get(pkgPath);
87
- try {
88
- const contents = await readFile(pkgPath, "utf-8");
89
- const parsed = JSON.parse(contents);
90
- packageJsonCache.set(pkgPath, parsed);
91
- return parsed;
92
- } catch {
93
- packageJsonCache.set(pkgPath, null);
94
- return null;
73
+ //#region src/find-entrypoint.ts
74
+ const frameworks = [
75
+ "express",
76
+ "hono",
77
+ "elysia",
78
+ "fastify",
79
+ "@nestjs/core",
80
+ "h3"
81
+ ];
82
+ const entrypointFilenames = [
83
+ "app",
84
+ "index",
85
+ "server",
86
+ "main",
87
+ "src/app",
88
+ "src/index",
89
+ "src/server",
90
+ "src/main"
91
+ ];
92
+ const entrypointExtensions = [
93
+ "js",
94
+ "cjs",
95
+ "mjs",
96
+ "ts",
97
+ "cts",
98
+ "mts"
99
+ ];
100
+ const entrypoints = entrypointFilenames.flatMap((filename) => entrypointExtensions.map((extension) => `${filename}.${extension}`));
101
+ const createFrameworkRegex = (framework) => new RegExp(`(?:from|require|import)\\s*(?:\\(\\s*)?["']${framework}["']\\s*(?:\\))?`, "g");
102
+ const findEntrypoint = async (cwd) => {
103
+ let packageJsonObject = null;
104
+ try {
105
+ const packageJson = await readFile(join(cwd, "package.json"), "utf-8");
106
+ packageJsonObject = JSON.parse(packageJson);
107
+ } catch (_) {}
108
+ if (packageJsonObject) {
109
+ const main = typeof packageJsonObject.main === "string" ? packageJsonObject.main.trim() : "";
110
+ if (main) {
111
+ const abs = resolve(cwd, main);
112
+ const rel = relative(cwd, abs);
113
+ if (!rel.startsWith("..") && rel !== "") try {
114
+ await readFile(abs, "utf-8");
115
+ return rel.split(sep).join("/");
116
+ } catch {}
95
117
  }
96
- };
97
- /**
98
- * Determine if a resolved module is CommonJS based on package.json exports
99
- */
100
- const isCommonJS = async (bareImport, resolvedPath, resolvedInfo) => {
101
- const ext = extname(resolvedPath);
102
- if (ext === ".cjs") return true;
103
- if (ext === ".mjs") return false;
104
- if (ext === ".js" || ext === ".ts") {
105
- const pkgJsonPath = resolvedInfo.packageJsonPath;
106
- if (!pkgJsonPath) return true;
107
- const pkgJson = await getPackageJson(pkgJsonPath);
108
- if (!pkgJson) return true;
109
- const pkgDir = dirname(pkgJsonPath);
110
- const relativePath = resolvedPath.startsWith(pkgDir) ? resolvedPath.slice(pkgDir.length + 1).replace(/\\/g, "/") : null;
111
- if (!relativePath) return pkgJson.type !== "module";
112
- const pkgName = pkgJson.name || "";
113
- const subpath = bareImport.startsWith(pkgName) ? `.${bareImport.slice(pkgName.length)}` || "." : ".";
114
- try {
115
- if (exports(pkgJson, subpath, {
116
- require: false,
117
- conditions: ["node", "import"]
118
- })?.some((p) => p === relativePath || p === `./${relativePath}`)) return false;
119
- if (exports(pkgJson, subpath, {
120
- require: true,
121
- conditions: ["node", "require"]
122
- })?.some((p) => p === relativePath || p === `./${relativePath}`)) return true;
123
- } catch (err) {
124
- console.warn("Export resolution failed::", err);
118
+ }
119
+ let framework;
120
+ if (packageJsonObject) framework = frameworks.find((framework$1) => packageJsonObject.dependencies?.[framework$1]);
121
+ if (!framework) for (const entrypoint of entrypoints) {
122
+ const entrypointPath = join(cwd, entrypoint);
123
+ try {
124
+ await readFile(entrypointPath, "utf-8");
125
+ return entrypoint;
126
+ } catch (_) {}
127
+ }
128
+ const regex = framework ? createFrameworkRegex(framework) : void 0;
129
+ for (const entrypoint of entrypoints) {
130
+ const entrypointPath = join(cwd, entrypoint);
131
+ try {
132
+ const content = await readFile(entrypointPath, "utf-8");
133
+ if (regex) {
134
+ if (regex.test(content)) return entrypoint;
125
135
  }
126
- if (pkgJson.module) return false;
127
- return pkgJson.type !== "module";
128
- }
129
- return true;
130
- };
131
- const isLocalImport = (id) => {
132
- if (id.startsWith("node:")) return false;
133
- if (id.includes("node_modules")) return false;
134
- return true;
135
- };
136
+ } catch (_) {}
137
+ }
138
+ };
139
+ const findEntrypointOrThrow = async (cwd) => {
140
+ const entrypoint = await findEntrypoint(cwd);
141
+ if (!entrypoint) throw new Error(`No entrypoint found in "${cwd}". Set package.json "main" to a server file, or add one of: ${entrypoints.join(", ")}`);
142
+ return entrypoint;
143
+ };
144
+ const findEntrypointWithHintOrThrow = async (workPath, configured) => {
145
+ const explicit = configured && configured !== "package.json" ? configured : null;
146
+ if (explicit && existsSync(join(workPath, explicit))) return explicit;
147
+ return findEntrypointOrThrow(workPath);
148
+ };
149
+ /**
150
+ * Normalized entrypoint detector for Node services. Wraps {@link findEntrypoint}
151
+ * and returns the result in the shared {@link DetectedEntrypoint} shape consumed
152
+ * by services auto-detection.
153
+ */
154
+ const detectEntrypoint = async ({ workPath }) => {
155
+ const file = await findEntrypoint(workPath);
156
+ if (!file) return null;
136
157
  return {
137
- name: "cervel",
138
- resolveId: {
139
- order: "pre",
140
- async handler(id, importer, rOpts) {
141
- if (id.startsWith(CJS_SHIM_PREFIX$1)) return {
142
- id,
143
- external: false
144
- };
145
- const resolved = await this.resolve(id, importer, rOpts);
146
- if (builtinModules.includes(id)) return {
147
- id: `node:${id}`,
148
- external: true
149
- };
150
- if (resolved?.id && isLocalImport(resolved.id)) tracedPaths.add(resolved.id);
151
- if (importer?.startsWith(CJS_SHIM_PREFIX$1) && isBareImport(id)) return {
152
- id,
153
- external: true
154
- };
155
- if (importer && isBareImport(id) && resolved?.id?.includes("node_modules")) {
156
- if (args.shimBareImports) {
157
- if (await isCommonJS(id, resolved.id, resolved)) {
158
- const importerPkgJsonPath = (await this.resolve(importer))?.packageJsonPath;
159
- if (importerPkgJsonPath) {
160
- const importerPkgDir = relative(args.repoRootPath, dirname(importerPkgJsonPath));
161
- const shimId$1 = `${CJS_SHIM_PREFIX$1}${importerPkgDir.replace(/\//g, "_")}_${id.replace(/\//g, "_")}`;
162
- shimMeta.set(shimId$1, {
163
- pkgDir: importerPkgDir,
164
- pkgName: id
165
- });
166
- return {
167
- id: shimId$1,
168
- external: false
169
- };
170
- }
171
- const shimId = `${CJS_SHIM_PREFIX$1}${id.replace(/\//g, "_")}`;
172
- shimMeta.set(shimId, {
173
- pkgDir: "",
174
- pkgName: id
175
- });
176
- return {
177
- id: shimId,
178
- external: false
179
- };
180
- }
181
- }
182
- return {
183
- external: true,
184
- id
185
- };
186
- }
187
- if (importer && isBareImport(id)) return resolved;
188
- return {
189
- external: true,
190
- ...resolved,
191
- id: resolved?.id || id
192
- };
193
- }
194
- },
195
- load: { async handler(id) {
196
- if (id.startsWith(CJS_SHIM_PREFIX$1)) {
197
- const meta = shimMeta.get(id);
198
- if (!meta) return { code: `module.exports = require('${id.slice(10)}');` };
199
- const { pkgDir, pkgName } = meta;
200
- if (pkgDir) return { code: `
201
- import { createRequire } from 'node:module';
202
- import { fileURLToPath } from 'node:url';
203
- import { dirname, join } from 'node:path';
204
-
205
- const requireFromContext = createRequire(join(dirname(fileURLToPath(import.meta.url)), '${join("..", pkgDir, "package.json")}'));
206
- module.exports = requireFromContext('${pkgName}');
207
- `.trim() };
208
- return { code: `module.exports = require('${pkgName}');` };
209
- }
210
- return null;
211
- } }
158
+ kind: "file",
159
+ entrypoint: file
212
160
  };
213
161
  };
214
162
 
215
163
  //#endregion
216
- //#region src/cervel/node-file-trace.ts
217
- const nodeFileTrace = async (args) => {
218
- const { span } = args;
219
- const files = {};
220
- const { tracedPaths } = args;
221
- const compiledSourceFiles = await glob("**/*", {
222
- cwd: args.outDir,
223
- follow: true,
224
- includeDirectories: true
225
- });
226
- for (const file of Object.keys(compiledSourceFiles)) files[file] = compiledSourceFiles[file];
227
- /**
228
- * While we're not using NFT to process source code, we are using it
229
- * to tree shake node deps, and include any fs reads for files that are
230
- * not part of the traced paths or compiled source files.
231
- * Most of this is identical to the `@vercel/node` implementation
232
- */
233
- const runNft = () => nodeFileTrace$1(Array.from(tracedPaths), {
234
- base: args.repoRootPath,
235
- processCwd: args.workPath,
236
- ts: true,
237
- mixedModules: true,
238
- async resolve(id, parent, job, cjsResolve) {
239
- return resolve$1(id, parent, job, cjsResolve);
240
- },
241
- async readFile(fsPath) {
242
- try {
243
- let source = await readFile(fsPath);
244
- if (fsPath.endsWith(".ts") && !fsPath.endsWith(".d.ts") || fsPath.endsWith(".tsx") || fsPath.endsWith(".mts") || fsPath.endsWith(".cts")) source = (await transform(fsPath, source.toString())).code;
245
- return source;
246
- } catch (error) {
247
- if (isNativeError(error) && "code" in error && (error.code === "ENOENT" || error.code === "EISDIR")) return null;
248
- throw error;
249
- }
250
- }
251
- });
252
- const result = await span.child("vc.builder.backends.nft").trace(runNft);
253
- if (!args.keepTracedPaths) for (const file of tracedPaths) {
254
- const relativeFile = relative(args.repoRootPath, file);
255
- result.fileList.delete(relativeFile);
256
- }
257
- debug("NFT traced files count:", result.fileList.size);
258
- for (const file of result.fileList) {
259
- const absolutePath = join(args.repoRootPath, file);
260
- const stats = await lstat(absolutePath);
261
- const outputPath = file;
262
- if (stats.isSymbolicLink() || stats.isFile()) files[outputPath] = new FileFsRef({
263
- fsPath: absolutePath,
264
- mode: stats.mode
265
- });
266
- }
267
- debug("Total files in context:", Object.keys(files).length);
268
- return files;
269
- };
270
-
271
- //#endregion
272
- //#region src/cervel/rolldown.ts
273
- const __dirname__filenameShim = `
274
- import { createRequire as __createRequire } from 'node:module';
275
- import { fileURLToPath as __fileURLToPath } from 'node:url';
276
- import { dirname as __dirname_ } from 'node:path';
277
- var require = typeof require !== 'undefined' ? require : __createRequire(import.meta.url);
278
- var __filename = typeof __filename !== 'undefined' ? __filename : __fileURLToPath(import.meta.url);
279
- var __dirname = typeof __dirname !== 'undefined' ? __dirname : __dirname_(__filename);
280
- `.trim();
281
- const rolldown$1 = async (args) => {
282
- const entrypointPath = join(args.workPath, args.entrypoint);
283
- const outputDir = join(args.workPath, args.out);
284
- const extension = extname(args.entrypoint);
285
- const extensionMap = {
286
- ".ts": {
287
- format: "auto",
288
- extension: "js"
164
+ //#region src/rolldown/resolve-format.ts
165
+ const resolveEntrypointAndFormat = async (args) => {
166
+ const extension = extname(args.entrypoint);
167
+ const extensionMap = {
168
+ ".ts": {
169
+ format: "auto",
170
+ extension: "js"
289
171
  },
290
172
  ".mts": {
291
173
  format: "esm",
@@ -309,9 +191,8 @@ const rolldown$1 = async (args) => {
309
191
  }
310
192
  };
311
193
  const extensionInfo = extensionMap[extension] || extensionMap[".js"];
312
- let resolvedFormat = extensionInfo.format === "auto" ? void 0 : extensionInfo.format;
194
+ let resolvedFormat = extensionInfo.format === "auto" ? args.defaultFormat : extensionInfo.format;
313
195
  const packageJsonPath = join(args.workPath, "package.json");
314
- const external = [];
315
196
  let pkg = {};
316
197
  if (existsSync(packageJsonPath)) {
317
198
  const source = await readFile(packageJsonPath, "utf8");
@@ -322,431 +203,46 @@ const rolldown$1 = async (args) => {
322
203
  }
323
204
  if (extensionInfo.format === "auto") if (pkg.type === "module") resolvedFormat = "esm";
324
205
  else resolvedFormat = "cjs";
325
- for (const dependency of Object.keys(pkg.dependencies || {})) external.push(dependency);
326
- for (const dependency of Object.keys(pkg.devDependencies || {})) external.push(dependency);
327
- for (const dependency of Object.keys(pkg.peerDependencies || {})) external.push(dependency);
328
- for (const dependency of Object.keys(pkg.optionalDependencies || {})) external.push(dependency);
329
- }
330
- const resolvedExtension = resolvedFormat === "esm" ? "mjs" : "cjs";
331
- const context = { tracedPaths: /* @__PURE__ */ new Set() };
332
- const runRolldown = () => build$2({
333
- input: entrypointPath,
334
- cwd: args.workPath,
335
- platform: "node",
336
- tsconfig: true,
337
- plugins: [plugin({
338
- repoRootPath: args.repoRootPath,
339
- outDir: outputDir,
340
- workPath: args.workPath,
341
- shimBareImports: resolvedFormat === "esm",
342
- context
343
- })],
344
- output: {
345
- cleanDir: true,
346
- dir: outputDir,
347
- format: resolvedFormat,
348
- entryFileNames: `[name].${resolvedExtension}`,
349
- preserveModules: true,
350
- preserveModulesRoot: args.repoRootPath,
351
- sourcemap: false,
352
- banner: resolvedFormat === "esm" ? __dirname__filenameShim : void 0
353
- }
354
- });
355
- const out = await args.span.child("vc.builder.backends.rolldown").trace(runRolldown);
356
- let handler = null;
357
- for (const entry of out.output) if (entry.type === "chunk") {
358
- if (entry.isEntry) handler = entry.fileName;
359
206
  }
360
- if (typeof handler !== "string") throw new Error(`Unable to resolve module for ${args.entrypoint}`);
361
- const outputFiles = await nodeFileTrace({
362
- outDir: outputDir,
363
- tracedPaths: Array.from(context.tracedPaths),
364
- repoRootPath: args.repoRootPath,
365
- workPath: args.workPath,
366
- keepTracedPaths: false,
367
- span: args.span
368
- });
369
- const cleanup = async () => {
370
- await rm(outputDir, {
371
- recursive: true,
372
- force: true
373
- });
374
- };
207
+ if (!resolvedFormat) throw new Error(`Unable to resolve format for ${args.entrypoint}`);
375
208
  return {
376
- result: {
377
- handler,
378
- outputDir,
379
- outputFiles
380
- },
381
- cleanup
209
+ format: resolvedFormat,
210
+ extension: resolvedFormat === "esm" ? "mjs" : "cjs"
382
211
  };
383
212
  };
384
213
 
385
214
  //#endregion
386
- //#region src/cervel/utils.ts
387
- const noColor = globalThis.process?.env?.NO_COLOR === "1" || globalThis.process?.env?.TERM === "dumb";
388
- const resets = {
389
- 1: 22,
390
- 31: 39,
391
- 32: 39,
392
- 33: 39,
393
- 34: 39,
394
- 35: 39,
395
- 36: 39,
396
- 90: 39
397
- };
398
- const _c = (c) => (text) => {
399
- if (noColor) return text;
400
- return `\u001B[${c}m${text}\u001B[${resets[c] ?? 0}m`;
401
- };
402
- const Colors = {
403
- bold: _c(1),
404
- red: _c(31),
405
- green: _c(32),
406
- yellow: _c(33),
407
- blue: _c(34),
408
- magenta: _c(35),
409
- cyan: _c(36),
410
- gray: _c(90),
411
- url: (title, url) => noColor ? `[${title}](${url})` : `\u001B]8;;${url}\u001B\\${title}\u001B]8;;\u001B\\`
412
- };
413
-
414
- //#endregion
415
- //#region src/typescript.ts
416
- /**
417
- * Typecheck via the TypeScript compiler API (`createProgram`, `getPreEmitDiagnostics`),
418
- * not by spawning the `tsc` binary.
419
- *
420
- * We only want to validate the deployment entrypoint and its import graph, not every
421
- * file matched by `tsconfig` `include`. The CLI cannot combine `--project` with explicit
422
- * root files (TS5042), so expressing 'project options + entry-only roots' in one `tsc`
423
- * call requires a generated tsconfig on disk. Writing beside the user's config is
424
- * invasive; a temp config elsewhere often breaks `node_modules` / `@types` resolution
425
- * relative to the real project. The API lets us reuse `parseJsonConfigFileContent` (same
426
- * options as `-p`) with explicit `rootNames`, no files written, and a compiler host whose
427
- * current directory stays `workPath`.
428
- *
429
- * The `typescript` package is resolved with `require` from the user's app (peer dependency), not bundled.
430
- */
431
- const require_ = createRequire(import.meta.url);
432
- const typescript = (args) => {
433
- const { span } = args;
434
- return span.child("vc.builder.backends.tsCompile").trace(async () => {
435
- const extension = extname(args.entrypoint);
436
- if (![
437
- ".ts",
438
- ".mts",
439
- ".cts"
440
- ].includes(extension)) return;
441
- const ts = resolveTypeScriptModule(args.workPath);
442
- if (!ts) {
443
- console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck skipped ${Colors.gray("(TypeScript not found)")}`));
444
- return null;
445
- }
446
- return doTypeCheck(args, ts);
447
- });
448
- };
449
- async function doTypeCheck(args, ts) {
450
- const entryAbsolute = resolve(args.workPath, args.entrypoint);
451
- const tsconfig = await findNearestTsconfig(args.workPath);
452
- const formatDiagnostics = process.stdout.isTTY ? ts.formatDiagnosticsWithColorAndContext : ts.formatDiagnostics;
453
- const diagnosticHost = {
454
- getNewLine: () => ts.sys.newLine,
455
- getCanonicalFileName: (fileName) => ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
456
- getCurrentDirectory: () => args.workPath
457
- };
458
- let options;
459
- let parseDiagnostics = [];
460
- if (tsconfig) {
461
- const configRead = ts.readConfigFile(tsconfig, ts.sys.readFile);
462
- if (configRead.error) {
463
- const message = formatDiagnostics([configRead.error], diagnosticHost);
464
- console.error("\nTypeScript type check failed:\n");
465
- console.error(message);
466
- throw new Error("TypeScript type check failed");
215
+ //#region src/service-vc-init.ts
216
+ async function applyServiceVcInit(args) {
217
+ const { format, extension } = await resolveShimFormat$1(args);
218
+ const handlerDir = dirname(args.handler);
219
+ const vcInitName = `${basename(args.handler, extname(args.handler))}.__vc_service_vc_init${extension}`;
220
+ const vcInitHandler = handlerDir === "." ? vcInitName : join(handlerDir, vcInitName);
221
+ const handlerImportPath = `./${basename(args.handler)}`;
222
+ const vcInitSource = format === "esm" ? createEsmServiceVcInit(handlerImportPath) : createCjsServiceVcInit(handlerImportPath);
223
+ return {
224
+ handler: vcInitHandler,
225
+ files: {
226
+ ...args.files,
227
+ [vcInitHandler]: new FileBlob({
228
+ data: vcInitSource,
229
+ mode: 420
230
+ })
467
231
  }
468
- const parsed = ts.parseJsonConfigFileContent(configRead.config, ts.sys, dirname(tsconfig), void 0, tsconfig);
469
- parseDiagnostics = parsed.errors;
470
- options = {
471
- ...parsed.options,
472
- noEmit: true,
473
- skipLibCheck: true,
474
- allowJs: true,
475
- esModuleInterop: true
476
- };
477
- } else options = {
478
- noEmit: true,
479
- skipLibCheck: true,
480
- allowJs: true,
481
- esModuleInterop: true,
482
- target: ts.ScriptTarget.ES2022,
483
- module: ts.ModuleKind.NodeNext,
484
- moduleResolution: ts.ModuleResolutionKind.NodeNext
485
232
  };
486
- const compilerHost = ts.createCompilerHost(options);
487
- compilerHost.getCurrentDirectory = () => args.workPath;
488
- const program = ts.createProgram([entryAbsolute], options, compilerHost);
489
- const errors = [...parseDiagnostics, ...ts.getPreEmitDiagnostics(program)].filter((d) => d.category === ts.DiagnosticCategory.Error);
490
- if (errors.length === 0) {
491
- console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck complete`));
492
- return;
493
- }
494
- const output = formatDiagnostics(errors, diagnosticHost);
495
- console.error("\nTypeScript type check failed:\n");
496
- console.error(output);
497
- throw new Error("TypeScript type check failed");
498
233
  }
499
- function resolveTypeScriptModule(workPath) {
500
- try {
501
- return require_(require_.resolve("typescript", { paths: [workPath] }));
502
- } catch (_e) {
503
- return null;
504
- }
234
+ async function resolveShimFormat$1(args) {
235
+ const { format } = await resolveEntrypointAndFormat({
236
+ entrypoint: args.handler,
237
+ workPath: args.workPath
238
+ });
239
+ return {
240
+ format,
241
+ extension: extname(args.handler) || (format === "esm" ? ".mjs" : ".cjs")
242
+ };
505
243
  }
506
- const findNearestTsconfig = async (workPath) => {
507
- const tsconfigPath = join(workPath, "tsconfig.json");
508
- if (existsSync(tsconfigPath)) return tsconfigPath;
509
- if (workPath === "/") return;
510
- return findNearestTsconfig(join(workPath, ".."));
511
- };
512
-
513
- //#endregion
514
- //#region src/find-entrypoint.ts
515
- const frameworks = [
516
- "express",
517
- "hono",
518
- "elysia",
519
- "fastify",
520
- "@nestjs/core",
521
- "h3"
522
- ];
523
- const entrypointFilenames = [
524
- "app",
525
- "index",
526
- "server",
527
- "main",
528
- "src/app",
529
- "src/index",
530
- "src/server",
531
- "src/main"
532
- ];
533
- const entrypointExtensions = [
534
- "js",
535
- "cjs",
536
- "mjs",
537
- "ts",
538
- "cts",
539
- "mts"
540
- ];
541
- const entrypoints = entrypointFilenames.flatMap((filename) => entrypointExtensions.map((extension) => `${filename}.${extension}`));
542
- const createFrameworkRegex = (framework) => new RegExp(`(?:from|require|import)\\s*(?:\\(\\s*)?["']${framework}["']\\s*(?:\\))?`, "g");
543
- const findEntrypoint = async (cwd) => {
544
- let packageJsonObject = null;
545
- try {
546
- const packageJson = await readFile(join(cwd, "package.json"), "utf-8");
547
- packageJsonObject = JSON.parse(packageJson);
548
- } catch (_) {}
549
- if (packageJsonObject) {
550
- const main = typeof packageJsonObject.main === "string" ? packageJsonObject.main.trim() : "";
551
- if (main) {
552
- const abs = resolve(cwd, main);
553
- const rel = relative(cwd, abs);
554
- if (!rel.startsWith("..") && rel !== "") try {
555
- await readFile(abs, "utf-8");
556
- return rel.split(sep).join("/");
557
- } catch {}
558
- }
559
- }
560
- let framework;
561
- if (packageJsonObject) framework = frameworks.find((framework$1) => packageJsonObject.dependencies?.[framework$1]);
562
- if (!framework) for (const entrypoint of entrypoints) {
563
- const entrypointPath = join(cwd, entrypoint);
564
- try {
565
- await readFile(entrypointPath, "utf-8");
566
- return entrypoint;
567
- } catch (_) {}
568
- }
569
- const regex = framework ? createFrameworkRegex(framework) : void 0;
570
- for (const entrypoint of entrypoints) {
571
- const entrypointPath = join(cwd, entrypoint);
572
- try {
573
- const content = await readFile(entrypointPath, "utf-8");
574
- if (regex) {
575
- if (regex.test(content)) return entrypoint;
576
- }
577
- } catch (_) {}
578
- }
579
- };
580
- const findEntrypointOrThrow = async (cwd) => {
581
- const entrypoint = await findEntrypoint(cwd);
582
- if (!entrypoint) throw new Error(`No entrypoint found in "${cwd}". Set package.json "main" to a server file, or add one of: ${entrypoints.join(", ")}`);
583
- return entrypoint;
584
- };
585
- /**
586
- * Normalized entrypoint detector for Node services. Wraps {@link findEntrypoint}
587
- * and returns the result in the shared {@link DetectedEntrypoint} shape consumed
588
- * by services auto-detection.
589
- */
590
- const detectEntrypoint = async ({ workPath }) => {
591
- const file = await findEntrypoint(workPath);
592
- if (!file) return null;
593
- return {
594
- kind: "file",
595
- entrypoint: file
596
- };
597
- };
598
-
599
- //#endregion
600
- //#region src/cervel/index.ts
601
- const require$2 = createRequire(import.meta.url);
602
- const getBuildSummary = async (outputDir) => {
603
- const buildSummary = await readFile$1(join(outputDir, ".cervel.json"), "utf-8");
604
- return JSON.parse(buildSummary);
605
- };
606
- const build$1 = async (args) => {
607
- const entrypoint = args.entrypoint || await findEntrypointOrThrow(args.workPath);
608
- const span = args.span ?? new Span({ name: "cervel-build" });
609
- const [, rolldownResult] = await Promise.all([typescript({
610
- entrypoint,
611
- workPath: args.workPath,
612
- span
613
- }), rolldown$1({
614
- entrypoint,
615
- workPath: args.workPath,
616
- repoRootPath: args.repoRootPath,
617
- out: args.out,
618
- span
619
- })]);
620
- await writeFile(join(args.workPath, args.out, ".cervel.json"), JSON.stringify({ handler: rolldownResult.result.handler }, null, 2));
621
- console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Build complete — Using ${Colors.bold(entrypoint)} as the root entrypoint.`));
622
- return { rolldownResult: rolldownResult.result };
623
- };
624
- const serve = async (args) => {
625
- const entrypoint = await findEntrypointOrThrow(args.workPath);
626
- const srvxBin = join(require$2.resolve("srvx"), "..", "..", "..", "bin", "srvx.mjs");
627
- const tsxBin = require$2.resolve("tsx");
628
- const restArgs = Object.entries(args.rest).filter(([, value]) => value !== void 0 && value !== false).map(([key, value]) => typeof value === "boolean" ? `--${key}` : `--${key}=${value}`);
629
- if (!args.rest.import) restArgs.push("--import", tsxBin);
630
- await execa("npx", [
631
- srvxBin,
632
- ...restArgs,
633
- entrypoint
634
- ], {
635
- cwd: args.workPath,
636
- stdio: "inherit"
637
- });
638
- };
639
- const srvxOptions = {
640
- help: {
641
- type: "boolean",
642
- short: "h"
643
- },
644
- version: {
645
- type: "boolean",
646
- short: "v"
647
- },
648
- prod: { type: "boolean" },
649
- port: {
650
- type: "string",
651
- short: "p"
652
- },
653
- host: {
654
- type: "string",
655
- short: "H"
656
- },
657
- static: {
658
- type: "string",
659
- short: "s"
660
- },
661
- import: { type: "string" },
662
- tls: { type: "boolean" },
663
- cert: { type: "string" },
664
- key: { type: "string" }
665
- };
666
-
667
- //#endregion
668
- //#region src/rolldown/resolve-format.ts
669
- const resolveEntrypointAndFormat = async (args) => {
670
- const extension = extname(args.entrypoint);
671
- const extensionMap = {
672
- ".ts": {
673
- format: "auto",
674
- extension: "js"
675
- },
676
- ".mts": {
677
- format: "esm",
678
- extension: "mjs"
679
- },
680
- ".cts": {
681
- format: "cjs",
682
- extension: "cjs"
683
- },
684
- ".cjs": {
685
- format: "cjs",
686
- extension: "cjs"
687
- },
688
- ".js": {
689
- format: "auto",
690
- extension: "js"
691
- },
692
- ".mjs": {
693
- format: "esm",
694
- extension: "mjs"
695
- }
696
- };
697
- const extensionInfo = extensionMap[extension] || extensionMap[".js"];
698
- let resolvedFormat = extensionInfo.format === "auto" ? args.defaultFormat : extensionInfo.format;
699
- const packageJsonPath = join(args.workPath, "package.json");
700
- let pkg = {};
701
- if (existsSync(packageJsonPath)) {
702
- const source = await readFile(packageJsonPath, "utf8");
703
- try {
704
- pkg = JSON.parse(source.toString());
705
- } catch (_e) {
706
- pkg = {};
707
- }
708
- if (extensionInfo.format === "auto") if (pkg.type === "module") resolvedFormat = "esm";
709
- else resolvedFormat = "cjs";
710
- }
711
- if (!resolvedFormat) throw new Error(`Unable to resolve format for ${args.entrypoint}`);
712
- return {
713
- format: resolvedFormat,
714
- extension: resolvedFormat === "esm" ? "mjs" : "cjs"
715
- };
716
- };
717
-
718
- //#endregion
719
- //#region src/service-vc-init.ts
720
- async function applyServiceVcInit(args) {
721
- const { format, extension } = await resolveShimFormat$1(args);
722
- const handlerDir = dirname(args.handler);
723
- const vcInitName = `${basename(args.handler, extname(args.handler))}.__vc_service_vc_init${extension}`;
724
- const vcInitHandler = handlerDir === "." ? vcInitName : join(handlerDir, vcInitName);
725
- const handlerImportPath = `./${basename(args.handler)}`;
726
- const vcInitSource = format === "esm" ? createEsmServiceVcInit(handlerImportPath) : createCjsServiceVcInit(handlerImportPath);
727
- return {
728
- handler: vcInitHandler,
729
- files: {
730
- ...args.files,
731
- [vcInitHandler]: new FileBlob({
732
- data: vcInitSource,
733
- mode: 420
734
- })
735
- }
736
- };
737
- }
738
- async function resolveShimFormat$1(args) {
739
- const { format } = await resolveEntrypointAndFormat({
740
- entrypoint: args.handler,
741
- workPath: args.workPath
742
- });
743
- return {
744
- format,
745
- extension: extname(args.handler) || (format === "esm" ? ".mjs" : ".cjs")
746
- };
747
- }
748
- const sharedShimPrelude = String.raw`
749
- const PATCH_SYMBOL = Symbol.for('vc.service.route-prefix-strip.patch')
244
+ const sharedShimPrelude = String.raw`
245
+ const PATCH_SYMBOL = Symbol.for('vc.service.route-prefix-strip.patch')
750
246
 
751
247
  function normalizeServiceRoutePrefix(rawPrefix) {
752
248
  if (!rawPrefix) {
@@ -906,7 +402,7 @@ async function applyCronDispatch(args) {
906
402
  const handlerDir = posix.dirname(args.handler);
907
403
  const dispatchName = `${posix.basename(args.handler, extname(args.handler))}.__vc_cron_dispatch${extension}`;
908
404
  const dispatchHandler = handlerDir === "." ? dispatchName : posix.join(handlerDir, dispatchName);
909
- const handlerImportPath = `./${posix.basename(args.handler)}`;
405
+ const handlerImportPath = args.modulePathOverride ?? `./${posix.basename(args.handler)}`;
910
406
  const routesJson = JSON.stringify(args.routes);
911
407
  if (routesJson.includes("\\") || routesJson.includes("'")) throw new Error(`cron route table contains characters that need JS-string escaping: ${routesJson}`);
912
408
  const dispatchSource = (format === "esm" ? ESM_TEMPLATE : CJS_TEMPLATE).replace(USER_MODULE_PLACEHOLDER, JSON.stringify(handlerImportPath)).replace(ROUTES_PLACEHOLDER, `'${routesJson}'`);
@@ -962,6 +458,515 @@ async function getServiceCrons(opts) {
962
458
  }];
963
459
  }
964
460
 
461
+ //#endregion
462
+ //#region src/cervel/plugin.ts
463
+ const CJS_SHIM_PREFIX$1 = "\0cjs-shim:";
464
+ const plugin = (args) => {
465
+ const packageJsonCache = /* @__PURE__ */ new Map();
466
+ const shimMeta = /* @__PURE__ */ new Map();
467
+ const { tracedPaths } = args.context;
468
+ const isBareImport = (id) => {
469
+ return !id.startsWith(".") && !id.startsWith("/") && !/^[a-z][a-z0-9+.-]*:/i.test(id);
470
+ };
471
+ /**
472
+ * Read and cache package.json contents
473
+ */
474
+ const getPackageJson = async (pkgPath) => {
475
+ if (packageJsonCache.has(pkgPath)) return packageJsonCache.get(pkgPath);
476
+ try {
477
+ const contents = await readFile(pkgPath, "utf-8");
478
+ const parsed = JSON.parse(contents);
479
+ packageJsonCache.set(pkgPath, parsed);
480
+ return parsed;
481
+ } catch {
482
+ packageJsonCache.set(pkgPath, null);
483
+ return null;
484
+ }
485
+ };
486
+ /**
487
+ * Determine if a resolved module is CommonJS based on package.json exports
488
+ */
489
+ const isCommonJS = async (bareImport, resolvedPath, resolvedInfo) => {
490
+ const ext = extname(resolvedPath);
491
+ if (ext === ".cjs") return true;
492
+ if (ext === ".mjs") return false;
493
+ if (ext === ".js" || ext === ".ts") {
494
+ const pkgJsonPath = resolvedInfo.packageJsonPath;
495
+ if (!pkgJsonPath) return true;
496
+ const pkgJson = await getPackageJson(pkgJsonPath);
497
+ if (!pkgJson) return true;
498
+ const pkgDir = dirname(pkgJsonPath);
499
+ const relativePath = resolvedPath.startsWith(pkgDir) ? resolvedPath.slice(pkgDir.length + 1).replace(/\\/g, "/") : null;
500
+ if (!relativePath) return pkgJson.type !== "module";
501
+ const pkgName = pkgJson.name || "";
502
+ const subpath = bareImport.startsWith(pkgName) ? `.${bareImport.slice(pkgName.length)}` || "." : ".";
503
+ try {
504
+ if (exports(pkgJson, subpath, {
505
+ require: false,
506
+ conditions: ["node", "import"]
507
+ })?.some((p) => p === relativePath || p === `./${relativePath}`)) return false;
508
+ if (exports(pkgJson, subpath, {
509
+ require: true,
510
+ conditions: ["node", "require"]
511
+ })?.some((p) => p === relativePath || p === `./${relativePath}`)) return true;
512
+ } catch (err) {
513
+ console.warn("Export resolution failed::", err);
514
+ }
515
+ if (pkgJson.module) return false;
516
+ return pkgJson.type !== "module";
517
+ }
518
+ return true;
519
+ };
520
+ const isLocalImport = (id) => {
521
+ if (id.startsWith("node:")) return false;
522
+ if (id.includes("node_modules")) return false;
523
+ return true;
524
+ };
525
+ return {
526
+ name: "cervel",
527
+ resolveId: {
528
+ order: "pre",
529
+ async handler(id, importer, rOpts) {
530
+ if (id.startsWith(CJS_SHIM_PREFIX$1)) return {
531
+ id,
532
+ external: false
533
+ };
534
+ const resolved = await this.resolve(id, importer, rOpts);
535
+ if (builtinModules.includes(id)) return {
536
+ id: `node:${id}`,
537
+ external: true
538
+ };
539
+ if (resolved?.id && isLocalImport(resolved.id)) tracedPaths.add(resolved.id);
540
+ if (importer?.startsWith(CJS_SHIM_PREFIX$1) && isBareImport(id)) return {
541
+ id,
542
+ external: true
543
+ };
544
+ if (importer && isBareImport(id) && resolved?.id?.includes("node_modules")) {
545
+ if (args.shimBareImports) {
546
+ if (await isCommonJS(id, resolved.id, resolved)) {
547
+ const importerPkgJsonPath = (await this.resolve(importer))?.packageJsonPath;
548
+ if (importerPkgJsonPath) {
549
+ const importerPkgDir = relative(args.repoRootPath, dirname(importerPkgJsonPath));
550
+ const shimId$1 = `${CJS_SHIM_PREFIX$1}${importerPkgDir.replace(/\//g, "_")}_${id.replace(/\//g, "_")}`;
551
+ shimMeta.set(shimId$1, {
552
+ pkgDir: importerPkgDir,
553
+ pkgName: id
554
+ });
555
+ return {
556
+ id: shimId$1,
557
+ external: false
558
+ };
559
+ }
560
+ const shimId = `${CJS_SHIM_PREFIX$1}${id.replace(/\//g, "_")}`;
561
+ shimMeta.set(shimId, {
562
+ pkgDir: "",
563
+ pkgName: id
564
+ });
565
+ return {
566
+ id: shimId,
567
+ external: false
568
+ };
569
+ }
570
+ }
571
+ return {
572
+ external: true,
573
+ id
574
+ };
575
+ }
576
+ if (importer && isBareImport(id)) return resolved;
577
+ return {
578
+ external: true,
579
+ ...resolved,
580
+ id: resolved?.id || id
581
+ };
582
+ }
583
+ },
584
+ load: { async handler(id) {
585
+ if (id.startsWith(CJS_SHIM_PREFIX$1)) {
586
+ const meta = shimMeta.get(id);
587
+ if (!meta) return { code: `module.exports = require('${id.slice(10)}');` };
588
+ const { pkgDir, pkgName } = meta;
589
+ if (pkgDir) return { code: `
590
+ import { createRequire } from 'node:module';
591
+ import { fileURLToPath } from 'node:url';
592
+ import { dirname, join } from 'node:path';
593
+
594
+ const requireFromContext = createRequire(join(dirname(fileURLToPath(import.meta.url)), '${join("..", pkgDir, "package.json")}'));
595
+ module.exports = requireFromContext('${pkgName}');
596
+ `.trim() };
597
+ return { code: `module.exports = require('${pkgName}');` };
598
+ }
599
+ return null;
600
+ } }
601
+ };
602
+ };
603
+
604
+ //#endregion
605
+ //#region src/cervel/node-file-trace.ts
606
+ const nodeFileTrace = async (args) => {
607
+ const { span } = args;
608
+ const files = {};
609
+ const { tracedPaths } = args;
610
+ const compiledSourceFiles = await glob("**/*", {
611
+ cwd: args.outDir,
612
+ follow: true,
613
+ includeDirectories: true
614
+ });
615
+ for (const file of Object.keys(compiledSourceFiles)) files[file] = compiledSourceFiles[file];
616
+ /**
617
+ * While we're not using NFT to process source code, we are using it
618
+ * to tree shake node deps, and include any fs reads for files that are
619
+ * not part of the traced paths or compiled source files.
620
+ * Most of this is identical to the `@vercel/node` implementation
621
+ */
622
+ const runNft = () => nodeFileTrace$1(Array.from(tracedPaths), {
623
+ base: args.repoRootPath,
624
+ processCwd: args.workPath,
625
+ ts: true,
626
+ mixedModules: true,
627
+ async resolve(id, parent, job, cjsResolve) {
628
+ return resolve$1(id, parent, job, cjsResolve);
629
+ },
630
+ async readFile(fsPath) {
631
+ try {
632
+ let source = await readFile(fsPath);
633
+ if (fsPath.endsWith(".ts") && !fsPath.endsWith(".d.ts") || fsPath.endsWith(".tsx") || fsPath.endsWith(".mts") || fsPath.endsWith(".cts")) source = (await transform(fsPath, source.toString())).code;
634
+ return source;
635
+ } catch (error) {
636
+ if (isNativeError(error) && "code" in error && (error.code === "ENOENT" || error.code === "EISDIR")) return null;
637
+ throw error;
638
+ }
639
+ }
640
+ });
641
+ const result = await span.child("vc.builder.backends.nft").trace(runNft);
642
+ if (!args.keepTracedPaths) for (const file of tracedPaths) {
643
+ const relativeFile = relative(args.repoRootPath, file);
644
+ result.fileList.delete(relativeFile);
645
+ }
646
+ debug("NFT traced files count:", result.fileList.size);
647
+ for (const file of result.fileList) {
648
+ const absolutePath = join(args.repoRootPath, file);
649
+ const stats = await lstat(absolutePath);
650
+ const outputPath = file;
651
+ if (stats.isSymbolicLink() || stats.isFile()) files[outputPath] = new FileFsRef({
652
+ fsPath: absolutePath,
653
+ mode: stats.mode
654
+ });
655
+ }
656
+ debug("Total files in context:", Object.keys(files).length);
657
+ return files;
658
+ };
659
+
660
+ //#endregion
661
+ //#region src/cervel/rolldown.ts
662
+ const __dirname__filenameShim = `
663
+ import { createRequire as __createRequire } from 'node:module';
664
+ import { fileURLToPath as __fileURLToPath } from 'node:url';
665
+ import { dirname as __dirname_ } from 'node:path';
666
+ var require = typeof require !== 'undefined' ? require : __createRequire(import.meta.url);
667
+ var __filename = typeof __filename !== 'undefined' ? __filename : __fileURLToPath(import.meta.url);
668
+ var __dirname = typeof __dirname !== 'undefined' ? __dirname : __dirname_(__filename);
669
+ `.trim();
670
+ const rolldown$1 = async (args) => {
671
+ const entrypointPath = join(args.workPath, args.entrypoint);
672
+ const outputDir = join(args.workPath, args.out);
673
+ const extension = extname(args.entrypoint);
674
+ const extensionMap = {
675
+ ".ts": {
676
+ format: "auto",
677
+ extension: "js"
678
+ },
679
+ ".mts": {
680
+ format: "esm",
681
+ extension: "mjs"
682
+ },
683
+ ".cts": {
684
+ format: "cjs",
685
+ extension: "cjs"
686
+ },
687
+ ".cjs": {
688
+ format: "cjs",
689
+ extension: "cjs"
690
+ },
691
+ ".js": {
692
+ format: "auto",
693
+ extension: "js"
694
+ },
695
+ ".mjs": {
696
+ format: "esm",
697
+ extension: "mjs"
698
+ }
699
+ };
700
+ const extensionInfo = extensionMap[extension] || extensionMap[".js"];
701
+ let resolvedFormat = extensionInfo.format === "auto" ? void 0 : extensionInfo.format;
702
+ const packageJsonPath = join(args.workPath, "package.json");
703
+ const external = [];
704
+ let pkg = {};
705
+ if (existsSync(packageJsonPath)) {
706
+ const source = await readFile(packageJsonPath, "utf8");
707
+ try {
708
+ pkg = JSON.parse(source.toString());
709
+ } catch (_e) {
710
+ pkg = {};
711
+ }
712
+ if (extensionInfo.format === "auto") if (pkg.type === "module") resolvedFormat = "esm";
713
+ else resolvedFormat = "cjs";
714
+ for (const dependency of Object.keys(pkg.dependencies || {})) external.push(dependency);
715
+ for (const dependency of Object.keys(pkg.devDependencies || {})) external.push(dependency);
716
+ for (const dependency of Object.keys(pkg.peerDependencies || {})) external.push(dependency);
717
+ for (const dependency of Object.keys(pkg.optionalDependencies || {})) external.push(dependency);
718
+ }
719
+ const resolvedExtension = resolvedFormat === "esm" ? "mjs" : "cjs";
720
+ const context = { tracedPaths: /* @__PURE__ */ new Set() };
721
+ const runRolldown = () => build$2({
722
+ input: entrypointPath,
723
+ cwd: args.workPath,
724
+ platform: "node",
725
+ tsconfig: true,
726
+ plugins: [plugin({
727
+ repoRootPath: args.repoRootPath,
728
+ outDir: outputDir,
729
+ workPath: args.workPath,
730
+ shimBareImports: resolvedFormat === "esm",
731
+ context
732
+ })],
733
+ output: {
734
+ cleanDir: true,
735
+ dir: outputDir,
736
+ format: resolvedFormat,
737
+ entryFileNames: `[name].${resolvedExtension}`,
738
+ preserveModules: true,
739
+ preserveModulesRoot: args.repoRootPath,
740
+ sourcemap: false,
741
+ banner: resolvedFormat === "esm" ? __dirname__filenameShim : void 0
742
+ }
743
+ });
744
+ const out = await args.span.child("vc.builder.backends.rolldown").trace(runRolldown);
745
+ let handler = null;
746
+ for (const entry of out.output) if (entry.type === "chunk") {
747
+ if (entry.isEntry) handler = entry.fileName;
748
+ }
749
+ if (typeof handler !== "string") throw new Error(`Unable to resolve module for ${args.entrypoint}`);
750
+ const outputFiles = await nodeFileTrace({
751
+ outDir: outputDir,
752
+ tracedPaths: Array.from(context.tracedPaths),
753
+ repoRootPath: args.repoRootPath,
754
+ workPath: args.workPath,
755
+ keepTracedPaths: false,
756
+ span: args.span
757
+ });
758
+ const cleanup = async () => {
759
+ await rm(outputDir, {
760
+ recursive: true,
761
+ force: true
762
+ });
763
+ };
764
+ return {
765
+ result: {
766
+ handler,
767
+ outputDir,
768
+ outputFiles
769
+ },
770
+ cleanup
771
+ };
772
+ };
773
+
774
+ //#endregion
775
+ //#region src/cervel/utils.ts
776
+ const noColor = globalThis.process?.env?.NO_COLOR === "1" || globalThis.process?.env?.TERM === "dumb";
777
+ const resets = {
778
+ 1: 22,
779
+ 31: 39,
780
+ 32: 39,
781
+ 33: 39,
782
+ 34: 39,
783
+ 35: 39,
784
+ 36: 39,
785
+ 90: 39
786
+ };
787
+ const _c = (c) => (text) => {
788
+ if (noColor) return text;
789
+ return `\u001B[${c}m${text}\u001B[${resets[c] ?? 0}m`;
790
+ };
791
+ const Colors = {
792
+ bold: _c(1),
793
+ red: _c(31),
794
+ green: _c(32),
795
+ yellow: _c(33),
796
+ blue: _c(34),
797
+ magenta: _c(35),
798
+ cyan: _c(36),
799
+ gray: _c(90),
800
+ url: (title, url) => noColor ? `[${title}](${url})` : `\u001B]8;;${url}\u001B\\${title}\u001B]8;;\u001B\\`
801
+ };
802
+
803
+ //#endregion
804
+ //#region src/typescript.ts
805
+ /**
806
+ * Typecheck via the TypeScript compiler API (`createProgram`, `getPreEmitDiagnostics`),
807
+ * not by spawning the `tsc` binary.
808
+ *
809
+ * We only want to validate the deployment entrypoint and its import graph, not every
810
+ * file matched by `tsconfig` `include`. The CLI cannot combine `--project` with explicit
811
+ * root files (TS5042), so expressing 'project options + entry-only roots' in one `tsc`
812
+ * call requires a generated tsconfig on disk. Writing beside the user's config is
813
+ * invasive; a temp config elsewhere often breaks `node_modules` / `@types` resolution
814
+ * relative to the real project. The API lets us reuse `parseJsonConfigFileContent` (same
815
+ * options as `-p`) with explicit `rootNames`, no files written, and a compiler host whose
816
+ * current directory stays `workPath`.
817
+ *
818
+ * The `typescript` package is resolved with `require` from the user's app (peer dependency), not bundled.
819
+ */
820
+ const require_ = createRequire(import.meta.url);
821
+ const typescript = (args) => {
822
+ const { span } = args;
823
+ return span.child("vc.builder.backends.tsCompile").trace(async () => {
824
+ const extension = extname(args.entrypoint);
825
+ if (![
826
+ ".ts",
827
+ ".mts",
828
+ ".cts"
829
+ ].includes(extension)) return;
830
+ const ts = resolveTypeScriptModule(args.workPath);
831
+ if (!ts) {
832
+ console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck skipped ${Colors.gray("(TypeScript not found)")}`));
833
+ return null;
834
+ }
835
+ return doTypeCheck(args, ts);
836
+ });
837
+ };
838
+ async function doTypeCheck(args, ts) {
839
+ const entryAbsolute = resolve(args.workPath, args.entrypoint);
840
+ const tsconfig = await findNearestTsconfig(args.workPath);
841
+ const formatDiagnostics = process.stdout.isTTY ? ts.formatDiagnosticsWithColorAndContext : ts.formatDiagnostics;
842
+ const diagnosticHost = {
843
+ getNewLine: () => ts.sys.newLine,
844
+ getCanonicalFileName: (fileName) => ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
845
+ getCurrentDirectory: () => args.workPath
846
+ };
847
+ let options;
848
+ let parseDiagnostics = [];
849
+ if (tsconfig) {
850
+ const configRead = ts.readConfigFile(tsconfig, ts.sys.readFile);
851
+ if (configRead.error) {
852
+ const message = formatDiagnostics([configRead.error], diagnosticHost);
853
+ console.error("\nTypeScript type check failed:\n");
854
+ console.error(message);
855
+ throw new Error("TypeScript type check failed");
856
+ }
857
+ const parsed = ts.parseJsonConfigFileContent(configRead.config, ts.sys, dirname(tsconfig), void 0, tsconfig);
858
+ parseDiagnostics = parsed.errors;
859
+ options = {
860
+ ...parsed.options,
861
+ noEmit: true,
862
+ skipLibCheck: true,
863
+ allowJs: true,
864
+ esModuleInterop: true
865
+ };
866
+ } else options = {
867
+ noEmit: true,
868
+ skipLibCheck: true,
869
+ allowJs: true,
870
+ esModuleInterop: true,
871
+ target: ts.ScriptTarget.ES2022,
872
+ module: ts.ModuleKind.NodeNext,
873
+ moduleResolution: ts.ModuleResolutionKind.NodeNext
874
+ };
875
+ const compilerHost = ts.createCompilerHost(options);
876
+ compilerHost.getCurrentDirectory = () => args.workPath;
877
+ const program = ts.createProgram([entryAbsolute], options, compilerHost);
878
+ const errors = [...parseDiagnostics, ...ts.getPreEmitDiagnostics(program)].filter((d) => d.category === ts.DiagnosticCategory.Error);
879
+ if (errors.length === 0) {
880
+ console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck complete`));
881
+ return;
882
+ }
883
+ const output = formatDiagnostics(errors, diagnosticHost);
884
+ console.error("\nTypeScript type check failed:\n");
885
+ console.error(output);
886
+ throw new Error("TypeScript type check failed");
887
+ }
888
+ function resolveTypeScriptModule(workPath) {
889
+ try {
890
+ return require_(require_.resolve("typescript", { paths: [workPath] }));
891
+ } catch (_e) {
892
+ return null;
893
+ }
894
+ }
895
+ const findNearestTsconfig = async (workPath) => {
896
+ const tsconfigPath = join(workPath, "tsconfig.json");
897
+ if (existsSync(tsconfigPath)) return tsconfigPath;
898
+ if (workPath === "/") return;
899
+ return findNearestTsconfig(join(workPath, ".."));
900
+ };
901
+
902
+ //#endregion
903
+ //#region src/cervel/index.ts
904
+ const require$2 = createRequire(import.meta.url);
905
+ const getBuildSummary = async (outputDir) => {
906
+ const buildSummary = await readFile$1(join(outputDir, ".cervel.json"), "utf-8");
907
+ return JSON.parse(buildSummary);
908
+ };
909
+ const build$1 = async (args) => {
910
+ const entrypoint = args.entrypoint || await findEntrypointOrThrow(args.workPath);
911
+ const span = args.span ?? new Span({ name: "cervel-build" });
912
+ const [, rolldownResult] = await Promise.all([typescript({
913
+ entrypoint,
914
+ workPath: args.workPath,
915
+ span
916
+ }), rolldown$1({
917
+ entrypoint,
918
+ workPath: args.workPath,
919
+ repoRootPath: args.repoRootPath,
920
+ out: args.out,
921
+ span
922
+ })]);
923
+ await writeFile(join(args.workPath, args.out, ".cervel.json"), JSON.stringify({ handler: rolldownResult.result.handler }, null, 2));
924
+ console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Build complete — Using ${Colors.bold(entrypoint)} as the root entrypoint.`));
925
+ return { rolldownResult: rolldownResult.result };
926
+ };
927
+ const serve = async (args) => {
928
+ const entrypoint = await findEntrypointOrThrow(args.workPath);
929
+ const srvxBin = join(require$2.resolve("srvx"), "..", "..", "..", "bin", "srvx.mjs");
930
+ const tsxBin = require$2.resolve("tsx");
931
+ const restArgs = Object.entries(args.rest).filter(([, value]) => value !== void 0 && value !== false).map(([key, value]) => typeof value === "boolean" ? `--${key}` : `--${key}=${value}`);
932
+ if (!args.rest.import) restArgs.push("--import", tsxBin);
933
+ await execa("npx", [
934
+ srvxBin,
935
+ ...restArgs,
936
+ entrypoint
937
+ ], {
938
+ cwd: args.workPath,
939
+ stdio: "inherit"
940
+ });
941
+ };
942
+ const srvxOptions = {
943
+ help: {
944
+ type: "boolean",
945
+ short: "h"
946
+ },
947
+ version: {
948
+ type: "boolean",
949
+ short: "v"
950
+ },
951
+ prod: { type: "boolean" },
952
+ port: {
953
+ type: "string",
954
+ short: "p"
955
+ },
956
+ host: {
957
+ type: "string",
958
+ short: "H"
959
+ },
960
+ static: {
961
+ type: "string",
962
+ short: "s"
963
+ },
964
+ import: { type: "string" },
965
+ tls: { type: "boolean" },
966
+ cert: { type: "string" },
967
+ key: { type: "string" }
968
+ };
969
+
965
970
  //#endregion
966
971
  //#region src/rolldown/nft.ts
967
972
  const nft = async (args) => {
@@ -1669,8 +1674,7 @@ const build = async (args) => {
1669
1674
  span.setAttributes({ "builder.name": builderName });
1670
1675
  const buildSpan = span.child("vc.builder.backends.build");
1671
1676
  return buildSpan.trace(async () => {
1672
- const explicit = args.entrypoint && args.entrypoint !== "package.json" ? args.entrypoint : null;
1673
- const entrypoint = explicit && existsSync(join(args.workPath, explicit)) ? explicit : await findEntrypointOrThrow(args.workPath);
1677
+ const entrypoint = await findEntrypointWithHintOrThrow(args.workPath, args.entrypoint);
1674
1678
  debug("Entrypoint", entrypoint);
1675
1679
  args.entrypoint = entrypoint;
1676
1680
  const cronEntries = await getServiceCrons({