convex
Version:
Client for the Convex Cloud
402 lines (401 loc) • 15.4 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var bundler_exports = {};
__export(bundler_exports, {
RecordingFs: () => import_fs2.RecordingFs,
actionsDir: () => actionsDir,
bundle: () => bundle,
bundleAuthConfig: () => bundleAuthConfig,
bundleSchema: () => bundleSchema,
doesImportConvexHttpRouter: () => doesImportConvexHttpRouter,
entryPoints: () => entryPoints,
entryPointsByEnvironment: () => entryPointsByEnvironment,
mustBeIsolate: () => mustBeIsolate,
nodeFs: () => import_fs2.nodeFs,
useNodeDirectiveRegex: () => useNodeDirectiveRegex,
walkDir: () => walkDir
});
module.exports = __toCommonJS(bundler_exports);
var import_path = __toESM(require("path"), 1);
var import_chalk = __toESM(require("chalk"), 1);
var import_esbuild = __toESM(require("esbuild"), 1);
var import_parser = require("@babel/parser");
var Sentry = __toESM(require("@sentry/node"), 1);
var import_context = require("./context.js");
var import_wasm = require("./wasm.js");
var import_external = require("./external.js");
var import_fs2 = require("./fs.js");
const actionsDir = "actions";
function* walkDir(fs, dirPath, depth) {
depth = depth ?? 0;
for (const dirEntry of fs.listDir(dirPath).sort()) {
const childPath = import_path.default.join(dirPath, dirEntry.name);
if (dirEntry.isDirectory()) {
yield { isDir: true, path: childPath, depth };
yield* walkDir(fs, childPath, depth + 1);
} else if (dirEntry.isFile()) {
yield { isDir: false, path: childPath, depth };
}
}
}
async function doEsbuild(ctx, dir, entryPoints2, generateSourceMaps, platform, chunksFolder, externalPackages) {
const external = (0, import_external.createExternalPlugin)(ctx, externalPackages);
try {
const result = await import_esbuild.default.build({
entryPoints: entryPoints2,
bundle: true,
platform,
format: "esm",
target: "esnext",
jsx: "automatic",
outdir: "out",
outbase: dir,
conditions: ["convex", "module"],
// The wasmPlugin should be last so it doesn't run on external modules.
plugins: [external.plugin, import_wasm.wasmPlugin],
write: false,
sourcemap: generateSourceMaps,
splitting: true,
chunkNames: import_path.default.join(chunksFolder, "[hash]"),
treeShaking: true,
minify: false,
keepNames: true,
metafile: true
});
for (const [relPath, input] of Object.entries(result.metafile.inputs)) {
if (relPath.indexOf("(disabled):") !== -1 || relPath.startsWith("wasm-binary:") || relPath.startsWith("wasm-stub:")) {
continue;
}
const absPath = import_path.default.resolve(relPath);
const st = ctx.fs.stat(absPath);
if (st.size !== input.bytes) {
(0, import_context.logWarning)(
ctx,
`Bundled file ${absPath} changed right after esbuild invocation`
);
return await ctx.crash({
exitCode: 1,
errorType: "transient",
printedMessage: null
});
}
ctx.fs.registerPath(absPath, st);
}
return {
...result,
externalModuleNames: external.externalModuleNames,
bundledModuleNames: external.bundledModuleNames
};
} catch {
return await ctx.crash({
exitCode: 1,
errorType: "invalid filesystem data",
// We don't print any error because esbuild already printed
// all the relevant information.
printedMessage: null
});
}
}
async function bundle(ctx, dir, entryPoints2, generateSourceMaps, platform, chunksFolder = "_deps", externalPackagesAllowList = []) {
const availableExternalPackages = await (0, import_external.computeExternalPackages)(
ctx,
externalPackagesAllowList
);
const result = await doEsbuild(
ctx,
dir,
entryPoints2,
generateSourceMaps,
platform,
chunksFolder,
availableExternalPackages
);
if (result.errors.length) {
const errorMessage = result.errors.map((e) => `esbuild error: ${e.text}`).join("\n");
return await ctx.crash({
exitCode: 1,
errorType: "invalid filesystem data",
printedMessage: errorMessage
});
}
for (const warning of result.warnings) {
(0, import_context.logWarning)(ctx, import_chalk.default.yellow(`esbuild warning: ${warning.text}`));
}
const sourceMaps = /* @__PURE__ */ new Map();
const modules = [];
const environment = platform === "node" ? "node" : "isolate";
for (const outputFile of result.outputFiles) {
const relPath = import_path.default.relative(import_path.default.normalize("out"), outputFile.path);
if (import_path.default.extname(relPath) === ".map") {
sourceMaps.set(relPath, outputFile.text);
continue;
}
const posixRelPath = relPath.split(import_path.default.sep).join(import_path.default.posix.sep);
modules.push({ path: posixRelPath, source: outputFile.text, environment });
}
for (const module2 of modules) {
const sourceMapPath = module2.path + ".map";
const sourceMap = sourceMaps.get(sourceMapPath);
if (sourceMap) {
module2.sourceMap = sourceMap;
}
}
return {
modules,
externalDependencies: await externalPackageVersions(
ctx,
availableExternalPackages,
result.externalModuleNames
),
bundledModuleNames: result.bundledModuleNames
};
}
async function externalPackageVersions(ctx, availableExternalPackages, referencedPackages) {
const versions = /* @__PURE__ */ new Map();
const referencedPackagesQueue = Array.from(referencedPackages.keys());
for (let i = 0; i < referencedPackagesQueue.length; i++) {
const moduleName = referencedPackagesQueue[i];
const modulePath = availableExternalPackages.get(moduleName).path;
const { version, peerAndOptionalDependencies } = await (0, import_external.findExactVersionAndDependencies)(ctx, moduleName, modulePath);
versions.set(moduleName, version);
for (const dependency of peerAndOptionalDependencies) {
if (availableExternalPackages.has(dependency) && !referencedPackages.has(dependency)) {
referencedPackagesQueue.push(dependency);
referencedPackages.add(dependency);
}
}
}
return versions;
}
async function bundleSchema(ctx, dir) {
let target = import_path.default.resolve(dir, "schema.ts");
if (!ctx.fs.exists(target)) {
target = import_path.default.resolve(dir, "schema.js");
}
const result = await bundle(ctx, dir, [target], true, "browser");
return result.modules;
}
async function bundleAuthConfig(ctx, dir) {
const authConfigPath = import_path.default.resolve(dir, "auth.config.js");
const authConfigTsPath = import_path.default.resolve(dir, "auth.config.ts");
if (ctx.fs.exists(authConfigPath) && ctx.fs.exists(authConfigTsPath)) {
return await ctx.crash({
exitCode: 1,
errorType: "invalid filesystem data",
printedMessage: `Found both ${authConfigPath} and ${authConfigTsPath}, choose one.`
});
}
const chosenPath = ctx.fs.exists(authConfigTsPath) ? authConfigTsPath : authConfigPath;
if (!ctx.fs.exists(chosenPath)) {
return [];
}
const result = await bundle(ctx, dir, [chosenPath], true, "browser");
return result.modules;
}
async function doesImportConvexHttpRouter(source) {
try {
const ast = (0, import_parser.parse)(source, {
sourceType: "module",
plugins: ["typescript"]
});
return ast.program.body.some((node) => {
if (node.type !== "ImportDeclaration") return false;
return node.specifiers.some((s) => {
const specifier = s;
const imported = specifier.imported;
return imported.name === "httpRouter";
});
});
} catch {
return source.match(
/import\s*\{\s*httpRouter.*\}\s*from\s*"\s*convex\/server\s*"/
) !== null;
}
}
async function entryPoints(ctx, dir, verbose) {
const entryPoints2 = [];
const log = (line) => {
if (verbose) {
console.log(line);
}
};
for (const { isDir, path: fpath, depth } of walkDir(ctx.fs, dir)) {
if (isDir) {
continue;
}
const relPath = import_path.default.relative(dir, fpath);
const parsedPath = import_path.default.parse(fpath);
const base = parsedPath.base;
const extension = parsedPath.ext.toLowerCase();
if (relPath.startsWith("_deps" + import_path.default.sep)) {
return await ctx.crash({
exitCode: 1,
errorType: "invalid filesystem data",
printedMessage: `The path "${fpath}" is within the "_deps" directory, which is reserved for dependencies. Please move your code to another directory.`
});
}
if (depth === 0 && base.toLowerCase().startsWith("https.")) {
const source = ctx.fs.readUtf8File(fpath);
if (await doesImportConvexHttpRouter(source))
(0, import_context.logWarning)(
ctx,
import_chalk.default.yellow(
`Found ${fpath}. HTTP action routes will not be imported from this file. Did you mean to include http${extension}?`
)
);
Sentry.captureMessage(
`User code top level directory contains file ${base} which imports httpRouter.`,
"warning"
);
}
if (relPath.startsWith("_generated" + import_path.default.sep)) {
log(import_chalk.default.yellow(`Skipping ${fpath}`));
} else if (base.startsWith(".")) {
log(import_chalk.default.yellow(`Skipping dotfile ${fpath}`));
} else if (base.startsWith("#")) {
log(import_chalk.default.yellow(`Skipping likely emacs tempfile ${fpath}`));
} else if (base === "README.md") {
log(import_chalk.default.yellow(`Skipping ${fpath}`));
} else if (base === "_generated.ts") {
log(import_chalk.default.yellow(`Skipping ${fpath}`));
} else if (base === "schema.ts" || base === "schema.js") {
log(import_chalk.default.yellow(`Skipping ${fpath}`));
} else if ((base.match(/\./g) || []).length > 1) {
log(import_chalk.default.yellow(`Skipping ${fpath} that contains multiple dots`));
} else if (base === "tsconfig.json") {
log(import_chalk.default.yellow(`Skipping ${fpath}`));
} else if (relPath.endsWith(".config.js")) {
log(import_chalk.default.yellow(`Skipping ${fpath}`));
} else if (relPath.includes(" ")) {
log(import_chalk.default.yellow(`Skipping ${relPath} because it contains a space`));
} else if (base.endsWith(".d.ts")) {
log(import_chalk.default.yellow(`Skipping ${fpath} declaration file`));
} else if (base.endsWith(".json")) {
log(import_chalk.default.yellow(`Skipping ${fpath} json file`));
} else if (base.endsWith(".jsonl")) {
log(import_chalk.default.yellow(`Skipping ${fpath} jsonl file`));
} else {
log(import_chalk.default.green(`Preparing ${fpath}`));
entryPoints2.push(fpath);
}
}
const nonEmptyEntryPoints = entryPoints2.filter((fpath) => {
if (!fpath.endsWith(".ts") && !fpath.endsWith(".tsx")) {
return true;
}
const contents = ctx.fs.readUtf8File(fpath);
if (/^\s{0,100}(import|export)/m.test(contents)) {
return true;
}
log(
import_chalk.default.yellow(
`Skipping ${fpath} because it has no export or import to make it a valid TypeScript module`
)
);
});
return nonEmptyEntryPoints;
}
const useNodeDirectiveRegex = /^\s*("|')use node("|');?\s*$/;
function hasUseNodeDirective(fs, fpath, verbose) {
const source = fs.readUtf8File(fpath);
if (source.indexOf("use node") === -1) {
return false;
}
try {
const ast = (0, import_parser.parse)(source, {
// parse in strict mode and allow module declarations
sourceType: "module",
// esbuild supports jsx and typescript by default. Allow the same plugins
// here too.
plugins: ["jsx", "typescript"]
});
return ast.program.directives.map((d) => d.value.value).includes("use node");
} catch (error) {
let lineMatches = false;
for (const line of source.split("\n")) {
if (line.match(useNodeDirectiveRegex)) {
lineMatches = true;
break;
}
}
if (verbose) {
console.warn(
`Failed to parse ${fpath}. Use node is set to ${lineMatches} based on regex. Parse error: ${error.toString()}.`
);
}
return lineMatches;
}
}
function mustBeIsolate(relPath) {
return ["http", "crons", "schema", "auth.config"].includes(
relPath.replace(/\.[^/.]+$/, "")
);
}
async function determineEnvironment(ctx, dir, fpath, verbose) {
const relPath = import_path.default.relative(dir, fpath);
const useNodeDirectiveFound = hasUseNodeDirective(ctx.fs, fpath, verbose);
if (useNodeDirectiveFound) {
if (mustBeIsolate(relPath)) {
return await ctx.crash({
exitCode: 1,
errorType: "invalid filesystem data",
printedMessage: `"use node" directive is not allowed for ${relPath}.`
});
}
return "node";
}
const actionsPrefix = actionsDir + import_path.default.sep;
if (relPath.startsWith(actionsPrefix)) {
return await ctx.crash({
exitCode: 1,
errorType: "invalid filesystem data",
printedMessage: `${relPath} is in /actions subfolder but has no "use node"; directive. You can now define actions in any folder and indicate they should run in node by adding "use node" directive. /actions is a deprecated way to choose Node.js environment, and we require "use node" for all files within that folder to avoid unexpected errors during the migration. See https://docs.convex.dev/functions/actions for more details`
});
}
return "isolate";
}
async function entryPointsByEnvironment(ctx, dir, verbose) {
const isolate = [];
const node = [];
for (const entryPoint of await entryPoints(ctx, dir, verbose)) {
const environment = await determineEnvironment(
ctx,
dir,
entryPoint,
verbose
);
if (environment === "node") {
node.push(entryPoint);
} else {
isolate.push(entryPoint);
}
}
return { isolate, node };
}
//# sourceMappingURL=index.js.map