mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-11 22:50:06 +00:00
test(core): Guard against private packages in the published runtime graph (#34889)
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4e43644bba
commit
2c4805cf4c
@@ -32,6 +32,12 @@ pre-commit:
|
||||
skip:
|
||||
- merge
|
||||
- rebase
|
||||
workspace_private_deps_check:
|
||||
glob: '**/package.json'
|
||||
run: node scripts/check-workspace-private-deps.mjs
|
||||
skip:
|
||||
- merge
|
||||
- rebase
|
||||
zod_peer_deps_check:
|
||||
glob: 'packages/{@n8n/api-types,workflow,core,@n8n/agents}/package.json'
|
||||
run: node scripts/check-zod-peer-deps.mjs
|
||||
|
||||
+2
-1
@@ -42,13 +42,14 @@
|
||||
"lint:styles:fix": "turbo run lint:styles:fix",
|
||||
"lint:affected": "turbo run lint --affected",
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"lint:ci": "turbo run lint lint:styles && pnpm boundaries:check",
|
||||
"lint:ci": "turbo run lint lint:styles && pnpm boundaries:check && pnpm check:workspace-private-deps",
|
||||
"boundaries": "turbo boundaries",
|
||||
"boundaries:check": "node scripts/check-boundaries.mjs",
|
||||
"boundaries:baseline": "node scripts/check-boundaries.mjs --write",
|
||||
"sync:skill-links": "node scripts/sync-agent-skill-links.mjs",
|
||||
"check:skill-links": "node scripts/sync-agent-skill-links.mjs --check",
|
||||
"check:zod-peer-deps": "node scripts/check-zod-peer-deps.mjs",
|
||||
"check:workspace-private-deps": "node scripts/check-workspace-private-deps.mjs",
|
||||
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
|
||||
"setup-backend-module": "node scripts/ensure-zx.mjs && zx scripts/backend-module/setup.mjs",
|
||||
"start": "node scripts/os-normalize.mjs --dir packages/cli/bin n8n",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Guards against a broken `npm install n8n`: a publishable (non-private)
|
||||
* workspace package must not depend at runtime on a `private` one.
|
||||
*
|
||||
* The `n8n` cli depends on `n8n-editor-ui` (published), which depended on
|
||||
* `@n8n/frontend-constants`, `@n8n/frontend-utils` and `@n8n/telemetry` — all
|
||||
* marked `"private": true`. Publishing `n8n` then produces an install graph
|
||||
* pointing at packages that were never published. Marking any package private
|
||||
* (or adding a private package as a runtime dependency of a published one)
|
||||
* silently reintroduces this.
|
||||
*
|
||||
* We only need to check *direct* runtime deps: if every published package's
|
||||
* direct workspace deps are also published, the whole runtime closure is clean
|
||||
* by induction. `devDependencies` don't ship, so they're ignored.
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
/**
|
||||
* Pure, testable core. Given the workspace manifests, return violation strings
|
||||
* for every non-private package that has a private workspace runtime dep.
|
||||
* @param {Array<{name: string, private?: boolean, dependencies?: Record<string,string>}>} manifests
|
||||
*/
|
||||
export function findViolations(manifests) {
|
||||
const byName = new Map(manifests.map((m) => [m.name, m]));
|
||||
const violations = [];
|
||||
for (const pkg of manifests) {
|
||||
if (pkg.private) continue; // a private package may depend on anything
|
||||
for (const [dep, range] of Object.entries(pkg.dependencies ?? {})) {
|
||||
if (typeof range !== 'string' || !range.startsWith('workspace:')) continue;
|
||||
if (byName.get(dep)?.private) {
|
||||
violations.push(`${pkg.name} → ${dep} (${dep} is "private": true)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
function loadManifests() {
|
||||
const files = globSync('packages/**/package.json', {
|
||||
cwd: root,
|
||||
exclude: (p) => p.includes('node_modules'),
|
||||
});
|
||||
return files.map((rel) => JSON.parse(readFileSync(resolve(root, rel), 'utf8'))).filter((m) => m.name);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const violations = findViolations(loadManifests());
|
||||
if (violations.length > 0) {
|
||||
console.error('');
|
||||
console.error('ERROR: publishable packages must not depend on "private" packages at runtime.');
|
||||
console.error('');
|
||||
console.error('These packages are published to npm, so a private runtime dependency leaves the');
|
||||
console.error('published install graph pointing at packages that were never published, breaking');
|
||||
console.error('`npm install n8n`.');
|
||||
console.error('');
|
||||
for (const v of violations) console.error(` - ${v}`);
|
||||
console.error('');
|
||||
console.error('Either drop "private": true from the dependency, or move it to devDependencies.');
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('OK: no publishable package depends on a private package at runtime.');
|
||||
}
|
||||
|
||||
// `--self-test` exercises the core logic without touching the workspace.
|
||||
if (process.argv.includes('--self-test')) {
|
||||
const manifests = [
|
||||
{ name: 'pub', dependencies: { priv: 'workspace:*', ok: 'workspace:*' } },
|
||||
{ name: 'priv', private: true },
|
||||
{ name: 'ok' },
|
||||
{ name: 'priv-dep-of-priv', private: true, dependencies: { priv: 'workspace:*' } },
|
||||
{ name: 'ext', dependencies: { lodash: '^4' } },
|
||||
];
|
||||
const v = findViolations(manifests);
|
||||
const pass = v.length === 1 && v[0].startsWith('pub → priv');
|
||||
console.log(pass ? 'self-test passed' : `self-test FAILED: ${JSON.stringify(v)}`);
|
||||
process.exit(pass ? 0 : 1);
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
Reference in New Issue
Block a user