build: 新增 vite 构建流程

This commit is contained in:
claude-code-best
2026-04-16 12:39:19 +08:00
parent 8169b96250
commit 3470783ced
7 changed files with 380 additions and 2 deletions

90
scripts/post-build.ts Normal file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env bun
/**
* Post-build processing for Vite build output.
*
* 1. Patch globalThis.Bun destructuring in third-party deps for Node.js compat
* 2. Copy native addon files
* 3. Bundle standalone scripts (download-ripgrep)
* 4. Generate dual entry points (cli-bun.js, cli-node.js)
*/
import { readdir, readFile, writeFile, cp } from "node:fs/promises";
import { chmodSync } from "node:fs";
import { join } from "node:path";
import { execSync } from "node:child_process";
const outdir = "dist";
async function postBuild() {
// Step 1: Patch globalThis.Bun destructuring from third-party deps
const files = await readdir(outdir, { recursive: true });
const BUN_DESTRUCTURE = /var \{([^}]+)\} = globalThis\.Bun;?/g;
const BUN_DESTRUCTURE_SAFE =
'var {$1} = typeof globalThis.Bun !== "undefined" ? globalThis.Bun : {};';
let bunPatched = 0;
for (const file of files) {
const filePath = join(outdir, file);
if (typeof file !== "string" || !file.endsWith(".js")) continue;
const content = await readFile(filePath, "utf-8");
if (BUN_DESTRUCTURE.test(content)) {
await writeFile(
filePath,
content.replace(BUN_DESTRUCTURE, BUN_DESTRUCTURE_SAFE),
);
bunPatched++;
}
BUN_DESTRUCTURE.lastIndex = 0;
}
// Step 2: Copy native addon files
const vendorDir = join(outdir, "vendor", "audio-capture");
await cp("vendor/audio-capture", vendorDir, { recursive: true } as never);
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`);
// Step 3: Bundle standalone scripts via Bun.build (kept for simplicity)
try {
const { default: Bun } = await import("bun");
const rgScript = await Bun.build({
entrypoints: ["scripts/download-ripgrep.ts"],
outdir,
target: "node",
});
if (rgScript.success) {
console.log(`Bundled download-ripgrep script to ${outdir}/`);
} else {
console.warn("Failed to bundle download-ripgrep script (non-fatal)");
}
} catch {
// Bun not available — try esbuild fallback
try {
execSync(
`npx esbuild scripts/download-ripgrep.ts --bundle --platform=node --outfile=${outdir}/download-ripgrep.js --format=esm`,
{ stdio: "inherit" },
);
console.log(`Bundled download-ripgrep script via esbuild to ${outdir}/`);
} catch {
console.warn(
"Failed to bundle download-ripgrep script — skipping (non-fatal)",
);
}
}
// Step 4: Generate dual entry points
const cliBun = join(outdir, "cli-bun.js");
const cliNode = join(outdir, "cli-node.js");
await writeFile(cliBun, '#!/usr/bin/env bun\nimport "./cli.js"\n');
await writeFile(cliNode, '#!/usr/bin/env node\nimport "./cli.js"\n');
chmodSync(cliBun, 0o755);
chmodSync(cliNode, 0o755);
console.log(
`Post-build complete: patched ${bunPatched} Bun destructure, generated entry points`,
);
}
postBuild().catch((err) => {
console.error("Post-build failed:", err);
process.exit(1);
});

View File

@@ -0,0 +1,118 @@
import type { Plugin } from "rollup";
/**
* Default features that match the official CLI build.
* Additional features can be enabled via FEATURE_<NAME>=1 env vars.
*/
const DEFAULT_BUILD_FEATURES = [
"AGENT_TRIGGERS_REMOTE",
"CHICAGO_MCP",
"VOICE_MODE",
"SHOT_STATS",
"PROMPT_CACHE_BREAK_DETECTION",
"TOKEN_BUDGET",
// P0: local features
"AGENT_TRIGGERS",
"ULTRATHINK",
"BUILTIN_EXPLORE_PLAN_AGENTS",
"LODESTONE",
// P1: API-dependent features
"EXTRACT_MEMORIES",
"VERIFICATION_AGENT",
"KAIROS_BRIEF",
"AWAY_SUMMARY",
"ULTRAPLAN",
// P2: daemon + remote control server
"DAEMON",
// PR-package restored features
"WORKFLOW_SCRIPTS",
"HISTORY_SNIP",
"CONTEXT_COLLAPSE",
"MONITOR_TOOL",
"FORK_SUBAGENT",
"KAIROS",
"COORDINATOR_MODE",
"LAN_PIPES",
// P3: poor mode
"POOR",
];
/**
* Collect enabled feature flags from defaults + env vars.
*/
export function getEnabledFeatures(): Set<string> {
const envFeatures = Object.keys(process.env)
.filter((k) => k.startsWith("FEATURE_"))
.map((k) => k.replace("FEATURE_", ""));
return new Set([...DEFAULT_BUILD_FEATURES, ...envFeatures]);
}
// Regex to match feature('FLAG_NAME') calls with string literal arguments
const FEATURE_CALL_RE = /feature\s*\(\s*['"]([\w]+)['"]\s*\)/g;
/**
* Vite/Rollup plugin that replaces `feature('X')` calls with boolean literals
* at the transform stage, BEFORE the bundler resolves imports.
*
* This approach is necessary because some feature-gated code blocks contain
* require() calls to files that don't exist (e.g. hunter.js inside
* feature('REVIEW_ARTIFACT')). The bundler must see these as dead code
* (`if (false) { ... }`) before attempting import resolution.
*
* Also resolves `import { feature } from 'bun:bundle'` as a virtual module
* to prevent "module not found" errors.
*/
export default function featureFlagsPlugin(): Plugin {
const features = getEnabledFeatures();
const virtualModuleId = "bun:bundle";
const resolvedVirtualModuleId = "\0" + virtualModuleId;
return {
name: "feature-flags",
// Resolve bun:bundle as a virtual module (prevents "module not found")
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},
// Provide a stub export for bun:bundle (unused at runtime after transform)
load(id) {
if (id === resolvedVirtualModuleId) {
return "export function feature(name) { return false; }";
}
},
// Replace feature('X') calls with true/false literals at transform time,
// and transpile `using` declarations for Node.js compatibility.
transform(code, id) {
// Skip node_modules
if (id.includes("node_modules")) return null;
let modified = false;
// 1. Replace feature('X') calls with boolean literals
let matchCount = 0;
let transformed = code.replace(FEATURE_CALL_RE, (match, flagName) => {
matchCount++;
return features.has(flagName) ? "true" : "false";
});
if (matchCount > 0) modified = true;
// 2. Transpile `using _ = expr;` to `const _ = expr;` for Node.js compat.
// Node.js v22 does not support `using` declarations (Explicit Resource Management).
// Safe because: SLOW_OPERATION_LOGGING is not enabled, so slowLogging returns
// a no-op disposable whose [Symbol.dispose]() is empty.
if (transformed.includes("using _")) {
transformed = transformed.replace(/\busing\s+(_\w*)\s*=/g, "const $1 =");
modified = true;
}
if (!modified) return null;
return { code: transformed, map: null };
},
};
}

View File

@@ -0,0 +1,25 @@
import type { Plugin } from "rollup";
/**
* Rollup plugin that replaces `var __require = import.meta.require;`
* with a Node.js compatible version that falls back to createRequire
* when import.meta.require is not available (e.g. in Node.js runtime).
*
* This replicates the post-processing done in the original build.ts.
*/
export default function importMetaRequirePlugin(): Plugin {
return {
name: "import-meta-require",
renderChunk(code) {
const pattern = "var __require = import.meta.require;";
const replacement =
'var __require = typeof import.meta.require === "function" ? import.meta.require : (await import("module")).createRequire(import.meta.url);';
if (code.includes(pattern)) {
return code.replace(pattern, replacement);
}
return null;
},
};
}