Files
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
CI / Shell Format Check (push) Waiting to run
CI / Check Ruby (3.4) (push) Waiting to run
CI / CI Config (push) Waiting to run
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Blocked by required conditions
CI / Build on Node ${{ matrix.node }} (push) Blocked by required conditions
CI / Style Check (push) Waiting to run
CI / Generate Assets (push) Waiting to run
CI / Check Python (3.14) (push) Waiting to run
CI / Check Python (3.9) (push) Waiting to run
CI / Build Docs (push) Waiting to run
CI / Code Scan Action (push) Waiting to run
CI / Site tests (push) Waiting to run
CI / webui tests (push) Waiting to run
CI / Run Integration Tests (push) Waiting to run
CI / Run Smoke Tests (push) Waiting to run
CI / Go Tests (push) Waiting to run
CI / Share Test (push) Waiting to run
CI / Redteam (Production API) (push) Waiting to run
CI / Redteam (Staging API) (push) Waiting to run
CI / GitHub Actions Lint (push) Waiting to run
CI / Check Ruby (3.0) (push) Waiting to run
release-please / release-please (push) Waiting to run
release-please / build (push) Blocked by required conditions
release-please / publish-npm (push) Blocked by required conditions
release-please / publish-npm-backfill (push) Waiting to run
release-please / docker (push) Blocked by required conditions
release-please / publish-code-scan-action (push) Blocked by required conditions
release-please / attest-code-scan-action (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

321 lines
9.1 KiB
TypeScript

/**
* Post-build script that copies non-TypeScript assets to the dist directory.
*
* This script runs automatically after the TypeScript build (tsdown) completes.
* It handles:
* - HTML template files (all *.html in src/)
* - Python/Go/Ruby wrapper scripts for custom providers
* - Drizzle ORM migration files
* - ESM package.json marker
* - CLI executable permissions
*
* @module scripts/postbuild
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT = path.join(__dirname, '..');
const DIST = path.join(ROOT, 'dist');
const SRC = path.join(ROOT, 'src');
/**
* Wrapper types supported by the build.
* IMPORTANT: Must match WrapperType in src/esm.ts (used by getWrapperDir()).
* If you add a new wrapper type, update both files.
*/
const WRAPPER_TYPES = ['python', 'ruby', 'golang'] as const;
type WrapperType = (typeof WRAPPER_TYPES)[number];
/**
* Wrapper files for each language type.
* Maps wrapper type to the list of files that should be copied.
*/
const WRAPPER_FILES: Record<WrapperType, string[]> = {
python: ['wrapper.py', 'persistent_wrapper.py'],
ruby: ['wrapper.rb'],
golang: ['wrapper.go'],
};
/**
* Files/patterns to exclude when copying the drizzle directory.
*/
const DRIZZLE_EXCLUDE_PATTERNS = ['.md', 'CLAUDE', 'AGENTS'];
export function shouldCopyDrizzlePath(src: string): boolean {
const basename = path.basename(src);
return !DRIZZLE_EXCLUDE_PATTERNS.some(
(pattern) => basename.includes(pattern) || basename.endsWith(pattern),
);
}
/**
* Critical build outputs that must exist for the build to be valid.
*/
const REQUIRED_BUILD_OUTPUTS = [
'dist/src/entrypoint.js', // CLI entry (Node version check wrapper)
'dist/src/main.js', // CLI main module
'dist/src/contracts.js', // ESM contracts subpath
'dist/src/contracts.cjs', // CJS contracts subpath
'dist/src/contracts.d.ts', // ESM contracts declarations
'dist/src/contracts.d.cts', // CJS contracts declarations
'dist/src/index.js', // ESM library entry
'dist/src/index.cjs', // CJS library entry
'dist/src/server/index.js', // Server entry
];
interface CopyTask {
src: string;
dest: string;
recursive?: boolean;
filter?: (src: string) => boolean;
}
interface PostbuildResult {
success: boolean;
copied: string[];
errors: string[];
}
/**
* Logs a message to stdout with consistent formatting.
*/
function log(message: string): void {
console.log(`[postbuild] ${message}`);
}
/**
* Logs an error message to stderr with consistent formatting.
*/
function logError(message: string): void {
console.error(`[postbuild] ERROR: ${message}`);
}
/**
* Find all HTML files in src/ directory (non-recursive).
*/
function getHtmlFiles(): CopyTask[] {
try {
return fs
.readdirSync(SRC)
.filter((file) => file.endsWith('.html'))
.map((file) => ({
src: path.join(SRC, file),
dest: path.join(DIST, 'src', file),
}));
} catch (error) {
logError(`Failed to read src/ directory: ${error}`);
return [];
}
}
/**
* Generate copy tasks for all wrapper scripts.
* Uses WRAPPER_TYPES and WRAPPER_FILES to ensure consistency with src/esm.ts
*
* Wrapper files are copied to two locations:
* 1. dist/src/{python,ruby,golang}/ - for CLI builds (entrypoint.js, main.js)
* 2. dist/src/server/{python,ruby,golang}/ - for bundled server build (server/index.js)
*
* This is necessary because getWrapperDir() uses import.meta.url to determine
* the base directory. In the bundled server, import.meta.url points to
* dist/src/server/index.js, so wrapper files need to be at dist/src/server/{type}/.
*/
function getWrapperTasks(): CopyTask[] {
const tasks: CopyTask[] = [];
// Destinations for wrapper files:
// - dist/src/ for CLI (entrypoint.js, main.js use import.meta.url → dist/src/)
// - dist/src/server/ for bundled server (server/index.js uses import.meta.url → dist/src/server/)
const destBases = [path.join(DIST, 'src'), path.join(DIST, 'src', 'server')];
for (const wrapperType of WRAPPER_TYPES) {
const files = WRAPPER_FILES[wrapperType];
for (const file of files) {
for (const destBase of destBases) {
tasks.push({
src: path.join(SRC, wrapperType, file),
dest: path.join(destBase, wrapperType, file),
});
}
}
}
return tasks;
}
/**
* Get the drizzle migration copy task with exclusion filter.
*/
function getDrizzleTask(): CopyTask {
return {
src: path.join(ROOT, 'drizzle'),
dest: path.join(DIST, 'drizzle'),
recursive: true,
filter: shouldCopyDrizzlePath,
};
}
/**
* Get the proto files copy task for OTLP protobuf support.
*/
function getProtoTask(): CopyTask {
return {
src: path.join(SRC, 'tracing', 'proto'),
dest: path.join(DIST, 'src', 'tracing', 'proto'),
recursive: true,
};
}
/**
* Verify that all critical build outputs exist.
*/
function verifyBuildOutputs(): string[] {
const missing: string[] = [];
for (const outputPath of REQUIRED_BUILD_OUTPUTS) {
const fullPath = path.join(ROOT, outputPath);
if (!fs.existsSync(fullPath)) {
missing.push(outputPath);
}
}
return missing;
}
/**
* Clean destination directories before copying to prevent stale files.
* Only cleans specific subdirectories, not all of dist/.
*/
function cleanDestinations(_tasks: CopyTask[]): void {
// Clean wrapper directories (both at dist/src/ and dist/src/server/)
const wrapperBases = [path.join(DIST, 'src'), path.join(DIST, 'src', 'server')];
for (const base of wrapperBases) {
for (const wrapperType of WRAPPER_TYPES) {
const wrapperDest = path.join(base, wrapperType);
if (fs.existsSync(wrapperDest)) {
fs.rmSync(wrapperDest, { recursive: true, force: true });
}
}
}
// Clean drizzle directory
const drizzleDest = path.join(DIST, 'drizzle');
if (fs.existsSync(drizzleDest)) {
fs.rmSync(drizzleDest, { recursive: true, force: true });
}
}
/**
* Execute a single copy task.
*/
function executeCopyTask(task: CopyTask): { success: boolean; error?: string } {
try {
if (!fs.existsSync(task.src)) {
return { success: false, error: `Source not found: ${task.src.replace(ROOT, '.')}` };
}
fs.mkdirSync(path.dirname(task.dest), { recursive: true });
fs.cpSync(task.src, task.dest, {
recursive: task.recursive ?? false,
filter: task.filter,
});
return { success: true };
} catch (error) {
return { success: false, error: `Copy failed: ${error}` };
}
}
/**
* Main postbuild function.
*/
export function postbuild(): PostbuildResult {
const result: PostbuildResult = {
success: true,
copied: [],
errors: [],
};
log('Starting postbuild...');
// Verify tsdown produced the expected outputs first
const missingOutputs = verifyBuildOutputs();
if (missingOutputs.length > 0) {
for (const missing of missingOutputs) {
result.errors.push(`Missing build output: ${missing}`);
}
logError('tsdown build appears to have failed. Missing outputs:');
for (const missing of missingOutputs) {
logError(` - ${missing}`);
}
result.success = false;
return result;
}
// Gather all copy tasks
const copyTasks = [...getHtmlFiles(), ...getWrapperTasks(), getDrizzleTask(), getProtoTask()];
// Clean destinations to prevent stale files
cleanDestinations(copyTasks);
// Execute copy tasks
for (const task of copyTasks) {
const copyResult = executeCopyTask(task);
if (copyResult.success) {
const relativePath = task.dest.replace(ROOT, '.');
result.copied.push(relativePath);
log(`Copied: ${task.src.replace(ROOT, '.')} -> ${relativePath}`);
} else {
result.errors.push(copyResult.error!);
logError(copyResult.error!);
result.success = false;
}
}
// Create ESM package.json marker for dist/src
const distSrcPackageJson = path.join(DIST, 'src', 'package.json');
try {
fs.writeFileSync(distSrcPackageJson, JSON.stringify({ type: 'module' }, null, 2) + '\n');
log('Created: ./dist/src/package.json');
} catch (error) {
result.errors.push(`Failed to create ESM marker: ${error}`);
logError(`Failed to create ESM marker: ${error}`);
result.success = false;
}
// Make CLI executables (no-op on Windows, but doesn't hurt)
const cliExecutables = ['entrypoint.js', 'main.js'];
for (const executable of cliExecutables) {
const execPath = path.join(DIST, 'src', executable);
try {
fs.chmodSync(execPath, 0o755);
log(`Made executable: ./dist/src/${executable}`);
} catch (error) {
// chmod may fail on Windows - this is acceptable
log(`Note: chmod failed (expected on Windows): ${error}`);
}
}
if (result.success) {
log(`Postbuild complete. Copied ${result.copied.length} items.`);
} else {
logError(`Postbuild failed with ${result.errors.length} error(s).`);
}
return result;
}
// Run if executed directly (not when imported for testing)
const isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
if (isMainModule) {
const result = postbuild();
if (!result.success) {
process.exit(1);
}
}