import { build } from "rolldown"; import { type ExtensionBuildSpec, isExternal } from "./spec.ts"; export interface BundleOptions { release?: boolean | undefined; } export interface BundleResult { /** Entry output files relative to the output directory. */ entries: string[]; /** Every emitted file relative to the output directory. */ files: string[]; } /** * Bundle one extension into `outDir`. * Chunks stay flat in the package root so `import.meta.url` relative resource * lookups resolve identically from the entry and from any chunk. */ export async function bundleExtension( spec: ExtensionBuildSpec, outDir: string, options: BundleOptions = {}, ): Promise { const result = await build({ input: spec.entries, cwd: spec.dir, platform: "node", // Source imports sibling modules as `./x.js`; the file on disk is `x.ts`. resolve: { extensionAlias: { ".js": [".ts", ".js"] } }, external: (id, _parent, isResolved) => isResolved ? false : isExternal(id, spec.externals), logLevel: "warn", onLog(level, log, defaultHandler) { if (level === "warn" && /import\.meta|circular/iu.test(log.message)) return; defaultHandler(level, log); }, output: { dir: outDir, format: "esm", // Pi transforms .ts through its loader; native .js bypasses host SDK bindings. entryFileNames: "[name].ts", chunkFileNames: "[name]-[hash].ts", hashCharacters: "base36", sourcemap: !options.release, minify: Boolean(options.release), }, }); const files: string[] = []; const entries: string[] = []; for (const output of result.output) { files.push(output.fileName); if (output.type === "chunk" && output.isEntry) entries.push(output.fileName); if (output.type === "chunk" && output.sourcemapFileName) files.push(output.sourcemapFileName); } return { entries, files }; } /** Bundle standalone Node graphs as JavaScript so Node can execute them anywhere. */ export async function bundleNodeEntries( spec: ExtensionBuildSpec, outDir: string, options: BundleOptions = {}, ): Promise { if (!Object.keys(spec.nodeEntries).length) return; await build({ input: spec.nodeEntries, cwd: spec.dir, platform: "node", resolve: { extensionAlias: { ".js": [".ts", ".js"] } }, external: (id, _parent, isResolved) => isResolved ? false : isExternal(id, spec.externals), logLevel: "warn", output: { dir: outDir, format: "esm", entryFileNames: "[name].js", chunkFileNames: "[name]-[hash].js", hashCharacters: "base36", sourcemap: !options.release, minify: Boolean(options.release), }, }); } /** Build opt-in browser code separately so Node entries never pull browser runtime. */ export async function bundleBrowserEntries( spec: ExtensionBuildSpec, outDir: string, options: BundleOptions = {}, ): Promise { if (!Object.keys(spec.browserEntries).length) return; await build({ input: spec.browserEntries, cwd: spec.dir, platform: "browser", // Browser-only bundles do not use React Server Components boundaries. checks: { moduleLevelDirective: false }, transform: { define: { "process.env.NODE_ENV": JSON.stringify("production") }, }, resolve: { extensionAlias: { ".js": [".ts", ".tsx", ".js"] } }, logLevel: "warn", output: { dir: `${outDir}/browser`, format: "esm", entryFileNames: "[name].js", chunkFileNames: "[name]-[hash].js", assetFileNames: "[name]-[hash][extname]", hashCharacters: "base36", sourcemap: !options.release, minify: Boolean(options.release), }, }); }