/** * @file runtime-deps.test.js * @description Every package the server code requires is a runtime dependency. * A plugin install runs `npm install --omit=dev`, so a package listed only in * devDependencies — or one that arrived only as some other package's * dependency — is missing there. v2.8.9 crashed at start on every plugin * install: `js-yaml` (server/lib/lane-detect.js) was a devDependency that * `redoc` used to pull in, and dropping `redoc` took it away. Machines with * Debian's global /usr/share/nodejs never saw it, so this checks the manifest * instead of trusting a local `require` to fail. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const { builtinModules } = require("node:module"); const ROOT = path.join(__dirname, "..", ".."); const pkg = require(path.join(ROOT, "package.json")); // Required only behind a runtime check, never on a plain Node server. const GUARDED = new Set(["electron"]); function sourceFiles(dir) { const out = []; for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const p = path.join(dir, e.name); if (e.isDirectory()) { if (!["node_modules", "__tests__", "vendor"].includes(e.name)) out.push(...sourceFiles(p)); } else if (/\.(c?js|mjs)$/.test(e.name)) out.push(p); } return out; } /** Bare package names `file` requires, comments left out. */ function requiredPackages(file) { const code = fs .readFileSync(file, "utf8") .replace(/\/\*[\s\S]*?\*\//g, "") .replace(/(^|[^:"'`\\])\/\/.*$/gm, "$1"); const names = new Set(); for (const m of code.matchAll(/\brequire\(\s*["']([^"'./][^"']*)["']\s*\)/g)) { const spec = m[1]; if (spec.startsWith("node:")) continue; const name = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0]; if (!builtinModules.includes(name)) names.add(name); } return names; } describe("runtime dependencies", () => { it("every package server/ requires is in dependencies or optionalDependencies", () => { const runtime = new Set([ ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.optionalDependencies || {}), ]); const missing = []; for (const file of sourceFiles(path.join(ROOT, "server"))) { for (const name of requiredPackages(file)) { if (!runtime.has(name) && !GUARDED.has(name)) missing.push(`${path.relative(ROOT, file)}: ${name}`); } } assert.deepEqual(missing, []); }); });