363 lines
No EOL
9.7 KiB
JavaScript
363 lines
No EOL
9.7 KiB
JavaScript
import { createRequire } from "module";
|
|
import { existsSync } from "fs";
|
|
import { readFile, rm, writeFile } from "fs/promises";
|
|
import { extname, join } from "path";
|
|
import { build as build$1 } from "rolldown";
|
|
import { spawn } from "child_process";
|
|
import execa from "execa";
|
|
|
|
//#region src/rolldown.ts
|
|
/**
|
|
* Escapes special regex characters in a string to treat it as a literal pattern.
|
|
*/
|
|
function escapeRegExp(string) {
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
const rolldown = async (args) => {
|
|
const baseDir = args.repoRootPath || args.workPath;
|
|
const entrypointPath = join(args.workPath, args.entrypoint);
|
|
const shouldAddSourcemapSupport = false;
|
|
const extension = extname(args.entrypoint);
|
|
const extensionMap = {
|
|
".ts": {
|
|
format: "auto",
|
|
extension: "js"
|
|
},
|
|
".mts": {
|
|
format: "esm",
|
|
extension: "mjs"
|
|
},
|
|
".cts": {
|
|
format: "cjs",
|
|
extension: "cjs"
|
|
},
|
|
".cjs": {
|
|
format: "cjs",
|
|
extension: "cjs"
|
|
},
|
|
".js": {
|
|
format: "auto",
|
|
extension: "js"
|
|
},
|
|
".mjs": {
|
|
format: "esm",
|
|
extension: "mjs"
|
|
}
|
|
};
|
|
const extensionInfo = extensionMap[extension] || extensionMap[".js"];
|
|
let resolvedFormat = extensionInfo.format === "auto" ? void 0 : extensionInfo.format;
|
|
const resolvedExtension = extensionInfo.extension;
|
|
const packageJsonPath = join(args.workPath, "package.json");
|
|
const external = [];
|
|
let pkg = {};
|
|
if (existsSync(packageJsonPath)) {
|
|
const source = await readFile(packageJsonPath, "utf8");
|
|
try {
|
|
pkg = JSON.parse(source.toString());
|
|
} catch (_e) {
|
|
pkg = {};
|
|
}
|
|
if (extensionInfo.format === "auto") if (pkg.type === "module") resolvedFormat = "esm";
|
|
else resolvedFormat = "cjs";
|
|
for (const dependency of Object.keys(pkg.dependencies || {})) external.push(dependency);
|
|
for (const dependency of Object.keys(pkg.devDependencies || {})) external.push(dependency);
|
|
for (const dependency of Object.keys(pkg.peerDependencies || {})) external.push(dependency);
|
|
for (const dependency of Object.keys(pkg.optionalDependencies || {})) external.push(dependency);
|
|
}
|
|
const relativeOutputDir = args.out;
|
|
const outputDir = join(baseDir, relativeOutputDir);
|
|
const out = await build$1({
|
|
input: entrypointPath,
|
|
cwd: baseDir,
|
|
platform: "node",
|
|
tsconfig: true,
|
|
external: external.map((pkg$1) => /* @__PURE__ */ new RegExp(`^${escapeRegExp(pkg$1)}`)),
|
|
output: {
|
|
cleanDir: true,
|
|
dir: outputDir,
|
|
format: resolvedFormat,
|
|
entryFileNames: `[name].${resolvedExtension}`,
|
|
preserveModules: true,
|
|
sourcemap: false
|
|
}
|
|
});
|
|
let handler = null;
|
|
for (const entry of out.output) if (entry.type === "chunk") {
|
|
if (entry.isEntry) handler = entry.fileName;
|
|
}
|
|
if (typeof handler !== "string") throw new Error(`Unable to resolve module for ${args.entrypoint}`);
|
|
const cleanup = async () => {
|
|
await rm(outputDir, {
|
|
recursive: true,
|
|
force: true
|
|
});
|
|
};
|
|
return {
|
|
result: {
|
|
pkg,
|
|
shouldAddSourcemapSupport,
|
|
handler,
|
|
outputDir
|
|
},
|
|
cleanup
|
|
};
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/utils.ts
|
|
const noColor = globalThis.process?.env?.NO_COLOR === "1" || globalThis.process?.env?.TERM === "dumb";
|
|
const resets = {
|
|
1: 22,
|
|
31: 39,
|
|
32: 39,
|
|
33: 39,
|
|
34: 39,
|
|
35: 39,
|
|
36: 39,
|
|
90: 39
|
|
};
|
|
const _c = (c) => (text) => {
|
|
if (noColor) return text;
|
|
return `\u001B[${c}m${text}\u001B[${resets[c] ?? 0}m`;
|
|
};
|
|
const Colors = {
|
|
bold: _c(1),
|
|
red: _c(31),
|
|
green: _c(32),
|
|
yellow: _c(33),
|
|
blue: _c(34),
|
|
magenta: _c(35),
|
|
cyan: _c(36),
|
|
gray: _c(90),
|
|
url: (title, url) => noColor ? `[${title}](${url})` : `\u001B]8;;${url}\u001B\\${title}\u001B]8;;\u001B\\`
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/typescript.ts
|
|
const require_ = createRequire(import.meta.url);
|
|
const typescript = (args) => {
|
|
const extension = extname(args.entrypoint);
|
|
if (![
|
|
".ts",
|
|
".mts",
|
|
".cts"
|
|
].includes(extension)) return;
|
|
const tscPath = resolveTscPath(args);
|
|
if (!tscPath) {
|
|
console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck skipped ${Colors.gray("(TypeScript not found)")}`));
|
|
return null;
|
|
}
|
|
return doTypeCheck(args, tscPath);
|
|
};
|
|
async function doTypeCheck(args, tscPath) {
|
|
let stdout = "";
|
|
let stderr = "";
|
|
/**
|
|
* This might be subject to change.
|
|
* - if no tscPath, skip typecheck
|
|
* - if tsconfig, provide the tsconfig path
|
|
* - else provide the entrypoint path
|
|
*/
|
|
const tscArgs = [
|
|
tscPath,
|
|
"--noEmit",
|
|
"--pretty",
|
|
"--allowJs",
|
|
"--esModuleInterop",
|
|
"--skipLibCheck"
|
|
];
|
|
const tsconfig = await findNearestTsconfig(args.workPath);
|
|
if (tsconfig) tscArgs.push("--project", tsconfig);
|
|
else tscArgs.push(args.entrypoint);
|
|
const child = spawn(process.execPath, tscArgs, {
|
|
cwd: args.workPath,
|
|
stdio: [
|
|
"ignore",
|
|
"pipe",
|
|
"pipe"
|
|
]
|
|
});
|
|
child.stdout?.on("data", (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
child.stderr?.on("data", (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
await new Promise((resolve, reject) => {
|
|
child.on("close", (code) => {
|
|
if (code === 0) {
|
|
console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Typecheck complete`));
|
|
resolve();
|
|
} else {
|
|
const output = stdout || stderr;
|
|
if (output) {
|
|
console.error("\nTypeScript type check failed:\n");
|
|
console.error(output);
|
|
}
|
|
reject(/* @__PURE__ */ new Error("TypeScript type check failed"));
|
|
}
|
|
});
|
|
child.on("error", (err) => {
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
const resolveTscPath = (args) => {
|
|
try {
|
|
return require_.resolve("typescript/bin/tsc", { paths: [args.workPath] });
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
};
|
|
const findNearestTsconfig = async (workPath) => {
|
|
const tsconfigPath = join(workPath, "tsconfig.json");
|
|
if (existsSync(tsconfigPath)) return tsconfigPath;
|
|
if (workPath === "/") return;
|
|
return findNearestTsconfig(join(workPath, ".."));
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/find-entrypoint.ts
|
|
const frameworks = [
|
|
"express",
|
|
"hono",
|
|
"elysia",
|
|
"fastify",
|
|
"@nestjs/core",
|
|
"h3"
|
|
];
|
|
const entrypointFilenames = [
|
|
"app",
|
|
"index",
|
|
"server",
|
|
"main"
|
|
];
|
|
const entrypointExtensions = [
|
|
"js",
|
|
"cjs",
|
|
"mjs",
|
|
"ts",
|
|
"cts",
|
|
"mts"
|
|
];
|
|
const entrypoints = entrypointFilenames.flatMap((filename) => entrypointExtensions.map((extension) => `${filename}.${extension}`));
|
|
const createFrameworkRegex = (framework) => new RegExp(`(?:from|require|import)\\s*(?:\\(\\s*)?["']${framework}["']\\s*(?:\\))?`, "g");
|
|
const findEntrypoint = async (cwd, options) => {
|
|
if (options?.ignoreRegex ?? false) {
|
|
for (const entrypoint of entrypoints) if (existsSync(join(cwd, entrypoint))) return entrypoint;
|
|
for (const entrypoint of entrypoints) if (existsSync(join(cwd, "src", entrypoint))) return join("src", entrypoint);
|
|
throw new Error("No entrypoint file found");
|
|
}
|
|
const packageJson = await readFile(join(cwd, "package.json"), "utf-8");
|
|
const packageJsonObject = JSON.parse(packageJson);
|
|
const framework = frameworks.find((framework$1) => packageJsonObject.dependencies?.[framework$1]);
|
|
if (!framework) {
|
|
for (const entrypoint of entrypoints) {
|
|
const entrypointPath = join(cwd, entrypoint);
|
|
try {
|
|
await readFile(entrypointPath, "utf-8");
|
|
return entrypoint;
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
throw new Error("No entrypoint or framework found");
|
|
}
|
|
const regex = createFrameworkRegex(framework);
|
|
for (const entrypoint of entrypoints) {
|
|
const entrypointPath = join(cwd, entrypoint);
|
|
try {
|
|
const content = await readFile(entrypointPath, "utf-8");
|
|
if (regex.test(content)) return entrypoint;
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
for (const entrypoint of entrypoints) {
|
|
const entrypointPath = join(cwd, "src", entrypoint);
|
|
try {
|
|
const content = await readFile(entrypointPath, "utf-8");
|
|
if (regex.test(content)) return join("src", entrypoint);
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
throw new Error("No entrypoint found");
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/index.ts
|
|
const require = createRequire(import.meta.url);
|
|
const getBuildSummary = async (outputDir) => {
|
|
const buildSummary = await readFile(join(outputDir, ".cervel.json"), "utf-8");
|
|
return JSON.parse(buildSummary);
|
|
};
|
|
const build = async (args) => {
|
|
const entrypoint = args.entrypoint || await findEntrypoint(args.cwd);
|
|
const tsPromise = typescript({
|
|
...args,
|
|
entrypoint,
|
|
workPath: args.cwd
|
|
});
|
|
const rolldownResult = await rolldown({
|
|
...args,
|
|
entrypoint,
|
|
workPath: args.cwd,
|
|
repoRootPath: args.cwd,
|
|
out: args.out
|
|
});
|
|
await writeFile(join(args.cwd, args.out, ".cervel.json"), JSON.stringify({ handler: rolldownResult.result.handler }, null, 2));
|
|
console.log(Colors.gray(`${Colors.bold(Colors.cyan("✓"))} Build complete`));
|
|
const typecheckComplete = true;
|
|
const result = tsPromise ? await Promise.race([tsPromise.then(() => typecheckComplete), Promise.resolve(false)]) : true;
|
|
if (tsPromise && !result) console.log(Colors.gray(`${Colors.bold(Colors.gray("*"))} Waiting for typecheck...`));
|
|
return {
|
|
rolldownResult: rolldownResult.result,
|
|
tsPromise
|
|
};
|
|
};
|
|
const serve = async (args) => {
|
|
const entrypoint = await findEntrypoint(args.cwd);
|
|
const srvxBin = join(require.resolve("srvx"), "..", "..", "..", "bin", "srvx.mjs");
|
|
const tsxBin = require.resolve("tsx");
|
|
const restArgs = Object.entries(args.rest).filter(([, value]) => value !== void 0 && value !== false).map(([key, value]) => typeof value === "boolean" ? `--${key}` : `--${key}=${value}`);
|
|
if (!args.rest.import) restArgs.push("--import", tsxBin);
|
|
await execa("npx", [
|
|
srvxBin,
|
|
...restArgs,
|
|
entrypoint
|
|
], {
|
|
cwd: args.cwd,
|
|
stdio: "inherit"
|
|
});
|
|
};
|
|
const srvxOptions = {
|
|
help: {
|
|
type: "boolean",
|
|
short: "h"
|
|
},
|
|
version: {
|
|
type: "boolean",
|
|
short: "v"
|
|
},
|
|
prod: { type: "boolean" },
|
|
port: {
|
|
type: "string",
|
|
short: "p"
|
|
},
|
|
host: {
|
|
type: "string",
|
|
short: "H"
|
|
},
|
|
static: {
|
|
type: "string",
|
|
short: "s"
|
|
},
|
|
import: { type: "string" },
|
|
tls: { type: "boolean" },
|
|
cert: { type: "string" },
|
|
key: { type: "string" }
|
|
};
|
|
|
|
//#endregion
|
|
export { build, findEntrypoint, getBuildSummary, serve, srvxOptions }; |