convex
Version:
Client for the Convex Cloud
225 lines (209 loc) • 6.41 kB
text/typescript
import path from "path";
import chalk from "chalk";
import esbuild from "esbuild";
import { Filesystem } from "./fs.js";
export { nodeFs, RecordingFs } from "./fs.js";
export type { Filesystem } from "./fs.js";
export const actionsDir = "actions";
// Returns a generator of { isDir, path } for all paths
// within dirPath in some topological order (not including
// dirPath itself).
export function* walkDir(
fs: Filesystem,
dirPath: string
): Generator<{ isDir: boolean; path: string }, void, void> {
for (const dirEntry of fs.listDir(dirPath)) {
const childPath = path.join(dirPath, dirEntry.name);
if (dirEntry.isDirectory()) {
yield { isDir: true, path: childPath };
yield* walkDir(fs, childPath);
} else if (dirEntry.isFile()) {
yield { isDir: false, path: childPath };
}
}
}
export interface Bundle {
path: string;
source: string;
sourceMap?: string;
}
export class BundleError extends Error {}
type EsBuildResult = esbuild.BuildResult & {
outputFiles: esbuild.OutputFile[];
};
async function doEsbuild(
fs: Filesystem,
dir: string,
entryPoints: string[],
generateSourceMaps: boolean,
platform: esbuild.Platform,
chunksFolder: string
): Promise<EsBuildResult> {
try {
const result = await esbuild.build({
entryPoints,
bundle: true,
platform: platform,
format: "esm",
target: "esnext",
outdir: "out",
outbase: dir,
write: false,
sourcemap: generateSourceMaps,
splitting: true,
chunkNames: path.join(chunksFolder, "[hash]"),
treeShaking: true,
minify: false,
metafile: true,
});
for (const [relPath, input] of Object.entries(result.metafile!.inputs)) {
// TODO: esbuild outputs paths prefixed with "(disabled)"" when bundling our internal
// udf-system package. The files do actually exist locally, though.
if (relPath.indexOf("(disabled):") !== -1) {
continue;
}
const absPath = path.resolve(relPath);
const st = fs.stat(absPath);
if (st.size !== input.bytes) {
throw new Error(
`Bundled file ${absPath} changed right after esbuild invocation`
);
}
fs.registerPath(absPath, st);
}
return result;
} catch (err) {
throw new BundleError(`esbuild failed: ${(err as any).toString()}`);
}
}
export async function bundle(
fs: Filesystem,
dir: string,
entryPoints: string[],
generateSourceMaps: boolean,
platform: esbuild.Platform,
chunksFolder = "_deps"
): Promise<Bundle[]> {
const result = await doEsbuild(
fs,
dir,
entryPoints,
generateSourceMaps,
platform,
chunksFolder
);
if (result.errors.length) {
for (const error of result.errors) {
console.log(chalk.red(`esbuild error: ${error.text}`));
}
throw new BundleError("esbuild failed");
}
for (const warning of result.warnings) {
console.log(chalk.yellow(`esbuild warning: ${warning.text}`));
}
const sourceMaps = new Map();
const modules: Bundle[] = [];
for (const outputFile of result.outputFiles) {
const relPath = path.relative(path.normalize("out"), outputFile.path);
if (path.extname(relPath) === ".map") {
sourceMaps.set(relPath, outputFile.text);
continue;
}
const posixRelPath = relPath.split(path.sep).join(path.posix.sep);
modules.push({ path: posixRelPath, source: outputFile.text });
}
for (const module of modules) {
const sourceMapPath = module.path + ".map";
const sourceMap = sourceMaps.get(sourceMapPath);
if (sourceMap) {
module.sourceMap = sourceMap;
}
}
return modules;
}
export async function bundleSchema(fs: Filesystem, dir: string) {
return bundle(fs, dir, [path.resolve(dir, "schema.ts")], true, "neutral");
}
// If you wanted to build regex patterns programatically (questionable but useful)
// you need to escape special characters in it.
function escapeRegex(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
async function entryPoints(
fs: Filesystem,
dir: string,
includePattern: RegExp,
verbose: boolean
): Promise<string[]> {
const entryPoints = [];
for (const { isDir, path: fpath } of walkDir(fs, dir)) {
if (isDir) {
continue;
}
const relPath = path.relative(dir, fpath);
const base = path.parse(fpath).base;
const log = (line: string) => {
if (verbose) {
console.log(line);
}
};
if (!relPath.match(includePattern)) {
// Skip without logging.
continue;
} else if (relPath.startsWith("_deps" + path.sep)) {
throw new Error(
`The path "${fpath}" is within the "_deps" directory, which is reserved for dependencies. Please move your code to another directory.`
);
} else if (relPath.startsWith("_generated" + path.sep)) {
log(chalk.yellow(`Skipping ${fpath}`));
} else if (base.startsWith(".")) {
log(chalk.yellow(`Skipping dotfile ${fpath}`));
} else if (base === "README.md") {
log(chalk.yellow(`Skipping ${fpath}`));
} else if (base === "_generated.ts") {
log(chalk.yellow(`Skipping ${fpath}`));
} else if (base === "schema.ts") {
log(chalk.yellow(`Skipping ${fpath}`));
} else if (base.includes(".test.")) {
log(chalk.yellow(`Skipping ${fpath}`));
} else if (base === "tsconfig.json") {
log(chalk.yellow(`Skipping ${fpath}`));
} else if (relPath.includes(" ")) {
log(chalk.yellow(`Skipping ${relPath} because it contains a space`));
} else {
log(chalk.green(`Preparing ${fpath}`));
entryPoints.push(fpath);
}
}
return entryPoints;
}
export async function allEntryPoints(
fs: Filesystem,
dir: string,
verbose: boolean
) {
return entryPoints(fs, dir, new RegExp(".*"), verbose);
}
export async function databaseEntryPoints(
fs: Filesystem,
dir: string,
verbose: boolean
): Promise<string[]> {
const excludePrefix = actionsDir + path.sep;
return entryPoints(
fs,
dir,
// Exclude functions/ subdirectory.
RegExp(`^(?!${escapeRegex(excludePrefix)})`),
verbose
);
}
export async function actionsEntryPoints(
fs: Filesystem,
dir: string,
verbose: boolean
): Promise<string[]> {
// Only look in functions subdirectory.
const prefix = actionsDir + path.sep;
return entryPoints(fs, dir, RegExp(`^${escapeRegex(prefix)}`), verbose);
}