Files
Termix/scripts/check-plugin-uses.cjs
T

154 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
/**
* A plugin that calls another plugin's action, fills its slot or extends its
* extension point has to say so in manifest.json contributes.uses. Nothing
* else records that coupling: the ids are plain strings, so without it a
* plugin moved to its own repo silently loses a feature when the other one is
* renamed, and nobody can tell what depends on what.
*
* Owners are read from the code: a plugin owns the actions it registers, the
* slots it renders (ActionSlot/ComponentSlot/useSlotContributions/
* declareActionSlot) and the extension points it reads (useExtensions). An id
* nobody owns is core's and needs no declaration.
*
* --list prints what each plugin uses from which owner.
*/
const fs = require("node:fs");
const path = require("node:path");
const ROOT = path.resolve(__dirname, "..");
const PLUGINS = path.join(ROOT, "plugins");
// Each pattern captures a string literal or a constant's name; constants are
// resolved against the plugin's own `const NAME = "id"` declarations.
const ID = String.raw`(?:"([^"]+)"|([A-Z][A-Z0-9_]*))`;
const OWNS = [
new RegExp(String.raw`registerAction\(\s*${ID}`, "g"),
new RegExp(String.raw`declareActionSlot\(\s*\{\s*id:\s*${ID}`, "g"),
new RegExp(
String.raw`<(?:ActionSlot|ComponentSlot)[^>]*?slotId=\{?${ID}`,
"g",
),
new RegExp(String.raw`useSlotContributions\(\s*${ID}`, "g"),
new RegExp(String.raw`useExtensions(?:<[^>]*>)?\(\s*${ID}`, "g"),
new RegExp(String.raw`getExtension\(\s*${ID}`, "g"),
];
const USES = [
new RegExp(String.raw`invokeAction\(\s*${ID}`, "g"),
new RegExp(String.raw`registerSlotContribution\(\s*${ID}`, "g"),
new RegExp(String.raw`registerExtension\(\s*${ID}`, "g"),
new RegExp(
String.raw`<(?:ActionSlot|ComponentSlot)[^>]*?slotId=\{?${ID}`,
"g",
),
new RegExp(String.raw`useSlotContributions\(\s*${ID}`, "g"),
];
function sourceOf(dir) {
let text = "";
const walk = (current) => {
if (!fs.existsSync(current)) return;
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const file = path.join(current, entry.name);
if (entry.isDirectory()) walk(file);
else if (/\.(ts|tsx)$/.test(entry.name)) {
text += fs.readFileSync(file, "utf8");
}
}
};
walk(path.join(dir, "src"));
return text;
}
function constants(text) {
const map = new Map();
for (const match of text.matchAll(
/const ([A-Z][A-Z0-9_]*)\s*=\s*"([^"]+)"/g,
)) {
map.set(match[1], match[2]);
}
return map;
}
function matches(patterns, text) {
const names = constants(text);
const found = new Set();
for (const pattern of patterns) {
for (const match of text.matchAll(pattern)) {
const id = match[1] ?? names.get(match[2]);
if (id) found.add(id);
}
}
return found;
}
function collect(pluginsDir = PLUGINS) {
const plugins = fs
.readdirSync(pluginsDir, { withFileTypes: true })
.filter((entry) =>
fs.existsSync(path.join(pluginsDir, entry.name, "manifest.json")),
)
.map((entry) => {
const dir = path.join(pluginsDir, entry.name);
const manifest = JSON.parse(
fs.readFileSync(path.join(dir, "manifest.json"), "utf8"),
);
const source = sourceOf(dir);
return {
id: manifest.id,
declared: new Set(manifest.contributes?.uses ?? []),
owns: matches(OWNS, source),
uses: matches(USES, source),
};
});
const owner = new Map();
for (const plugin of plugins) {
for (const id of plugin.owns) if (!owner.has(id)) owner.set(id, plugin.id);
}
const problems = [];
const usage = {};
for (const plugin of plugins) {
for (const id of plugin.uses) {
const by = owner.get(id);
if (!by || by === plugin.id || plugin.owns.has(id)) continue;
(usage[plugin.id] ??= []).push(`${id} (${by})`);
if (!plugin.declared.has(id)) {
problems.push(
`${plugin.id} uses "${id}" from the ${by} plugin; add it to contributes.uses in plugins/${plugin.id}/manifest.json`,
);
}
}
for (const id of plugin.declared) {
if (!plugin.uses.has(id)) {
problems.push(
`${plugin.id} declares "${id}" in contributes.uses but never uses it`,
);
}
}
}
return { problems, usage, owner };
}
function main() {
const { problems, usage } = collect();
if (process.argv.includes("--list")) {
console.log(JSON.stringify(usage, null, 2));
return;
}
if (problems.length === 0) return;
for (const problem of problems) console.error(problem);
process.exit(1);
}
if (require.main === module) main();
/** Every action, slot and extension point id a plugin owns, by owner. */
function owners(pluginsDir = PLUGINS) {
return collect(pluginsDir).owner;
}
module.exports = { collect, owners };