#!/usr/bin/env node import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; const logLevel = process.env.OPENCLAW_BUILD_VERBOSE ? "info" : "warn"; const extraArgs = process.argv.slice(2); const INEFFECTIVE_DYNAMIC_IMPORT_RE = /\[INEFFECTIVE_DYNAMIC_IMPORT\]/; const UNRESOLVED_IMPORT_RE = /\[UNRESOLVED_IMPORT\]/; const ANSI_ESCAPE_RE = new RegExp(String.raw`\u001B\[[0-9;]*m`, "g"); function removeDistPluginNodeModulesSymlinks(rootDir) { const extensionsDir = path.join(rootDir, "extensions"); if (!fs.existsSync(extensionsDir)) { return; } for (const dirent of fs.readdirSync(extensionsDir, { withFileTypes: true })) { if (!dirent.isDirectory()) { continue; } const nodeModulesPath = path.join(extensionsDir, dirent.name, "node_modules"); try { if (fs.lstatSync(nodeModulesPath).isSymbolicLink()) { fs.rmSync(nodeModulesPath, { force: true, recursive: true }); } } catch { // Skip missing or unreadable paths so the build can proceed. } } } function pruneStaleRuntimeSymlinks() { const cwd = process.cwd(); // runtime-postbuild stages plugin-owned node_modules into dist/ and links the // dist-runtime overlay back to that tree. Remove only those symlinks up front // so tsdown's clean step cannot traverse stale runtime overlays on rebuilds. removeDistPluginNodeModulesSymlinks(path.join(cwd, "dist")); removeDistPluginNodeModulesSymlinks(path.join(cwd, "dist-runtime")); } pruneStaleRuntimeSymlinks(); function findFatalUnresolvedImport(lines) { for (const line of lines) { if (!UNRESOLVED_IMPORT_RE.test(line)) { continue; } const normalizedLine = line.replace(ANSI_ESCAPE_RE, ""); if (!normalizedLine.includes("extensions/")) { return normalizedLine; } } return null; } const result = spawnSync( "pnpm", ["exec", "tsdown", "--config-loader", "unrun", "--logLevel", logLevel, ...extraArgs], { encoding: "utf8", stdio: "pipe", shell: process.platform === "win32", }, ); const stdout = result.stdout ?? ""; const stderr = result.stderr ?? ""; if (stdout) { process.stdout.write(stdout); } if (stderr) { process.stderr.write(stderr); } if (result.status === 0 && INEFFECTIVE_DYNAMIC_IMPORT_RE.test(`${stdout}\n${stderr}`)) { console.error( "Build emitted [INEFFECTIVE_DYNAMIC_IMPORT]. Replace transparent runtime re-export facades with real runtime boundaries.", ); process.exit(1); } const fatalUnresolvedImport = result.status === 0 ? findFatalUnresolvedImport(`${stdout}\n${stderr}`.split("\n")) : null; if (fatalUnresolvedImport) { console.error(`Build emitted [UNRESOLVED_IMPORT] outside extensions: ${fatalUnresolvedImport}`); process.exit(1); } if (typeof result.status === "number") { process.exit(result.status); } process.exit(1);