repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
scripts/build/bundle.ts
Rawimport { 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<BundleResult> {
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<void> {
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<void> {
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),
},
});
}