mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: make each plugin an npm workspace built by a termix-plugin CLI
This commit is contained in:
1 parent
4758016f41
commit
5796186a1c
448 files changed
+3087
-2003
No files matched your search
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Builds every plugin workspace, then stages the result in dist/plugins.
|
||||
*
|
||||
* getBundledPluginsDir() in src/backend/plugins/paths.ts resolves
|
||||
* dist/backend/backend/plugins -> dist/plugins, which is where this writes.
|
||||
* Only what a server needs at runtime is copied: the manifest, the built
|
||||
* bundles, locales and migrations. Source and tests stay out of the image.
|
||||
*/
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const source = path.join(root, "plugins");
|
||||
const destination = path.join(root, "dist", "plugins");
|
||||
const cli = path.join(root, "packages", "plugin-sdk", "cli", "index.mjs");
|
||||
|
||||
const SHIPPED = ["manifest.json", "dist", "locales", "migrations", "README.md"];
|
||||
|
||||
if (!fs.existsSync(source)) {
|
||||
console.log("No plugins/ directory, nothing to bundle.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const pluginIds = fs
|
||||
.readdirSync(source, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.filter((entry) =>
|
||||
fs.existsSync(path.join(source, entry.name, "manifest.json")),
|
||||
)
|
||||
.map((entry) => entry.name);
|
||||
|
||||
fs.rmSync(destination, { recursive: true, force: true });
|
||||
|
||||
for (const id of pluginIds) {
|
||||
const pluginDir = path.join(source, id);
|
||||
|
||||
execFileSync(process.execPath, [cli, "build"], {
|
||||
cwd: pluginDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
const built = path.join(pluginDir, "dist", "backend.js");
|
||||
if (!fs.existsSync(built)) {
|
||||
throw new Error(`${id} produced no dist/backend.js`);
|
||||
}
|
||||
|
||||
const outDir = path.join(destination, id);
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
for (const entry of SHIPPED) {
|
||||
const from = path.join(pluginDir, entry);
|
||||
if (!fs.existsSync(from)) continue;
|
||||
fs.cpSync(from, path.join(outDir, entry), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Bundled ${pluginIds.length} plugin(s): ${pluginIds.join(", ")}`);
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Counts the import-boundary debt between core and plugins, and fails when it
|
||||
* grows.
|
||||
*
|
||||
* The eslint fence marks these directions as warnings because there is nowhere
|
||||
* for them to go yet (see packages/plugin-sdk/ARCHITECTURE.md). A warning does
|
||||
* not fail a build, so this script holds the line instead: every offender is
|
||||
* listed in plugin-boundary-allowlist.json, a new one fails, and an entry that
|
||||
* stopped being an offender fails too, so the list shrinks as B steps land
|
||||
* rather than rotting. D1 empties it and the eslint rules become errors.
|
||||
*/
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const allowlistPath = path.join(
|
||||
root,
|
||||
"scripts",
|
||||
"plugin-boundary-allowlist.json",
|
||||
);
|
||||
const norm = (p) => p.split(path.sep).join("/");
|
||||
|
||||
const DIRECTIONS = {
|
||||
// The shell importing plugin components. A7 empties this.
|
||||
"ui-to-plugin": {
|
||||
roots: ["src/ui"],
|
||||
skip: (rel) => rel.includes("/tests/"),
|
||||
pattern: /["'][^"']*\/plugins\/[a-z0-9-]+\/src\/(frontend|backend)\//,
|
||||
},
|
||||
// A plugin backend reaching core by relative path. D1 empties this.
|
||||
"plugin-to-core": {
|
||||
roots: ["plugins"],
|
||||
only: (rel) => /^plugins\/[a-z0-9-]+\/src\//.test(rel),
|
||||
pattern: /["'](?:\.\.\/)+src\/(backend|types)\//,
|
||||
},
|
||||
// One plugin importing another's source. B18 empties this.
|
||||
"plugin-to-plugin": {
|
||||
roots: ["plugins"],
|
||||
only: (rel) => /^plugins\/[a-z0-9-]+\/src\//.test(rel),
|
||||
pattern: /["'](?:\.\.\/)+(?!src\/)[a-z0-9-]+\/src\/(backend|frontend)\//,
|
||||
},
|
||||
};
|
||||
|
||||
function walk(dir, out = []) {
|
||||
if (!fs.existsSync(dir)) return out;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === "node_modules" || entry.name === "dist") continue;
|
||||
walk(full, out);
|
||||
} else if (/\.(ts|tsx|mjs)$/.test(entry.name)) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function offendersFor(name, config) {
|
||||
const found = new Set();
|
||||
for (const base of config.roots) {
|
||||
for (const file of walk(path.join(root, base))) {
|
||||
const rel = norm(path.relative(root, file));
|
||||
if (config.only && !config.only(rel)) continue;
|
||||
if (config.skip && config.skip(rel)) continue;
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
if (config.pattern.test(source)) found.add(rel);
|
||||
}
|
||||
}
|
||||
return [...found].sort();
|
||||
}
|
||||
|
||||
const allowlist = fs.existsSync(allowlistPath)
|
||||
? JSON.parse(fs.readFileSync(allowlistPath, "utf8"))
|
||||
: {};
|
||||
|
||||
if (process.argv.includes("--write")) {
|
||||
const next = {};
|
||||
for (const [name, config] of Object.entries(DIRECTIONS)) {
|
||||
next[name] = offendersFor(name, config);
|
||||
}
|
||||
fs.writeFileSync(allowlistPath, JSON.stringify(next, null, 2) + "\n");
|
||||
console.log("Wrote the allowlist from the current tree.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let failed = false;
|
||||
|
||||
for (const [name, config] of Object.entries(DIRECTIONS)) {
|
||||
const offenders = offendersFor(name, config);
|
||||
const allowed = new Set(allowlist[name] ?? []);
|
||||
|
||||
const added = offenders.filter((file) => !allowed.has(file));
|
||||
const stale = [...allowed].filter((file) => !offenders.includes(file));
|
||||
|
||||
if (added.length > 0) {
|
||||
failed = true;
|
||||
console.error(`\n${name}: ${added.length} new violation(s):`);
|
||||
for (const file of added) console.error(` ${file}`);
|
||||
}
|
||||
|
||||
if (stale.length > 0) {
|
||||
failed = true;
|
||||
console.error(
|
||||
`\n${name}: ${stale.length} allowlist entry(ies) no longer violate. Remove them:`,
|
||||
);
|
||||
for (const file of stale) console.error(` ${file}`);
|
||||
}
|
||||
|
||||
if (added.length === 0 && stale.length === 0) {
|
||||
console.log(`${name}: ${offenders.length} remaining (allowlisted)`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
console.error(
|
||||
"\nRun `node scripts/check-plugin-boundaries.cjs --write` if the change is intentional.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/**
|
||||
* 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, host-metrics and remote-desktop are the exception: their backends
|
||||
* are real TypeScript, physically relocated from src/backend/hosts/docker/,
|
||||
* src/backend/hosts/metrics/ and src/backend/hosts/guacamole/ rather than
|
||||
* kept as hand-written JS, because each is thousands of lines of typed
|
||||
* SSH/session (or, for remote-desktop, guacd protocol) 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(", ")}`);
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"ui-to-plugin": [
|
||||
"src/ui/AppShell.tsx",
|
||||
"src/ui/dashboard/DashboardTab.tsx",
|
||||
"src/ui/features/ai/useMentions.ts",
|
||||
"src/ui/features/collab/CollabGuestView.tsx",
|
||||
"src/ui/features/collab/CollabRoomTab.tsx",
|
||||
"src/ui/features/homepage/widgets/DockerWidget.tsx",
|
||||
"src/ui/features/session-sharing/SharedSessionView.tsx",
|
||||
"src/ui/features/terminal/Terminal.tsx",
|
||||
"src/ui/main-axios.ts",
|
||||
"src/ui/shell/tabUtils.tsx",
|
||||
"src/ui/sidebar/AdminSettingsPanel.tsx",
|
||||
"src/ui/sidebar/HostEditor.tsx",
|
||||
"src/ui/sidebar/HostEditorWebUiSection.tsx",
|
||||
"src/ui/sidebar/HostsPanel.tsx",
|
||||
"src/ui/sidebar/quick-connect-host.ts",
|
||||
"src/ui/sidebar/tree/HostItem/HostItem.tsx"
|
||||
],
|
||||
"plugin-to-core": [
|
||||
"plugins/ai/src/backend/egress.ts",
|
||||
"plugins/ai/src/backend/engine.ts",
|
||||
"plugins/ai/src/backend/gating.ts",
|
||||
"plugins/ai/src/backend/index.ts",
|
||||
"plugins/ai/src/backend/providers/http.ts",
|
||||
"plugins/ai/src/backend/routes.ts",
|
||||
"plugins/ai/src/backend/tools/executor.ts",
|
||||
"plugins/ai/src/backend/tools/read-tools.ts",
|
||||
"plugins/automations/src/backend/actions/host-targets.ts",
|
||||
"plugins/automations/src/backend/actions/index.ts",
|
||||
"plugins/automations/src/backend/actions/types.ts",
|
||||
"plugins/automations/src/backend/conditions.ts",
|
||||
"plugins/automations/src/backend/docker-watcher.ts",
|
||||
"plugins/automations/src/backend/engine.ts",
|
||||
"plugins/automations/src/backend/headless-viewer.ts",
|
||||
"plugins/automations/src/backend/http.ts",
|
||||
"plugins/automations/src/backend/notify.ts",
|
||||
"plugins/automations/src/backend/routes.ts",
|
||||
"plugins/automations/src/backend/scheduler.ts",
|
||||
"plugins/automations/src/backend/triggers.ts",
|
||||
"plugins/docker/src/backend/console.ts",
|
||||
"plugins/docker/src/backend/container-routes.ts",
|
||||
"plugins/docker/src/backend/index.ts",
|
||||
"plugins/docker/src/backend/routes.ts",
|
||||
"plugins/docker/src/backend/session-manager.ts",
|
||||
"plugins/fleets/src/backend/routes.ts",
|
||||
"plugins/host-metrics/src/backend/automation-bridge.ts",
|
||||
"plugins/host-metrics/src/backend/helpers.ts",
|
||||
"plugins/host-metrics/src/backend/history-routes.ts",
|
||||
"plugins/host-metrics/src/backend/managers/cron.ts",
|
||||
"plugins/host-metrics/src/backend/managers/firewall.ts",
|
||||
"plugins/host-metrics/src/backend/managers/health.ts",
|
||||
"plugins/host-metrics/src/backend/managers/index.ts",
|
||||
"plugins/host-metrics/src/backend/managers/logs.ts",
|
||||
"plugins/host-metrics/src/backend/managers/packages.ts",
|
||||
"plugins/host-metrics/src/backend/managers/processes.ts",
|
||||
"plugins/host-metrics/src/backend/managers/route-helpers.ts",
|
||||
"plugins/host-metrics/src/backend/managers/services.ts",
|
||||
"plugins/host-metrics/src/backend/managers/simple-reads.ts",
|
||||
"plugins/host-metrics/src/backend/managers/ssl.ts",
|
||||
"plugins/host-metrics/src/backend/managers/tailscale.ts",
|
||||
"plugins/host-metrics/src/backend/managers/types.ts",
|
||||
"plugins/host-metrics/src/backend/managers/users.ts",
|
||||
"plugins/host-metrics/src/backend/managers/wireguard.ts",
|
||||
"plugins/host-metrics/src/backend/preferences-routes.ts",
|
||||
"plugins/host-metrics/src/backend/routes.ts",
|
||||
"plugins/host-metrics/src/backend/sessions.ts",
|
||||
"plugins/host-metrics/src/backend/settings-routes.ts",
|
||||
"plugins/host-metrics/src/backend/viewer-routes.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/cpu-collector.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/disk-collector.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/firewall-collector.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/gpu-collector.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/login-stats-collector.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/memory-collector.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/network-collector.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/ports-collector.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/processes-collector.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/system-collector.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/temperature-collector.ts",
|
||||
"plugins/host-metrics/src/backend/widgets/uptime-collector.ts",
|
||||
"plugins/network-topology/src/backend/routes.ts",
|
||||
"plugins/proxmox/src/backend/routes.ts",
|
||||
"plugins/remote-desktop/src/backend/guacamole-server.ts",
|
||||
"plugins/remote-desktop/src/backend/index.ts",
|
||||
"plugins/remote-desktop/src/backend/routes.ts",
|
||||
"plugins/remote-desktop/src/backend/token-service.ts",
|
||||
"plugins/ssh-terminal/src/backend/index.ts",
|
||||
"plugins/tailscale/src/backend/routes.ts",
|
||||
"plugins/web-endpoint/src/backend/routes.ts",
|
||||
"plugins/workspaces/src/backend/routes.ts"
|
||||
],
|
||||
"plugin-to-plugin": [
|
||||
"plugins/ai/src/backend/tools/executor.ts",
|
||||
"plugins/host-metrics/src/backend/automation-bridge.ts",
|
||||
"plugins/host-metrics/src/backend/routes.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Runs every plugin's own vitest suite.
|
||||
*
|
||||
* Core's `npm run test` covers src/ only. A plugin's tests live beside it and
|
||||
* run under its own config, so a plugin that moves out of this repo keeps a
|
||||
* working suite.
|
||||
*/
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const source = path.join(root, "plugins");
|
||||
const cli = path.join(root, "packages", "plugin-sdk", "cli", "index.mjs");
|
||||
|
||||
const pluginIds = fs
|
||||
.readdirSync(source, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.filter((entry) =>
|
||||
fs.existsSync(path.join(source, entry.name, "vitest.config.ts")),
|
||||
)
|
||||
.map((entry) => entry.name);
|
||||
|
||||
const failed = [];
|
||||
|
||||
for (const id of pluginIds) {
|
||||
const pluginDir = path.join(source, id);
|
||||
|
||||
const hasTests = ["tests/backend", "tests/frontend"].some((dir) => {
|
||||
const full = path.join(pluginDir, dir);
|
||||
return (
|
||||
fs.existsSync(full) &&
|
||||
fs
|
||||
.readdirSync(full, { recursive: true })
|
||||
.some(
|
||||
(name) =>
|
||||
String(name).endsWith(".test.ts") ||
|
||||
String(name).endsWith(".test.tsx"),
|
||||
)
|
||||
);
|
||||
});
|
||||
if (!hasTests) continue;
|
||||
|
||||
const result = spawnSync(process.execPath, [cli, "test"], {
|
||||
cwd: pluginDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (result.status !== 0) failed.push(id);
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
console.error(`\nPlugin tests failed: ${failed.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\nPlugin tests passed for ${pluginIds.length} plugin(s).`);
|
||||
@@ -14,7 +14,13 @@ const path = require("node:path");
|
||||
const { pathToFileURL } = require("node:url");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const sdkEntry = path.join(root, "packages", "plugin-sdk", "dist", "manifest.js");
|
||||
const sdkEntry = path.join(
|
||||
root,
|
||||
"packages",
|
||||
"plugin-sdk",
|
||||
"dist",
|
||||
"manifest.js",
|
||||
);
|
||||
|
||||
async function loadValidator() {
|
||||
if (!fs.existsSync(sdkEntry)) {
|
||||
|
||||
Reference in new issue
Block a user