UNPKG

convex

Version:

Client for the Convex Cloud

488 lines (487 loc) 17.5 kB
"use strict"; 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 bundle_exports = {}; __export(bundle_exports, { bundleDefinitions: () => bundleDefinitions, bundleImplementations: () => bundleImplementations, componentGraph: () => componentGraph, getDeps: () => getDeps }); module.exports = __toCommonJS(bundle_exports); var import_path = __toESM(require("path"), 1); var import_directoryStructure = require("./directoryStructure.js"); var import_context = require("../../../../bundler/context.js"); var import_esbuild = __toESM(require("esbuild"), 1); var import_chalk = __toESM(require("chalk"), 1); var import_bundler = require("../../../../bundler/index.js"); function componentPlugin({ mode = "bundle", rootComponentDirectory, verbose, ctx }) { const components = /* @__PURE__ */ new Map(); return { name: `convex-${mode === "discover" ? "discover-components" : "bundle-components"}`, async setup(build) { build.onResolve({ filter: /.*convex.config.*/ }, async (args) => { verbose && (0, import_context.logMessage)(ctx, "esbuild resolving import:", args); if (args.namespace !== "file") { verbose && (0, import_context.logMessage)(ctx, " Not a file."); return; } if (args.kind === "entry-point") { verbose && (0, import_context.logMessage)(ctx, " -> Top-level entry-point."); const componentDirectory = await (0, import_directoryStructure.buildComponentDirectory)( ctx, import_path.default.resolve(args.path) ); if (components.get(args.path)) { throw new Error( `Entry point component "${args.path}" already registered.` ); } components.set(args.path, componentDirectory); return; } const candidates = [args.path]; const ext = import_path.default.extname(args.path); if (ext === ".js") { candidates.push(args.path.slice(0, -".js".length) + ".ts"); } if (ext !== ".js" && ext !== ".ts") { candidates.push(args.path + ".js"); candidates.push(args.path + ".ts"); } let resolvedPath = void 0; for (const candidate of candidates) { const result = await build.resolve(candidate, { // We expect this to be "import-statement" but pass 'kind' through // to say honest to normal esbuild behavior. kind: args.kind, resolveDir: args.resolveDir }); if (result.path) { resolvedPath = result.path; break; } } if (resolvedPath === void 0) { verbose && (0, import_context.logMessage)(ctx, ` -> ${args.path} not found.`); return; } const parentDir = import_path.default.dirname(resolvedPath); let imported = components.get(resolvedPath); if (!imported) { const isComponent = (0, import_directoryStructure.isComponentDirectory)(ctx, parentDir, false); if (isComponent.kind !== "ok") { verbose && (0, import_context.logMessage)(ctx, " -> Not a component:", isComponent); return; } imported = isComponent.component; components.set(resolvedPath, imported); } verbose && (0, import_context.logMessage)( ctx, " -> Component import! Recording it.", args.path, resolvedPath ); if (mode === "discover") { return { path: resolvedPath }; } else { const componentPath = (0, import_directoryStructure.toComponentDefinitionPath)( rootComponentDirectory, imported ); const importPath = definitionImportPath(componentPath); return { path: importPath, external: true }; } }); } }; } function definitionImportPath(componentPath) { return `./_componentDeps/${Buffer.from(componentPath).toString("base64url")}`; } function sharedEsbuildOptions({ liveComponentSources = false }) { const options = { bundle: true, platform: "browser", format: "esm", target: "esnext", conditions: ["convex", "module"], // `false` is the default for splitting. It's simpler to evaluate these on // the server as a single file. // Splitting could be enabled for speed once the server supports it. splitting: false, // place output files in memory at their source locations write: false, outdir: import_path.default.parse(process.cwd()).root, outbase: import_path.default.parse(process.cwd()).root, minify: true, keepNames: true, metafile: true }; if (liveComponentSources) { options.conditions.push("@convex-dev/component-source"); } return options; } async function componentGraph(ctx, absWorkingDir, rootComponentDirectory, liveComponentSources, verbose = true) { let result; try { result = await import_esbuild.default.build({ absWorkingDir, // This is mostly useful for formatting error messages. entryPoints: [(0, import_directoryStructure.qualifiedDefinitionPath)(rootComponentDirectory)], plugins: [ componentPlugin({ ctx, mode: "discover", verbose, rootComponentDirectory }) ], sourcemap: "external", sourcesContent: false, ...sharedEsbuildOptions({ liveComponentSources }) }); await registerEsbuildReads(ctx, absWorkingDir, result.metafile); } catch (err) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: `esbuild failed: ${err}` }); } if (result.errors.length) { const message = result.errors.map((error) => error.text).join("\n"); return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: message }); } for (const warning of result.warnings) { console.log(import_chalk.default.yellow(`esbuild warning: ${warning.text}`)); } return await findComponentDependencies(ctx, result.metafile); } function getDeps(rootComponent, dependencyGraph, definitionPath) { return dependencyGraph.filter( ([importer, _imported]) => importer.definitionPath === definitionPath ).map( ([_importer, imported]) => (0, import_directoryStructure.toComponentDefinitionPath)(rootComponent, imported) ); } async function findComponentDependencies(ctx, metafile) { const { inputs } = metafile; const componentInputs = Object.keys(inputs).filter( (path2) => path2.includes(".config.") ); const componentsByAbsPath = /* @__PURE__ */ new Map(); for (const inputPath of componentInputs) { const importer = await (0, import_directoryStructure.buildComponentDirectory)(ctx, inputPath); componentsByAbsPath.set(import_path.default.resolve(inputPath), importer); } const dependencyGraph = []; for (const inputPath of componentInputs) { const importer = componentsByAbsPath.get(import_path.default.resolve(inputPath)); const { imports } = inputs[inputPath]; const componentImports = imports.filter( (imp) => imp.path.includes(".config.") ); for (const importPath of componentImports.map((dep) => dep.path)) { const imported = componentsByAbsPath.get(import_path.default.resolve(importPath)); if (!imported) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: `Didn't find ${import_path.default.resolve(importPath)} in ${[...componentsByAbsPath.keys()].toString()}` }); } dependencyGraph.push([importer, imported]); } } const components = /* @__PURE__ */ new Map(); for (const directory of componentsByAbsPath.values()) { components.set(directory.path, directory); } return { components, dependencyGraph }; } async function bundleDefinitions(ctx, absWorkingDir, dependencyGraph, rootComponentDirectory, componentDirectories, liveComponentSources, verbose = false) { let result; try { result = await import_esbuild.default.build({ absWorkingDir, entryPoints: componentDirectories.map( (dir) => (0, import_directoryStructure.qualifiedDefinitionPath)(dir) ), plugins: [ componentPlugin({ ctx, mode: "bundle", verbose, rootComponentDirectory }) ], sourcemap: true, ...sharedEsbuildOptions({ liveComponentSources }) }); await registerEsbuildReads(ctx, absWorkingDir, result.metafile); } catch (err) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: `esbuild failed: ${err}` }); } if (result.errors.length) { const message = result.errors.map((error) => error.text).join("\n"); return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: message }); } for (const warning of result.warnings) { console.log(import_chalk.default.yellow(`esbuild warning: ${warning.text}`)); } const outputs = []; for (const directory of componentDirectories) { const absInput = import_path.default.resolve(absWorkingDir, directory.definitionPath); const expectedOutputJs = absInput.slice(0, absInput.lastIndexOf(".")) + ".js"; const expectedOutputMap = absInput.slice(0, absInput.lastIndexOf(".")) + ".js.map"; const outputJs = result.outputFiles.filter( (outputFile) => outputFile.path === expectedOutputJs )[0]; if (!outputJs) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `no JS found matching ${expectedOutputJs} in ${result.outputFiles.map((x) => x.path).toString()}` }); } const outputJsMap = result.outputFiles.filter( (outputFile) => outputFile.path === expectedOutputMap )[0]; outputs.push({ outputJs, outputJsMap, directory }); } const appBundles = outputs.filter( (out) => out.directory.path === rootComponentDirectory.path ); if (appBundles.length !== 1) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "found wrong number of app bundles" }); } const appBundle = appBundles[0]; const componentBundles = outputs.filter( (out) => out.directory.path !== rootComponentDirectory.path ); const componentDefinitionSpecsWithoutImpls = componentBundles.map(({ directory, outputJs, outputJsMap }) => ({ definitionPath: (0, import_directoryStructure.toComponentDefinitionPath)( rootComponentDirectory, directory ), definition: { path: import_path.default.relative(directory.path, outputJs.path), source: outputJs.text, sourceMap: outputJsMap?.text, environment: "isolate" }, dependencies: getDeps( rootComponentDirectory, dependencyGraph, directory.definitionPath ) })); const appDeps = getDeps( rootComponentDirectory, dependencyGraph, appBundle.directory.definitionPath ); const appDefinitionSpecWithoutImpls = { definition: { path: import_path.default.relative(rootComponentDirectory.path, appBundle.outputJs.path), source: appBundle.outputJs.text, sourceMap: appBundle.outputJsMap?.text, environment: "isolate" }, dependencies: appDeps }; return { appDefinitionSpecWithoutImpls, componentDefinitionSpecsWithoutImpls }; } async function bundleImplementations(ctx, rootComponentDirectory, componentDirectories, nodeExternalPackages, extraConditions, verbose = false) { let appImplementation; const componentImplementations = []; let isRoot = true; for (const directory of [rootComponentDirectory, ...componentDirectories]) { const resolvedPath = import_path.default.resolve( rootComponentDirectory.path, directory.path ); let schema; if (ctx.fs.exists(import_path.default.resolve(resolvedPath, "schema.ts"))) { schema = (await (0, import_bundler.bundleSchema)(ctx, resolvedPath, extraConditions))[0] || null; } else if (ctx.fs.exists(import_path.default.resolve(resolvedPath, "schema.js"))) { schema = (await (0, import_bundler.bundleSchema)(ctx, resolvedPath, extraConditions))[0] || null; } else { schema = null; } const entryPoints = await (0, import_bundler.entryPointsByEnvironment)(ctx, resolvedPath); const convexResult = await (0, import_bundler.bundle)( ctx, resolvedPath, entryPoints.isolate, true, "browser", void 0, void 0, extraConditions ); if (convexResult.externalDependencies.size !== 0) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "external dependencies not supported" }); } const functions = convexResult.modules; if (isRoot) { if (verbose) { (0, import_context.showSpinner)(ctx, "Bundling modules for Node.js runtime..."); } const nodeResult = await (0, import_bundler.bundle)( ctx, resolvedPath, entryPoints.node, true, "node", import_path.default.join("_deps", "node"), nodeExternalPackages, extraConditions ); const externalNodeDependencies = []; for (const [ moduleName, moduleVersion ] of nodeResult.externalDependencies) { externalNodeDependencies.push({ name: moduleName, version: moduleVersion }); } const authBundle = await (0, import_bundler.bundleAuthConfig)(ctx, resolvedPath); appImplementation = { schema, functions: functions.concat(nodeResult.modules).concat(authBundle), externalNodeDependencies }; } else { if (directory.path !== rootComponentDirectory.path) { const nodeResult = await (0, import_bundler.bundle)( ctx, resolvedPath, entryPoints.node, true, "node", import_path.default.join("_deps", "node"), nodeExternalPackages, extraConditions ); if (nodeResult.modules.length > 0) { await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: `"use node" directive is not supported in components. Remove it from the component at: ${resolvedPath}.` }); } } const definitionPath = (0, import_directoryStructure.toComponentDefinitionPath)( rootComponentDirectory, directory ); componentImplementations.push({ definitionPath, schema, functions }); } isRoot = false; } if (!appImplementation) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "No app implementation found" }); } return { appImplementation, componentImplementations }; } async function registerEsbuildReads(ctx, absWorkingDir, metafile) { for (const [relPath, input] of Object.entries(metafile.inputs)) { if ( // We rewrite these files so this integrity check isn't useful. import_path.default.basename(relPath).includes("convex.config") || // TODO: esbuild outputs paths prefixed with "(disabled)" when bundling our internal // udf-system package. The files do actually exist locally, though. relPath.indexOf("(disabled):") !== -1 || relPath.startsWith("wasm-binary:") || relPath.startsWith("wasm-stub:") ) { continue; } const absPath = import_path.default.resolve(absWorkingDir, 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); } } //# sourceMappingURL=bundle.js.map