243 lines
8.1 KiB
JavaScript
243 lines
8.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Compile all TypeScript source + test files to dist-test/ using esbuild.
|
|
* Run compiled JS directly with node --test (no per-file TS overhead).
|
|
*
|
|
* Usage: node scripts/compile-tests.mjs
|
|
*/
|
|
|
|
import { symlinkSync } from "node:fs";
|
|
import { cp, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
const ROOT = join(__dirname, "..");
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const esbuild = require(join(ROOT, "node_modules/esbuild"));
|
|
|
|
// Recursively collect files by extension (skip node_modules, templates, etc.)
|
|
// Directories to skip during file collection
|
|
const SKIP_DIRS = new Set([
|
|
"node_modules",
|
|
"templates",
|
|
"__tests__",
|
|
"integration",
|
|
]);
|
|
|
|
async function collectFiles(dir, exts = [".ts", ".mjs"]) {
|
|
const results = [];
|
|
let entries;
|
|
try {
|
|
entries = await readdir(dir, { withFileTypes: true });
|
|
} catch {
|
|
return results;
|
|
}
|
|
for (const entry of entries) {
|
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
const full = join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
results.push(...(await collectFiles(full, exts)));
|
|
} else if (
|
|
exts.some((ext) => entry.name.endsWith(ext)) &&
|
|
!entry.name.endsWith(".d.ts")
|
|
) {
|
|
results.push(full);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
// Dirs to skip when copying assets (node_modules are never useful in dist-test)
|
|
const ASSET_SKIP_DIRS = new Set(["node_modules", "__tests__", "integration"]);
|
|
|
|
/**
|
|
* Recursively copy files from srcDir to destDir.
|
|
* Skips node_modules only. Copies everything: .ts/.tsx originals (for jiti),
|
|
* .mjs helpers, .md/.yaml/.json assets, etc.
|
|
* esbuild compiled .js output already lands in dist-test, so we just
|
|
* overlay the asset files on top.
|
|
*/
|
|
async function copyAssets(srcDir, destDir) {
|
|
let entries;
|
|
try {
|
|
entries = await readdir(srcDir, { withFileTypes: true });
|
|
} catch {
|
|
return; // directory doesn't exist, nothing to copy
|
|
}
|
|
for (const entry of entries) {
|
|
if (ASSET_SKIP_DIRS.has(entry.name)) continue;
|
|
const srcPath = join(srcDir, entry.name);
|
|
const destPath = join(destDir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
await copyAssets(srcPath, destPath);
|
|
} else {
|
|
await mkdir(destDir, { recursive: true });
|
|
await cp(srcPath, destPath, { force: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const start = Date.now();
|
|
|
|
// Collect entry points from src/ and packages/*/src/
|
|
const srcFiles = await collectFiles(join(ROOT, "src"));
|
|
|
|
const packagesDir = join(ROOT, "packages");
|
|
const pkgEntries = await readdir(packagesDir, { withFileTypes: true });
|
|
const packageFiles = [];
|
|
for (const entry of pkgEntries) {
|
|
if (!entry.isDirectory()) continue;
|
|
const pkgSrc = join(packagesDir, entry.name, "src");
|
|
packageFiles.push(...(await collectFiles(pkgSrc)));
|
|
}
|
|
|
|
// Also compile web/lib/ — some tests import from ../../web/lib/
|
|
const webLibFiles = await collectFiles(join(ROOT, "web", "lib"));
|
|
|
|
const entryPoints = [...srcFiles, ...packageFiles, ...webLibFiles];
|
|
console.log(`Compiling ${entryPoints.length} files to dist-test/...`);
|
|
|
|
// bundle:false transforms TypeScript but keeps import specifiers verbatim.
|
|
// We post-process the output to rewrite .ts → .js in import strings.
|
|
await esbuild.build({
|
|
entryPoints,
|
|
outdir: join(ROOT, "dist-test"),
|
|
outbase: ROOT,
|
|
bundle: false,
|
|
format: "esm",
|
|
platform: "node",
|
|
target: "node24",
|
|
sourcemap: "inline",
|
|
packages: "external",
|
|
logLevel: "warning",
|
|
});
|
|
|
|
// Copy non-compiled assets from src/ to dist-test/src/ maintaining structure.
|
|
// Tests use import.meta.url to resolve sibling .md, .yaml, .json, .ts etc.
|
|
// Also copy original .ts files — jiti-based imports load .ts source directly.
|
|
const srcDir = join(ROOT, "src");
|
|
const distSrcDir = join(ROOT, "dist-test", "src");
|
|
await copyAssets(srcDir, distSrcDir);
|
|
console.log("Copied non-TS assets and .ts source files to dist-test/src/");
|
|
|
|
// Copy packages/*/src/ assets as well
|
|
for (const entry of pkgEntries) {
|
|
if (!entry.isDirectory()) continue;
|
|
const pkgSrc = join(packagesDir, entry.name, "src");
|
|
const pkgDistSrc = join(ROOT, "dist-test", "packages", entry.name, "src");
|
|
await copyAssets(pkgSrc, pkgDistSrc);
|
|
}
|
|
|
|
// Copy web/lib/ assets (tests import from ../../web/lib/ relative to dist-test/src/tests/)
|
|
await copyAssets(
|
|
join(ROOT, "web", "lib"),
|
|
join(ROOT, "dist-test", "web", "lib"),
|
|
);
|
|
|
|
// Copy web/components/ assets (xterm-theme test reads shell-terminal.tsx via import.meta.dirname)
|
|
await copyAssets(
|
|
join(ROOT, "web", "components"),
|
|
join(ROOT, "dist-test", "web", "components"),
|
|
);
|
|
|
|
// Copy scripts/ non-TS files (.cjs etc) — some tests require() scripts directly
|
|
await copyAssets(join(ROOT, "scripts"), join(ROOT, "dist-test", "scripts"));
|
|
|
|
// Copy root package.json — some tests read it to check version/engines fields
|
|
await cp(
|
|
join(ROOT, "package.json"),
|
|
join(ROOT, "dist-test", "package.json"),
|
|
{ force: true },
|
|
);
|
|
|
|
// Copy root dist/ into dist-test/dist/ — some tests compute projectRoot as
|
|
// 3 levels up from dist-test/src/tests/ which lands at dist-test/, then
|
|
// import from dist/mcp-server.js etc.
|
|
const rootDistDir = join(ROOT, "dist");
|
|
const distTestDistDir = join(ROOT, "dist-test", "dist");
|
|
await copyAssets(rootDistDir, distTestDistDir);
|
|
|
|
// Post-process: rewrite .ts import specifiers to .js in all compiled JS files.
|
|
// esbuild with bundle:false preserves original specifiers; Node can't load .ts.
|
|
const compiledJsFiles = await collectFiles(join(ROOT, "dist-test"), [".js"]);
|
|
// Regex matches .ts in from/import() strings but not sourceMappingURL comments
|
|
const tsImportRe = /(from\s+["'])(\.\.?\/[^"']*?)\.ts(["'])/g;
|
|
const tsDynImportRe = /(import\(["'])(\.\.?\/[^"']*?)\.ts(["'])\)/g;
|
|
|
|
let rewritten = 0;
|
|
await Promise.all(
|
|
compiledJsFiles.map(async (file) => {
|
|
const src = await readFile(file, "utf-8");
|
|
const out = src
|
|
.replace(tsImportRe, (_, a, b, c) => `${a}${b}.js${c}`)
|
|
.replace(tsDynImportRe, (_, a, b, c) => `${a}${b}.js${c})`);
|
|
if (out !== src) {
|
|
await writeFile(file, out, "utf-8");
|
|
rewritten++;
|
|
}
|
|
}),
|
|
);
|
|
if (rewritten > 0) {
|
|
console.log(`Rewrote .ts → .js imports in ${rewritten} files`);
|
|
}
|
|
|
|
// Remove stale compiled test files: dist-test entries whose source no longer exists
|
|
// in a non-integration source directory (e.g. test moved to integration/).
|
|
// Only cleans *.test.js and *.test.ts files to avoid touching non-test outputs.
|
|
const { rm } = await import("node:fs/promises");
|
|
const { existsSync } = await import("node:fs");
|
|
const testDirsToClean = [
|
|
[join(ROOT, "dist-test", "src", "tests"), join(ROOT, "src", "tests")],
|
|
[
|
|
join(ROOT, "dist-test", "src", "resources", "extensions", "sf", "tests"),
|
|
join(ROOT, "src", "resources", "extensions", "sf", "tests"),
|
|
],
|
|
];
|
|
let staleCleaned = 0;
|
|
for (const [distDir, srcDir] of testDirsToClean) {
|
|
let distEntries;
|
|
try {
|
|
distEntries = await readdir(distDir, { withFileTypes: true });
|
|
} catch {
|
|
continue;
|
|
}
|
|
for (const entry of distEntries) {
|
|
if (!entry.isFile()) continue;
|
|
if (!entry.name.match(/\.test\.(js|ts)$/)) continue;
|
|
const stem = entry.name.replace(/\.(js|ts)$/, "");
|
|
// Source could be .ts or .mjs (esbuild compiles both to .js)
|
|
const hasTsSrc = existsSync(join(srcDir, stem + ".ts"));
|
|
const hasMjsSrc = existsSync(join(srcDir, stem + ".mjs"));
|
|
if (!hasTsSrc && !hasMjsSrc) {
|
|
await rm(join(distDir, entry.name));
|
|
staleCleaned++;
|
|
}
|
|
}
|
|
}
|
|
if (staleCleaned > 0) {
|
|
console.log(
|
|
`Removed ${staleCleaned} stale compiled test files from dist-test/`,
|
|
);
|
|
}
|
|
|
|
// Ensure dist-test/node_modules exists so resource-loader.ts (which computes
|
|
// packageRoot from import.meta.url) resolves sfNodeModules to a real path.
|
|
// Without this, initResources creates dangling symlinks in test environments.
|
|
const distNodeModules = join(ROOT, "dist-test", "node_modules");
|
|
if (!existsSync(distNodeModules)) {
|
|
symlinkSync(join(ROOT, "node_modules"), distNodeModules);
|
|
}
|
|
|
|
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
|
|
console.log(`Done in ${elapsed}s`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|