UNPKG

convex

Version:

Client for the Convex Cloud

225 lines (209 loc) 6.41 kB
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); }