Plugins are switched on and off in a Plugins tab, the server keeps the state in data/plugins/plugins.json, and switched-off plugins are never delivered (#114)
game-maker / gate (push) Successful in 1m38s
game-maker / suite (push) Successful in 5m43s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-17 20:15:51 +02:00
1 parent 57a97ba94f
commit c17b2b5426
17 files changed
+994 -129

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
import {
chownSync,
copyFileSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from 'node:fs';
import { resolve } from 'node:path';
/**
* Puts the plugins that ship with the repository (`plugins/*.js`) where the editor loads them from,
* `data/plugins/`, and switches them on in `plugins.json` (#114) – a plugin put there by hand stays
* off until someone switches it on in the Plugins tab. Used by the build and by the e2e seeder.
*
* `force` is the seeder's: the checks need the demo plugin on, whatever a person did with the
* switch. The build only switches on what the state does not name yet, so a plugin switched off
* on the server stays off across deployments.
*
* The API writes `plugins.json` as the web server's user. When this runs as root (the build on the
* host), the folder and what it wrote are handed to the owner of `data/`, which is that user.
*/
export function publishPlugins(root, { force = false } = {}) {
const target = resolve(root, 'data/plugins');
mkdirSync(target, { recursive: true });
const written = [target];
const shipped = readdirSync(resolve(root, 'plugins')).filter((f) => f.endsWith('.js'));
for (const file of shipped) {
copyFileSync(resolve(root, 'plugins', file), resolve(target, file));
written.push(resolve(target, file));
}
const statePath = resolve(target, 'plugins.json');
let state = {};
if (existsSync(statePath)) {
try {
state = JSON.parse(readFileSync(statePath, 'utf8'));
} catch {
console.warn(`${statePath} is not readable JSON – left as it is, no plugin switched on`);
state = null;
}
}
if (state && typeof state === 'object') {
let changed = false;
for (const file of shipped) {
if (!force && Object.hasOwn(state, file)) continue;
if (state[file]?.active === true) continue;
state[file] = { active: true };
changed = true;
}
if (changed) {
const sorted = Object.fromEntries(
Object.entries(state).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
);
writeFileSync(statePath, `${JSON.stringify(sorted, null, 4)}\n`);
}
written.push(statePath);
}
if (process.getuid?.() === 0) {
const { uid, gid } = statSync(resolve(root, 'data'));
for (const path of written) if (existsSync(path)) chownSync(path, uid, gid);
}
}