mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-10 13:51:45 +00:00
copy-bundled-plugins.cjs compiled and rewrote plugin backend TS in place, copied it to dist/plugins, but never deleted the .js it had just emitted next to the .ts sources. Those files shadowed the real source for vitest/tsx and had import paths rewritten for the dist layout, so tests failed with "Cannot find module" once a local build had run. Affects docker, host-metrics and ai, not just this one.
180 lines
6.7 KiB
JavaScript
180 lines
6.7 KiB
JavaScript
/**
|
|
* Compiles plugin backend TypeScript, then copies plugins/ into dist/plugins
|
|
* so bundled first-party plugins ship with a built server.
|
|
*
|
|
* Most plugin backend entries are hand-written .mjs plus a manifest.json --
|
|
* tsc only emits .ts, so without a copy step the plugins directory simply
|
|
* would not exist in dist and the loader would find nothing.
|
|
*
|
|
* docker and host-metrics are the exception: their backends are real
|
|
* TypeScript, physically relocated from src/backend/hosts/docker/ and
|
|
* src/backend/hosts/metrics/ rather than kept as hand-written JS, because
|
|
* each is thousands of lines of typed SSH/session logic that is not worth
|
|
* hand-transpiling. tsconfig.plugins.json compiles each plugin backend's
|
|
* TypeScript to a sibling .js next to its source. Their imports into core
|
|
* (e.g. "../../../src/backend/utils/logger.js") are written relative to the
|
|
* TypeScript SOURCE tree so tsc can type-check them, since plugins/ is not
|
|
* under tsconfig.node.json's rootDir and cannot be added to it without
|
|
* changing every existing dist/backend/backend/... path in the codebase.
|
|
* That means the emitted .js still points at src/backend/, which does not
|
|
* exist in a built server -- only its compiled counterpart at
|
|
* dist/backend/backend/ does. rewriteCoreImports() below corrects that one
|
|
* prefix after compilation, for every plugins/*\/backend directory tsc
|
|
* compiled, before the directory is copied into dist/.
|
|
*
|
|
* getBundledPluginsDir() in src/backend/plugins/paths.ts resolves
|
|
* dist/backend/backend/plugins -> dist/plugins, which is where this writes.
|
|
*/
|
|
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { execSync } = require("node:child_process");
|
|
|
|
const root = path.resolve(__dirname, "..");
|
|
const source = path.join(root, "plugins");
|
|
const destination = path.join(root, "dist", "plugins");
|
|
|
|
if (!fs.existsSync(source)) {
|
|
console.log("No plugins/ directory, nothing to bundle.");
|
|
process.exit(0);
|
|
}
|
|
|
|
const SRC_BACKEND_PREFIX = "../../../src/backend/";
|
|
const COMPILED_BACKEND_PREFIX = "../../../backend/backend/";
|
|
|
|
function compilePluginBackends() {
|
|
execSync("npx tsc -p tsconfig.plugins.json", {
|
|
cwd: root,
|
|
stdio: "inherit",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Rewrites the compiled plugin JS's imports into core from the source-tree
|
|
* path tsc needed to resolve them, to the compiled-output path they need to
|
|
* resolve at runtime. See the module comment above for why this exists.
|
|
*/
|
|
function rewriteCoreImports(dir) {
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const entryPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
rewriteCoreImports(entryPath);
|
|
continue;
|
|
}
|
|
if (!entry.name.endsWith(".js")) continue;
|
|
|
|
const original = fs.readFileSync(entryPath, "utf8");
|
|
if (!original.includes(SRC_BACKEND_PREFIX)) continue;
|
|
|
|
const rewritten = original
|
|
.split(SRC_BACKEND_PREFIX)
|
|
.join(COMPILED_BACKEND_PREFIX);
|
|
fs.writeFileSync(entryPath, rewritten);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* tsconfig.plugins.json has rootDir/outDir "." so the docker plugin's .js
|
|
* lands beside its own .ts, but tsc also pulls every file the plugin
|
|
* imports into the same program for type-checking -- core utils, repository
|
|
* factories, src/types -- and stray-emits a sibling .js next to each of
|
|
* those .ts files too, even though none of that code is actually compiled
|
|
* by this pass (only referenced for types/signatures). Those stray files
|
|
* are not part of the normal build and must not leak into dist/ or the
|
|
* working tree, so anything under src/ that is a .js with no git history
|
|
* and a same-named .ts sibling gets removed after every compile.
|
|
*/
|
|
function isGitTracked(filePath) {
|
|
try {
|
|
execSync(`git ls-files --error-unmatch "${filePath}"`, {
|
|
cwd: root,
|
|
stdio: "ignore",
|
|
});
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function removeStrayEmits(dir) {
|
|
if (!fs.existsSync(dir)) return;
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const entryPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
removeStrayEmits(entryPath);
|
|
continue;
|
|
}
|
|
if (!entry.name.endsWith(".js")) continue;
|
|
const tsSibling = entryPath.slice(0, -".js".length) + ".ts";
|
|
// Never touch a tracked file, even if some future .ts/.js pair
|
|
// legitimately coexists -- only remove what is both untracked and has a
|
|
// .ts sibling, which is what a stray tsc emit always looks like.
|
|
if (fs.existsSync(tsSibling) && !isGitTracked(entryPath)) {
|
|
fs.rmSync(entryPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Every plugin directory with a backend/ that tsc actually compiled (i.e. it
|
|
* has a .ts file in it, not just a hand-written index.mjs). Scanned rather
|
|
* than hardcoded so a future plugin only needs a tsconfig.plugins.json entry,
|
|
* not a second call site here.
|
|
*/
|
|
function compiledPluginBackendDirs() {
|
|
return fs
|
|
.readdirSync(source, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => path.join(source, entry.name, "backend"))
|
|
.filter(
|
|
(dir) =>
|
|
fs.existsSync(dir) &&
|
|
fs.readdirSync(dir).some((name) => name.endsWith(".ts")),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Deletes every .js file tsc just emitted directly under a plugin's own
|
|
* backend/ directory (recursively), leaving its .ts siblings untouched.
|
|
*
|
|
* Without this, the compiled .js sits next to the .ts in plugins/ after every
|
|
* build, shadowing the source when vitest/tsx resolve an import -- and since
|
|
* those .js files were rewritten for the dist/ layout (see rewriteCoreImports),
|
|
* their relative paths are wrong at the plugins/ depth, so anything importing
|
|
* them from a test fails with "Cannot find module". .gitignore already
|
|
* expects plugins/*\/backend/**\/*.js to never be tracked; this is what keeps
|
|
* the working tree matching that after a local build, the same way dist/ is
|
|
* expected to hold the only copy of compiled plugin output.
|
|
*/
|
|
function removeCompiledPluginEmits(dir) {
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const entryPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
removeCompiledPluginEmits(entryPath);
|
|
continue;
|
|
}
|
|
if (entry.name.endsWith(".js")) fs.rmSync(entryPath);
|
|
}
|
|
}
|
|
|
|
compilePluginBackends();
|
|
const compiledDirs = compiledPluginBackendDirs();
|
|
for (const dir of compiledDirs) {
|
|
rewriteCoreImports(dir);
|
|
}
|
|
removeStrayEmits(path.join(root, "src"));
|
|
|
|
fs.rmSync(destination, { recursive: true, force: true });
|
|
fs.cpSync(source, destination, { recursive: true });
|
|
|
|
for (const dir of compiledDirs) {
|
|
removeCompiledPluginEmits(dir);
|
|
}
|
|
|
|
const bundled = fs
|
|
.readdirSync(destination, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name);
|
|
|
|
console.log(`Bundled ${bundled.length} plugin(s): ${bundled.join(", ")}`);
|