Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

1138 lines
33 KiB
TypeScript

#!/usr/bin/env bun
/**
* Dual build script for @elizaos/core - generates both Node.js and browser builds
*/
import { execFile } from "node:child_process";
import { existsSync, type FSWatcher, mkdirSync, watch } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import type { BuildConfig, BunPlugin } from "bun";
const execFileAsync = promisify(execFile);
const RM_RECURSIVE_SCRIPT = fileURLToPath(
new URL("../scripts/rm-path-recursive.mjs", import.meta.url),
);
export interface ElizaBuildOptions {
/** Entry points - defaults to ['src/index.ts'] */
entrypoints?: string[];
/** Output directory - defaults to 'dist' */
outdir?: string;
/** Target environment - defaults to 'node' for packages */
target?: "node" | "bun" | "browser";
/** External dependencies */
external?: string[];
/** Whether to generate sourcemaps */
sourcemap?: boolean | "linked" | "inline" | "external";
/** Whether to minify */
minify?: boolean;
/** Additional plugins */
plugins?: BunPlugin[];
/** Format - defaults to 'esm' */
format?: "esm" | "cjs";
/** Copy assets configuration */
assets?: Array<{ from: string; to: string }>;
/** Whether to leave existing files in place before building */
skipClean?: boolean;
/** Whether to generate TypeScript declarations (using tsc separately) */
generateDts?: boolean;
/**
* The name of the package being built (e.g., "@elizaos/core").
* When set, this package will NOT be added to externals to avoid self-referential imports.
*/
selfPackageName?: string;
}
/**
* Get performance timer
*/
export function getTimer() {
const start = performance.now();
return {
elapsed: () => {
const end = performance.now();
return (end - start).toFixed(2);
},
elapsedMs: () => {
const end = performance.now();
return Math.round(end - start);
},
};
}
function resolveTscBin(): string {
const workspaceTsc = join(process.cwd(), "../../node_modules/.bin/tsc");
return existsSync(workspaceTsc) ? workspaceTsc : "tsc";
}
async function withCoreBuildLock<T>(build: () => Promise<T>): Promise<T> {
const fs = await import("node:fs/promises");
const lockParent = join(process.cwd(), "../../.turbo");
const lockDir = join(lockParent, "core-build.lock");
const cleanupHelper = join(process.cwd(), "../scripts/rm-path-recursive.mjs");
const staleAfterMs = 30 * 60 * 1000;
let announcedWait = false;
async function readLockOwnerPid(): Promise<number | null> {
try {
const rawOwner = await fs.readFile(join(lockDir, "owner"), "utf8");
const pid = Number.parseInt(rawOwner.split(/\r?\n/, 1)[0] ?? "", 10);
return Number.isFinite(pid) && pid > 0 ? pid : null;
} catch {
return null;
}
}
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return (error as NodeJS.ErrnoException).code !== "ESRCH";
}
}
async function removeLockDir(): Promise<void> {
await execFileAsync("node", [cleanupHelper, lockDir], {
cwd: process.cwd(),
});
}
await fs.mkdir(lockParent, { recursive: true });
for (;;) {
try {
await fs.mkdir(lockDir);
await fs.writeFile(
join(lockDir, "owner"),
`${process.pid}\n${new Date().toISOString()}\n`,
);
break;
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code !== "EEXIST") {
throw error;
}
// error-policy:J3 existence probe — null means the lock dir vanished (or
// cannot be stat'd) between the EEXIST and here; the staleness check below
// treats a missing stat as "no owner mtime", never as a masked failure.
const stat = await fs.stat(lockDir).catch(() => null);
const ownerPid = await readLockOwnerPid();
if (
(ownerPid !== null && !isProcessAlive(ownerPid)) ||
(stat && Date.now() - stat.mtimeMs > staleAfterMs)
) {
await removeLockDir();
continue;
}
if (!announcedWait) {
console.log("⏳ Waiting for another @elizaos/core build to finish...");
announcedWait = true;
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
}
try {
return await build();
} finally {
// error-policy:J6 best-effort teardown — release the build lock dir; a failed
// cleanup must not mask the build result (or error) propagating from the try.
await removeLockDir().catch(() => {});
}
}
/**
* Creates a standardized Bun build configuration for elizaOS packages
*/
export async function createElizaBuildConfig(
options: ElizaBuildOptions,
): Promise<BuildConfig> {
const {
entrypoints = ["src/index.ts"],
outdir = "dist",
target = "node",
external = [],
sourcemap = false,
minify = false,
plugins = [],
format = "esm",
selfPackageName,
} = options;
const resolvedEntrypoints = entrypoints
.filter((entry) => entry && entry.trim() !== "")
.map((entry) => (entry.startsWith("./") ? entry : `./${entry}`));
const nodeExternals =
target === "node" || target === "bun"
? [
"node:*",
"fs",
"path",
"crypto",
"stream",
"buffer",
"util",
"events",
"url",
"http",
"https",
"os",
"child_process",
"worker_threads",
"cluster",
"zlib",
"querystring",
"string_decoder",
"tls",
"net",
"dns",
"dgram",
"readline",
"repl",
"vm",
"assert",
"console",
"process",
"timers",
"perf_hooks",
"async_hooks",
]
: [];
const elizaExternals = [
"@elizaos/core",
"@elizaos/cloud-routing",
"@elizaos/shared",
"@elizaos/plugin-*",
].filter((pkg) => pkg !== selfPackageName);
const cleanExternals = [...external].filter(
(ext) => ext && !ext.startsWith("//") && ext.trim() !== "",
);
const config: BuildConfig = {
entrypoints: resolvedEntrypoints,
outdir,
target: target === "node" ? "node" : target,
format,
sourcemap,
minify,
external: [...nodeExternals, ...elizaExternals, ...cleanExternals],
plugins,
naming: {
entry: "[dir]/[name].[ext]",
chunk: "[name]-[hash].[ext]",
asset: "[name]-[hash].[ext]",
},
};
return config;
}
/**
* Copy assets after build with proper error handling (parallel processing)
*/
export async function copyAssets(assets: Array<{ from: string; to: string }>) {
if (!assets.length) return;
const timer = getTimer();
const { cp } = await import("node:fs/promises");
console.log("Copying assets...");
// Process all assets in parallel
const copyPromises = assets.map(async (asset) => {
const assetTimer = getTimer();
try {
if (existsSync(asset.from)) {
await cp(asset.from, asset.to, { recursive: true });
return {
success: true,
message: `Copied ${asset.from} to ${asset.to} (${assetTimer.elapsed()}ms)`,
asset,
};
} else {
return {
success: false,
message: `Source not found: ${asset.from}`,
asset,
error: "Source not found",
};
}
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
message: `Failed to copy ${asset.from} to ${asset.to}: ${errorMessage}`,
asset,
error: errorMessage,
};
}
});
// Wait for all copies to complete
const results = await Promise.all(copyPromises);
// Process results
let successCount = 0;
const failedAssets: Array<{
asset: { from: string; to: string };
error: string;
}> = [];
results.forEach((result) => {
if (result.success) {
successCount++;
} else {
console.warn(` ⚠ ${result.message}`);
if (result.error) {
// Check for specific error types
if (result.error.includes("EACCES") || result.error.includes("EPERM")) {
console.error(
` Permission denied. Try running with elevated privileges.`,
);
} else if (result.error.includes("ENOSPC")) {
console.error(` Insufficient disk space.`);
}
failedAssets.push({ asset: result.asset, error: result.error });
}
}
});
const totalTime = timer.elapsed();
if (failedAssets.length === 0) {
console.log(`✓ Assets copied (${totalTime}ms)`);
} else if (successCount > 0) {
console.warn(
`⚠ Copied ${successCount}/${assets.length} assets (${totalTime}ms)`,
);
console.warn(
` Failed assets: ${failedAssets.map((f) => f.asset.from).join(", ")}`,
);
} else {
throw new Error(
`Failed to copy all assets. Errors: ${failedAssets.map((f) => `${f.asset.from}: ${f.error}`).join("; ")}`,
);
}
}
/**
* Generate TypeScript declarations using tsc
*/
export async function generateDts(
tsconfigPath = "./tsconfig.build.json",
throwOnError = true,
) {
const timer = getTimer();
const { $ } = await import("bun");
if (!existsSync(tsconfigPath)) {
console.warn(
`TypeScript config not found at ${tsconfigPath}, skipping d.ts generation`,
);
return;
}
console.log("Generating TypeScript declarations...");
try {
// Use incremental compilation for faster subsequent builds
await $`${resolveTscBin()} --emitDeclarationOnly --noCheck --project ${tsconfigPath} --composite false --incremental false --types node,bun`;
console.log(
`✓ TypeScript declarations generated successfully (${timer.elapsed()}ms)`,
);
} catch (error: unknown) {
console.error(
`✗ Failed to generate TypeScript declarations (${timer.elapsed()}ms)`,
);
console.error(
"Error details:",
error instanceof Error ? error.message : String(error),
);
if (throwOnError) {
// Propagate so calling build fails hard on TS errors
throw error;
}
console.warn("Continuing build without TypeScript declarations...");
}
}
/**
* Clean build artifacts with proper error handling and retry logic
*/
export async function cleanBuild(outdir = "dist", maxRetries = 3) {
const timer = getTimer();
if (!existsSync(outdir)) {
console.log(`✓ ${outdir} directory already clean (${timer.elapsed()}ms)`);
return;
}
let lastError: unknown;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await execFileAsync("node", [RM_RECURSIVE_SCRIPT, outdir], {
cwd: process.cwd(),
});
console.log(`✓ Cleaned ${outdir} directory (${timer.elapsed()}ms)`);
return; // Success, exit the function
} catch (error: unknown) {
lastError = error;
const errorMessage =
error instanceof Error ? error.message : String(error);
// Check for specific error types
if (errorMessage.includes("EACCES") || errorMessage.includes("EPERM")) {
console.error(`✗ Permission denied while cleaning ${outdir}`);
console.error(
` Try running with elevated privileges or check file permissions.`,
);
throw error; // Don't retry permission errors
} else if (errorMessage.includes("ENOENT")) {
// Directory was already deleted (possibly by concurrent process)
console.log(
`✓ ${outdir} directory was already removed (${timer.elapsed()}ms)`,
);
return;
} else if (
errorMessage.includes("EBUSY") ||
errorMessage.includes("EMFILE")
) {
// Resource busy or too many open files - these might be temporary
if (attempt < maxRetries) {
const waitTime = attempt * 500; // Exponential backoff: 500ms, 1000ms, 1500ms
console.warn(
`⚠ Failed to clean ${outdir} (attempt ${attempt}/${maxRetries}): ${errorMessage}`,
);
console.warn(` Retrying in ${waitTime}ms...`);
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
} else {
// Unknown error
console.error(`✗ Failed to clean ${outdir}: ${errorMessage}`);
throw error;
}
}
}
// If we've exhausted all retries
const finalError =
lastError instanceof Error ? lastError : new Error(String(lastError));
console.error(`✗ Failed to clean ${outdir} after ${maxRetries} attempts`);
throw finalError;
}
/**
* Watch files for changes and trigger rebuilds with proper cleanup
*/
export function watchFiles(
directory: string,
onChange: () => void,
options: {
extensions?: string[];
debounceMs?: number;
} = {},
): () => void {
const { extensions = [".ts", ".js", ".tsx", ".jsx"], debounceMs = 100 } =
options;
let debounceTimer: NodeJS.Timeout | null = null;
let watcher: FSWatcher | null = null;
let isCleanedUp = false;
console.log(`📁 Watching ${directory} for changes...`);
console.log("💡 Press Ctrl+C to stop\n");
// Cleanup function to close watcher and clear timers
const cleanup = () => {
if (isCleanedUp) return;
isCleanedUp = true;
if (debounceTimer) {
clearTimeout(debounceTimer);
debounceTimer = null;
}
if (watcher) {
try {
watcher.close();
} catch (_error) {
// Ignore errors during cleanup
}
watcher = null;
}
};
// Create the watcher with proper error handling
watcher = watch(directory, { recursive: true }, (_eventType, filename) => {
if (isCleanedUp) return;
if (filename && extensions.some((ext) => filename.endsWith(ext))) {
// Debounce to avoid multiple rapid rebuilds
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
if (!isCleanedUp) {
console.log(`\n📝 File changed: ${filename}`);
onChange();
}
}, debounceMs);
}
});
// Handle watcher errors
if (watcher && typeof watcher.on === "function") {
watcher.on("error", (error: Error) => {
console.error("Watch error:", error.message);
if (error.message.includes("EMFILE")) {
console.error(
"Too many open files. Consider increasing your system limits or reducing the watch scope.",
);
}
});
}
// Register cleanup handlers only once per watcher
const handleExit = () => {
cleanup();
console.log("\n\n👋 Stopping watch mode...");
process.exit(0);
};
// Remove any existing handlers to avoid duplicates
process.removeAllListeners("SIGINT");
process.removeAllListeners("SIGTERM");
// Add new handlers
process.once("SIGINT", handleExit);
process.once("SIGTERM", handleExit);
// Also cleanup on normal exit
process.once("exit", cleanup);
// Return cleanup function for manual cleanup
return cleanup;
}
/**
* Standard build runner configuration
*/
export interface BuildRunnerOptions {
packageName: string;
buildOptions: ElizaBuildOptions;
onBuildComplete?: (success: boolean) => void;
}
/**
* Run a build with optional watch mode support
*/
export async function runBuild(
options: BuildRunnerOptions & { isRebuild?: boolean },
) {
const {
packageName,
buildOptions,
isRebuild = false,
onBuildComplete,
} = options;
const totalTimer = getTimer();
// Clear console and show timestamp for rebuilds
if (isRebuild) {
console.clear();
const timestamp = new Date().toLocaleTimeString();
console.log(`[${timestamp}] 🔄 Rebuilding ${packageName}...\n`);
} else {
console.log(`🚀 Building ${packageName}...\n`);
}
const resolvedOutdir = buildOptions.outdir ?? "dist";
if (buildOptions.skipClean) {
mkdirSync(resolvedOutdir, { recursive: true });
} else {
// Clean previous build
await cleanBuild(resolvedOutdir);
}
// Bun.build does not always recreate an emptied outdir; ensure it exists after clean.
mkdirSync(resolvedOutdir, { recursive: true });
// Create build configuration
const configTimer = getTimer();
const config = await createElizaBuildConfig(buildOptions);
console.log(`✓ Configuration prepared (${configTimer.elapsed()}ms)`);
// Build with Bun
console.log("\nBundling with Bun...");
const buildTimer = getTimer();
const result = await Bun.build(config);
if (!result.success) {
console.error("✗ Build failed:", result.logs);
onBuildComplete?.(false);
return false;
}
const totalSize = result.outputs.reduce(
(sum, output) => sum + output.size,
0,
);
const sizeMB = (totalSize / 1024 / 1024).toFixed(2);
console.log(
`✓ Built ${result.outputs.length} file(s) - ${sizeMB}MB (${buildTimer.elapsed()}ms)`,
);
// Run post-build tasks
const postBuildTasks: Promise<undefined | null>[] = [];
// Add TypeScript declarations generation if requested
if (buildOptions.generateDts) {
postBuildTasks.push(
generateDts("./tsconfig.build.json")
.then(() => undefined)
.catch((err) => {
console.error("⚠ TypeScript declarations generation failed:", err);
// Don't throw here, as it's often non-critical
return null;
}),
);
}
// Add asset copying if specified
if (buildOptions.assets && buildOptions.assets.length > 0) {
postBuildTasks.push(
copyAssets(buildOptions.assets)
.then(() => undefined)
.catch((err) => {
console.error("✗ Asset copying failed:", err);
throw err; // Asset copying failure is critical
}),
);
}
// Execute all post-build tasks
if (postBuildTasks.length > 0) {
const postBuildTimer = getTimer();
await Promise.all(postBuildTasks);
console.log(`✓ Post-build tasks completed (${postBuildTimer.elapsed()}ms)`);
}
console.log(`\n✅ ${packageName} build complete!`);
console.log(`⏱️ Total build time: ${totalTimer.elapsed()}ms`);
onBuildComplete?.(true);
return true;
}
/**
* Create a standardized build runner with watch mode support
*/
export function createBuildRunner(options: BuildRunnerOptions) {
const isWatchMode = process.argv.includes("--watch");
let cleanupWatcher: (() => void) | null = null;
async function build(isRebuild = false) {
return runBuild({
...options,
isRebuild,
});
}
async function startWatchMode() {
console.log("👀 Starting watch mode...\n");
// Initial build
const buildSuccess = await build(false);
if (buildSuccess) {
const srcDir = join(process.cwd(), "src");
// Store the cleanup function returned by watchFiles
// The watcher stays active throughout the entire session
cleanupWatcher = watchFiles(srcDir, async () => {
await build(true);
console.log("📁 Watching src/ directory for changes...");
console.log("💡 Press Ctrl+C to stop\n");
});
}
}
// Ensure cleanup on process exit
const cleanup = () => {
if (cleanupWatcher) {
cleanupWatcher();
cleanupWatcher = null;
}
};
process.once("beforeExit", cleanup);
process.once("SIGUSR1", cleanup);
process.once("SIGUSR2", cleanup);
// Return the main function to run
return async function run() {
if (isWatchMode) {
await startWatchMode();
} else {
const success = await build();
if (!success) {
process.exit(1);
}
}
};
}
// Source directory for TypeScript
const TS_SRC = "src";
// Ensure dist directories exist
["dist", "dist/node", "dist/browser", "dist/edge"].forEach((dir) => {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
});
// Browser-specific externals (these should be provided by the host environment)
const browserExternals = [
// These will be loaded via CDN or bundled by the consuming app
"sharp", // Image processing - not available in browser
"@hapi/shot", // Test utility - not needed in browser
"@opentelemetry/context-async-hooks", // Exclude OpenTelemetry Node modules
"async_hooks", // Node.js built-in module
"node:diagnostics_channel", // Node.js built-in module
"node:async_hooks", // Node.js built-in module
"fs-extra", // Node-only fs library; host bundlers stub this for browser/Capacitor
// Document extractors are Node-only (mammoth uses fs.readFile.bind, unpdf
// pulls in Node fs/util internals). Reachable from
// features/documents/utils.ts; with `target: "node"` Bun inlines these
// even when only used inside Node-side flows. Mark as external so the
// host bundler resolves them only on the Node side.
"mammoth",
"unpdf",
// Vercel AI SDK gateway has a transitive `@vercel/oidc` import that
// touches `os.platform()`/`os.arch()`/`os.hostname()` at module-load
// to build a User-Agent string. That throws in the browser even when
// host bundlers stub `os`, because the call site uses
// `(0, os.platform)()` after a destructured re-binding, which loses the
// Proxy chaining. Externalise the gateway and its transitive sentinel
// so the browser bundle never invokes the call.
"@ai-sdk/gateway",
"@vercel/oidc",
];
// Node-specific externals (native modules and node-specific packages)
const nodeExternals = ["dotenv", "sharp", "zod", "@hapi/shot"];
// Shared configuration
const sharedConfig = {
packageName: "@elizaos/core",
sourcemap: true,
minify: false,
generateDts: true,
};
/**
* Build for Node.js environment
*/
async function buildNode() {
console.log("🔨 Building for Node.js...");
const startTime = Date.now();
const runNode = createBuildRunner({
...sharedConfig,
buildOptions: {
entrypoints: [`${TS_SRC}/index.node.ts`, `${TS_SRC}/roles.ts`],
outdir: "dist/node",
target: "node",
format: "esm",
external: nodeExternals,
sourcemap: true,
minify: false,
generateDts: false, // We'll generate declarations separately for all entry points
skipClean: true,
selfPackageName: "@elizaos/core", // Exclude self from externals to avoid self-referential imports
},
});
await runNode();
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`✅ Node.js build complete in ${duration}s`);
}
/**
* Build for browser environment
*/
async function buildBrowser() {
console.log("🌐 Building for Browser...");
const startTime = Date.now();
const runBrowser = createBuildRunner({
...sharedConfig,
buildOptions: {
entrypoints: [`${TS_SRC}/index.browser.ts`, `${TS_SRC}/roles.ts`],
outdir: "dist/browser",
// Use the Node target so `node:*` imports bundle without broken browser polyfills.
// The dashboard/Vite shell still aliases `node:*` where the bundle runs in the browser.
target: "node",
format: "esm",
external: browserExternals,
sourcemap: true,
// Bun 1.4 can emit invalid ESM for this large barrel when identifier
// minification drops declarations that remain in the final export list.
// App/example bundlers minify their final browser assets, so keep this
// package artifact readable and valid.
minify: false,
generateDts: false, // Use the same .d.ts files from Node build
skipClean: true,
plugins: [],
selfPackageName: "@elizaos/core", // Exclude self from externals to avoid self-referential imports
},
});
await runBrowser();
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`✅ Browser build complete in ${duration}s`);
}
/**
* Build for edge runtimes (Vercel Edge, Cloudflare Workers, Deno Deploy)
*/
async function buildEdge() {
console.log("⚡ Building for Edge...");
const startTime = Date.now();
const runEdge = createBuildRunner({
...sharedConfig,
buildOptions: {
entrypoints: [`${TS_SRC}/index.edge.ts`],
outdir: "dist/edge",
target: "node",
format: "esm",
external: browserExternals,
sourcemap: true,
minify: false,
generateDts: false,
skipClean: true,
selfPackageName: "@elizaos/core",
},
});
await runEdge();
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`✅ Edge build complete in ${duration}s`);
}
/**
* Build testing module (Node.js only)
*/
async function buildTesting() {
console.log("🧪 Building testing module...");
const startTime = Date.now();
const runTesting = createBuildRunner({
...sharedConfig,
buildOptions: {
entrypoints: [
`${TS_SRC}/testing/index.ts`,
`${TS_SRC}/testing/live-provider.ts`,
],
outdir: "dist/testing",
target: "node",
format: "esm",
external: [...nodeExternals, "@elizaos/plugin-sql"],
sourcemap: true,
minify: false,
generateDts: false,
skipClean: true,
selfPackageName: "@elizaos/core", // Exclude self from externals to avoid self-referential imports
},
});
await runTesting();
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`✅ Testing module build complete in ${duration}s`);
}
async function buildNodeOnly() {
console.log("🚀 Starting Node-only build process for @elizaos/core");
const totalStart = Date.now();
const skipTesting = process.argv.includes("--skip-testing");
const tasks: Array<Promise<void>> = [buildNode()];
if (!skipTesting) tasks.push(buildTesting());
await Promise.all(tasks);
await generateTypeScriptDeclarations();
const totalDuration = ((Date.now() - totalStart) / 1000).toFixed(2);
console.log(`\n🎉 Node-only build complete in ${totalDuration}s`);
}
/**
* Build for both targets
*/
async function buildAll() {
console.log("🚀 Starting dual build process for @elizaos/core");
const totalStart = Date.now();
// Build JS in parallel first
await Promise.all([buildNode(), buildBrowser(), buildEdge(), buildTesting()]);
// Generate TypeScript declarations AFTER JS builds complete
// This prevents race conditions where buildNode() might clean dist/node
// after generateTypeScriptDeclarations() creates the index.d.ts file
await generateTypeScriptDeclarations();
const totalDuration = ((Date.now() - totalStart) / 1000).toFixed(2);
console.log(`\n🎉 All builds complete in ${totalDuration}s`);
}
/**
* Rewrite relative module specifiers in emitted `.d.ts` files so they carry
* explicit `.js` extensions.
*
* tsc is run with `moduleResolution: "bundler"` for declarations (so internal
* source does not need to write extensions), but that leaves barrel re-exports
* like `export * from "./utils/state-dir"` in the emitted `.d.ts` files.
* External consumers compiled under `moduleResolution: "nodenext"` (the
* package's own `tsconfig.base.json` default) cannot resolve those — the
* symbol set becomes invisible, which is why downstream packages such as
* `@elizaos/skills` lost access to `resolveStateDir` and had to keep an inline
* copy.
*
* This pass walks `dist/**\/*.d.ts`, finds relative `import`/`export`
* specifiers, and rewrites them:
* - `"./foo"` → `"./foo.js"`
* - `"./foo.ts"` → `"./foo.js"`
* - `"./foo/index"` → `"./foo/index.js"`
* - `"./foo.js"` → unchanged
* - `"./foo.json"` → unchanged (non-script asset)
* Bare-directory specifiers (e.g. `"./foo"` where `foo/` is a directory) are
* rewritten to `"./foo/index.js"` so NodeNext can follow them.
*/
async function fixDtsExtensions(rootDir: string): Promise<void> {
const path = await import("node:path");
const fs = await import("node:fs/promises");
const walk = async (dir: string): Promise<string[]> => {
const out: string[] = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...(await walk(full)));
} else if (entry.isFile() && full.endsWith(".d.ts")) {
out.push(full);
}
}
return out;
};
// Patterns that capture `from "..."` and `import("...")` and
// `export * from "..."` style specifiers. We rewrite only the specifier
// content, preserving the surrounding syntax.
const specifierRegex =
/(\bfrom\s*['"]|\bimport\s*\(\s*['"])(\.\.?\/[^'"]+)(['"])/g;
const rewriteSpecifier = async (
fileDir: string,
spec: string,
): Promise<string> => {
// Already has a terminal script/asset extension — leave alone.
if (/\.(js|mjs|cjs|json)$/.test(spec)) {
return spec;
}
// TypeScript source extension leaked into emitted d.ts — rewrite to .js.
if (/\.tsx?$/.test(spec)) {
return spec.replace(/\.tsx?$/, ".js");
}
// `./foo.d.ts` style — rewrite to `.js`.
if (/\.d\.ts$/.test(spec)) {
return spec.replace(/\.d\.ts$/, ".js");
}
// No extension. Prefer a sibling `.js`/`.d.ts` if one exists — TypeScript
// resolves `utils.ts` over `utils/index.ts` when both are present, so
// after emit we must mirror that choice. We check `.d.ts` too because
// the runtime bundle may inline JS (so no sibling `.js` is emitted) but
// declarations are still written per file.
const resolved = path.resolve(fileDir, spec);
const siblingExists = await Promise.any([
fs.stat(`${resolved}.js`).then((s) => s.isFile()),
fs.stat(`${resolved}.d.ts`).then((s) => s.isFile()),
]).catch(() => false);
if (siblingExists) {
return `${spec}.js`;
}
const dirStat = await fs
.stat(resolved)
.then((s) => s.isDirectory())
.catch(() => false);
return dirStat ? `${spec}/index.js` : `${spec}.js`;
};
const files = await walk(rootDir);
let rewrittenFiles = 0;
let rewrittenSpecifiers = 0;
const readDtsWithRetry = async (file: string): Promise<string> => {
let last: Error | undefined;
for (let attempt = 0; attempt < 5; attempt++) {
try {
return await fs.readFile(file, "utf8");
} catch (e) {
const err = e as NodeJS.ErrnoException;
last = err instanceof Error ? err : new Error(String(e));
if (err.code === "ENOENT" && attempt < 4) {
await new Promise((r) => setTimeout(r, 30 * (attempt + 1)));
continue;
}
throw last;
}
}
throw last ?? new Error(`Failed to read ${file}`);
};
for (const file of files) {
const src = await readDtsWithRetry(file);
const fileDir = path.dirname(file);
const matches: Array<{ start: number; end: number; replacement: string }> =
[];
// Collect matches with indices — we rewrite in a second pass because
// `rewriteSpecifier` is async.
for (const m of src.matchAll(specifierRegex)) {
const [, prefix, spec, suffix] = m;
const matchStart = m.index;
const newSpec = await rewriteSpecifier(fileDir, spec);
if (newSpec === spec) continue;
matches.push({
start: matchStart,
end: matchStart + prefix.length + spec.length + suffix.length,
replacement: `${prefix}${newSpec}${suffix}`,
});
}
if (matches.length === 0) continue;
// Apply replacements right-to-left to keep earlier indices stable.
let patched = src;
for (let i = matches.length - 1; i >= 0; i--) {
const { start, end, replacement } = matches[i];
patched = patched.slice(0, start) + replacement + patched.slice(end);
}
await fs.writeFile(file, patched, "utf8");
rewrittenFiles++;
rewrittenSpecifiers += matches.length;
}
console.log(
` Rewrote ${rewrittenSpecifiers} relative specifier(s) in ${rewrittenFiles} .d.ts file(s) for NodeNext ESM`,
);
}
/**
* Generate TypeScript declarations for all entry points
*/
async function generateTypeScriptDeclarations() {
const fs = await import("node:fs/promises");
const { $ } = await import("bun");
console.log("📝 Generating TypeScript declarations...");
const startTime = Date.now();
// Generate TypeScript declarations using tsc
console.log(" Compiling TypeScript declarations...");
await $`${resolveTscBin()} --project tsconfig.declarations.json`;
// Post-process: add `.js` extensions to all relative specifiers so external
// consumers compiled under `moduleResolution: "nodenext"` can resolve them.
await fixDtsExtensions("dist");
// Ensure directories exist for conditional exports
await fs.mkdir("dist/node", { recursive: true });
await fs.mkdir("dist/browser", { recursive: true });
await fs.mkdir("dist/edge", { recursive: true });
// Create re-export files for conditional exports structure
// dist/node/index.d.ts - points to the Node.js entry point
// Note: Use .js extension for NodeNext module resolution compatibility
await fs.writeFile(
"dist/node/index.d.ts",
`// Type definitions for @elizaos/core (Node.js)\nexport * from '../index.node.js';\n`,
);
// dist/browser/index.d.ts - points to the browser entry point
await fs.writeFile(
"dist/browser/index.d.ts",
`// Type definitions for @elizaos/core (Browser)\nexport * from '../index.browser.js';\n`,
);
// dist/edge/index.d.ts - points to the edge entry point
await fs.writeFile(
"dist/edge/index.d.ts",
`// Type definitions for @elizaos/core (Edge)\nexport * from './index.edge.js';\n`,
);
// Create main index.js for runtime fallback (when conditional exports don't match)
await fs.writeFile(
"dist/index.js",
`// Main entry point fallback for @elizaos/core\nexport * from './node/index.node.js';\n`,
);
// Some tooling (including Bun in certain situations) may attempt to follow the
// "dist/index.d.ts -> ./index.node" re-export at runtime. Provide explicit JS
// entrypoints so resolution always lands on real JS modules.
await fs.writeFile(
"dist/index.node.js",
`// Node entry point (explicit)\nexport * from './node/index.node.js';\n`,
);
await fs.writeFile(
"dist/index.browser.js",
`// Browser entry point (explicit)\nexport * from './browser/index.browser.js';\n`,
);
await fs.writeFile(
"dist/roles.js",
`// Roles subpath entry point (explicit)\nexport * from './node/roles.js';\n`,
);
// Create main index.d.ts to re-export all types from node build
// This ensures TypeScript resolves all exports when using moduleResolution: bundler
// Note: Use .js extension for NodeNext module resolution compatibility
await fs.writeFile(
"dist/index.d.ts",
`// Type definitions for @elizaos/core\n// Re-exports all types from the Node.js entry point\nexport * from './index.node.js';\n`,
);
// Ensure testing module directory and declarations exist
await fs.mkdir("dist/testing", { recursive: true });
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`✅ TypeScript declarations generated in ${duration}s`);
}
if (import.meta.main) {
const isNodeOnly = process.argv.includes("--node-only");
const build = isNodeOnly ? buildNodeOnly : buildAll;
withCoreBuildLock(build).catch((error) => {
console.error("Build script error:", error);
process.exit(1);
});
}