FN-8994: add workspace package graph validation
Prevent cold workspace installs from failing on missing or unglobbed local packages. - Validate workspace-protocol dependencies and overrides against glob-covered packages. - Add static-gate coverage and regression tests for missing plugin packages. - Document the workspace package graph check. Files changed: docs/testing.md | 4 +- package.json | 9 +- scripts/__tests__/check-workspace-package-graph.test.mjs | 103 +++++++++++++++ scripts/check-workspace-package-graph.mjs | 138 +++++++++++++++++++++ 4 files changed, 249 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-8994 Fusion-Task-Lineage: 17dbe062-79aa-4417-ae38-43df881e9fa4 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -48,7 +48,9 @@ pnpm verify:workspace # deep opt-in verification: lint -> test:full -> build (N
|
||||
<!-- FNXC:TestInfrastructure 2026-06-25-00:00: verify:fast is the opt-in test-free verification path. docs/testing.md observes the broad test gate caught no recalled real bugs while consuming ~70% of shipping time in flake triage; typecheck+build+boot-smoke gives deterministic, flake-free signal without running tests. It changes no default — pnpm test, the merge gate, and CI are untouched; the full suite stays available and runs non-blocking. -->
|
||||
<!-- FNXC:TestInfrastructure 2026-06-26-00:49: verify:fast must bootstrap missing workspace dist artifacts and build @runfusion/fusion even when the CLI package is not in the changed-package set because package builds and the boot smoke invoke source-checkout wrappers that require dist outputs in fresh worktrees. -->
|
||||
<!-- FNXC:TestInfrastructure 2026-07-22-12:00: Cheap deterministic policy gates must fail before verify:fast's expensive work. Read canonical package.json pretest commands and invoke their validator entry points directly so test-free verification and the merge gate cannot drift. -->
|
||||
`pnpm verify:fast` (`scripts/verify-fast.mjs`) is the recommended **test-free verification** command. It first runs the canonical, read-only static validators from root `pretest` — `check-no-nohup`, `check-no-kill-4040`, `check-no-getdatabase`, `check-prerebase-inert`, `check-no-node-only-core-imports-in-dashboard`, `check-pi-versions-pinned`, `check-no-test-timeout-appeasement`, `check-changeset-format`, `check-routes-modular`, and `check-runtime-skill-loader-drift` (which enforces the Claude/Grok runtime skill loaders' clean rename-diff) — then bootstraps missing/stale workspace dist artifacts, runs **typecheck + build scoped to the changed packages** (reusing the same git-diff / changed-package resolution as `pnpm test`), always builds the `@runfusion/fusion` CLI package required by the source-checkout boot smoke, and runs the existing **boot smoke** once. The static phase invokes each existing validator entry point without update flags, is bounded and fail-fast, and runs **no Vitest or test lane**. It gives deterministic, flake-free signal in seconds, so it is a sound project `testCommand`/verification command when you want non-test verification. With no affected package (root/docs-only diff) it runs static checks, artifact bootstrap, the CLI prerequisite build, and boot smoke. Each step is bounded by the shared `runWithWatchdog` (class `changed`) so a hang fails fast, and it exits nonzero on the first failing step. This is purely additive: it does not change `pnpm test`, the merge gate, or CI, and the full suite stays available (`pnpm test:full`, non-blocking on push to main).
|
||||
`pnpm verify:fast` (`scripts/verify-fast.mjs`) is the recommended **test-free verification** command. It first runs the canonical, read-only static validators from root `pretest` — `check-no-nohup`, `check-no-kill-4040`, `check-no-getdatabase`, `check-prerebase-inert`, `check-no-node-only-core-imports-in-dashboard`, `check-pi-versions-pinned`, `check-workspace-package-graph`, `check-no-test-timeout-appeasement`, `check-changeset-format`, `check-routes-modular`, and `check-runtime-skill-loader-drift` (which enforces the Claude/Grok runtime skill loaders' clean rename-diff) — then bootstraps missing/stale workspace dist artifacts, runs **typecheck + build scoped to the changed packages** (reusing the same git-diff / changed-package resolution as `pnpm test`), always builds the `@runfusion/fusion` CLI package required by the source-checkout boot smoke, and runs the existing **boot smoke** once. The static phase invokes each existing validator entry point without update flags, is bounded and fail-fast, and runs **no Vitest or test lane**. It gives deterministic, flake-free signal in seconds, so it is a sound project `testCommand`/verification command when you want non-test verification. With no affected package (root/docs-only diff) it runs static checks, artifact bootstrap, the CLI prerequisite build, and boot smoke. Each step is bounded by the shared `runWithWatchdog` (class `changed`) so a hang fails fast, and it exits nonzero on the first failing step. This is purely additive: it does not change `pnpm test`, the merge gate, or CI, and the full suite stays available (`pnpm test:full`, non-blocking on push to main).
|
||||
|
||||
`pnpm check:workspace-package-graph` verifies that every `workspace:` dependency or override in the root importer or a glob-matched workspace manifest resolves to a glob-covered workspace package, and that no package directory under `packages/` or `plugins/` falls outside `pnpm-workspace.yaml` package globs.
|
||||
|
||||
<!-- FNXC:WorkspaceBuild 2026-06-30-00:00: FN-7290 keeps root pnpm build operator-facing while allowing unchanged plugin workspaces to skip their package build only when required dist outputs exist and a git-backed content hash matches the last successful plugin build cache entry. Missing dist, absent entries, changed plugin, declared local workspace-dependency, or root build config/tooling inputs, unavailable git hashes, or cache-version changes must rebuild rather than trust mtimes. -->
|
||||
`pnpm build` runs `scripts/build-workspace.mjs`: non-plugin workspace packages with build scripts still build on every run (excluding `@fusion/desktop` and `@fusion/mobile`), while plugin packages under `plugins/` and `plugins/examples/` can be skipped when `.fusion/cache/plugin-build-cache.json` records the same content hash as the current plugin package inputs plus declared local workspace-dependency inputs, root TypeScript/pnpm/build-tooling inputs, and all required `dist/` outputs are present. A plugin rebuild is forced for a missing or partial `dist/`, no successful-build cache entry, changed tracked or untracked plugin/dependency/root build inputs, unavailable git content hash, or build-cache version changes. The cache is an optimization only; cache writes are best-effort and a failed package build still makes `pnpm build` exit nonzero with the planned package names.
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"scripts": {
|
||||
"pretest": "node scripts/check-no-nohup.mjs && node scripts/check-no-cwd-relative-dashboard-test-reads.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-prerebase-inert.mjs && node scripts/check-capacity-pool-id.mjs && node scripts/check-no-node-only-core-imports-in-dashboard.mjs && node scripts/check-pi-versions-pinned.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs && node scripts/check-routes-modular.mjs && node scripts/check-runtime-skill-loader-drift.mjs",
|
||||
"pretest:full": "node scripts/check-no-nohup.mjs && node scripts/check-no-cwd-relative-dashboard-test-reads.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-prerebase-inert.mjs && node scripts/check-capacity-pool-id.mjs && node scripts/check-no-node-only-core-imports-in-dashboard.mjs && node scripts/check-pi-versions-pinned.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs && node scripts/check-routes-modular.mjs && node scripts/check-runtime-skill-loader-drift.mjs",
|
||||
"pretest": "node scripts/check-no-nohup.mjs && node scripts/check-no-cwd-relative-dashboard-test-reads.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-prerebase-inert.mjs && node scripts/check-capacity-pool-id.mjs && node scripts/check-no-node-only-core-imports-in-dashboard.mjs && node scripts/check-pi-versions-pinned.mjs && node scripts/check-workspace-package-graph.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs && node scripts/check-routes-modular.mjs && node scripts/check-runtime-skill-loader-drift.mjs",
|
||||
"pretest:full": "node scripts/check-no-nohup.mjs && node scripts/check-no-cwd-relative-dashboard-test-reads.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-prerebase-inert.mjs && node scripts/check-capacity-pool-id.mjs && node scripts/check-no-node-only-core-imports-in-dashboard.mjs && node scripts/check-pi-versions-pinned.mjs && node scripts/check-workspace-package-graph.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs && node scripts/check-routes-modular.mjs && node scripts/check-runtime-skill-loader-drift.mjs",
|
||||
"check:line-count": "node scripts/check-file-line-count.mjs",
|
||||
"check:routes-modular": "node scripts/check-routes-modular.mjs",
|
||||
"check:changesets": "node scripts/check-changeset-format.mjs",
|
||||
@@ -31,7 +31,7 @@
|
||||
"census:lifecycle-columns": "node scripts/lifecycle-column-census.mjs",
|
||||
"check:quarantine-ledger": "node scripts/check-quarantine-ledger.mjs --strict",
|
||||
"check:mock-completeness": "node scripts/check-mock-completeness.mjs",
|
||||
"test:gate:static": "node scripts/check-no-nohup.mjs && node scripts/check-no-cwd-relative-dashboard-test-reads.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-prerebase-inert.mjs && node scripts/check-capacity-pool-id.mjs && node scripts/check-no-node-only-core-imports-in-dashboard.mjs && node scripts/check-pi-versions-pinned.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs && node scripts/check-mock-completeness.mjs && node scripts/check-inert-sync-lane-conversions.mjs && node scripts/check-runtime-skill-loader-drift.mjs",
|
||||
"test:gate:static": "node scripts/check-no-nohup.mjs && node scripts/check-no-cwd-relative-dashboard-test-reads.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-prerebase-inert.mjs && node scripts/check-capacity-pool-id.mjs && node scripts/check-no-node-only-core-imports-in-dashboard.mjs && node scripts/check-pi-versions-pinned.mjs && node scripts/check-workspace-package-graph.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs && node scripts/check-mock-completeness.mjs && node scripts/check-inert-sync-lane-conversions.mjs && node scripts/check-runtime-skill-loader-drift.mjs",
|
||||
"test:gate": "node scripts/run-static-gate-checks.mjs && sh -c 'pnpm --filter @fusion/engine test:core & engine_pid=$!; pnpm --filter @fusion/core test:pg-gate & pg_pid=$!; pnpm --filter @fusion/core test:unit-gate & unit_pid=$!; status=0; wait $engine_pid || status=1; wait $pg_pid || status=1; wait $unit_pid || status=1; exit $status' && pnpm --filter @runfusion/fusion test:ci-shape",
|
||||
"smoke:boot": "node scripts/boot-smoke.mjs",
|
||||
"local": "node scripts/start-local.mjs",
|
||||
@@ -92,7 +92,8 @@
|
||||
"mobile:run:android": "bash scripts/mobile-run-android.sh",
|
||||
"ios:acceptance": "node scripts/ios-acceptance.mjs",
|
||||
"build:desktop": "pnpm --filter @fusion/desktop build",
|
||||
"dist:desktop:win": "pnpm --filter @fusion/desktop build && pnpm --filter @fusion/desktop dist:win"
|
||||
"dist:desktop:win": "pnpm --filter @fusion/desktop build && pnpm --filter @fusion/desktop dist:win",
|
||||
"check:workspace-package-graph": "node scripts/check-workspace-package-graph.mjs"
|
||||
},
|
||||
"pnpm": {
|
||||
"ignoredBuiltDependencies": [
|
||||
|
||||
103
scripts/__tests__/check-workspace-package-graph.test.mjs
Normal file
103
scripts/__tests__/check-workspace-package-graph.test.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
matchesWorkspaceGlob,
|
||||
readWorkspacePackageGraph,
|
||||
scanWorkspacePackageGraph,
|
||||
validateWorkspacePackageGraph,
|
||||
} from "../check-workspace-package-graph.mjs";
|
||||
import { readStaticGateChecks } from "../run-static-gate-checks.mjs";
|
||||
|
||||
const GLOBS = ["packages/*", "plugins/fusion-plugin-*"];
|
||||
const VALID_PLUGIN = {
|
||||
filePath: "plugins/fusion-plugin-claude-runtime/package.json",
|
||||
manifest: { name: "@fusion-plugin-examples/claude-runtime" },
|
||||
};
|
||||
|
||||
function validate(manifests, workspaceGlobs = GLOBS) {
|
||||
return validateWorkspacePackageGraph({ manifests, workspaceGlobs });
|
||||
}
|
||||
|
||||
describe("check-workspace-package-graph", () => {
|
||||
it("matches package globs without relying on node:path.matchesGlob", () => {
|
||||
assert.equal(matchesWorkspaceGlob("packages/dashboard", "packages/*"), true);
|
||||
assert.equal(matchesWorkspaceGlob("packages/dashboard/nested", "packages/*"), false);
|
||||
assert.equal(matchesWorkspaceGlob("plugins/fusion-plugin-claude-runtime", "plugins/fusion-plugin-*"), true);
|
||||
assert.equal(matchesWorkspaceGlob("plugins/examples/claude-runtime", "plugins/examples/*"), true);
|
||||
});
|
||||
|
||||
it("passes against the live repository", () => {
|
||||
assert.deepEqual(scanWorkspacePackageGraph(), []);
|
||||
});
|
||||
|
||||
it("includes the root importer without treating it as an unglobbed package", () => {
|
||||
const { manifests } = readWorkspacePackageGraph();
|
||||
assert.equal(manifests.some(({ filePath, isRoot }) => filePath === "package.json" && isRoot), true);
|
||||
assert.deepEqual(validate([{ filePath: "package.json", isRoot: true, manifest: { name: "fusion-workspace" } }]), []);
|
||||
});
|
||||
|
||||
it("rejects dangling workspace dependencies from the root importer", () => {
|
||||
const violations = validate([{
|
||||
filePath: "package.json",
|
||||
isRoot: true,
|
||||
manifest: { dependencies: { "@fusion-plugin-examples/missing": "workspace:*" } },
|
||||
}]);
|
||||
assert.equal(violations.length, 1);
|
||||
assert.match(violations[0], /package\.json/);
|
||||
assert.match(violations[0], /@fusion-plugin-examples\/missing/);
|
||||
});
|
||||
|
||||
it("rejects dangling workspace-protocol overrides", () => {
|
||||
const violations = validate([{
|
||||
filePath: "package.json",
|
||||
isRoot: true,
|
||||
manifest: { pnpm: { overrides: { "@fusion-plugin-examples/missing": "workspace:*" } } },
|
||||
}]);
|
||||
assert.equal(violations.length, 1);
|
||||
assert.match(violations[0], /overrides\.@fusion-plugin-examples\/missing/);
|
||||
});
|
||||
|
||||
it("reconstructs the missing claude-runtime package symptom", () => {
|
||||
const violations = validate([{
|
||||
filePath: "packages/dashboard/package.json",
|
||||
manifest: { dependencies: { "@fusion-plugin-examples/claude-runtime": "workspace:*" } },
|
||||
}]);
|
||||
assert.deepEqual(violations.length, 1);
|
||||
assert.match(violations[0], /packages\/dashboard\/package\.json/);
|
||||
assert.match(violations[0], /@fusion-plugin-examples\/claude-runtime/);
|
||||
});
|
||||
|
||||
it("checks every dependency block and accepts published or non-workspace dependencies", () => {
|
||||
for (const blockName of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
|
||||
const violations = validate([{ filePath: "packages/dashboard/package.json", manifest: { [blockName]: { missing: "workspace:*" } } }]);
|
||||
assert.equal(violations.length, 1, `${blockName} must be checked`);
|
||||
}
|
||||
assert.deepEqual(validate([
|
||||
VALID_PLUGIN,
|
||||
{ filePath: "packages/dashboard/package.json", manifest: { dependencies: { "@fusion-plugin-examples/claude-runtime": "workspace:*", external: "^1.2.3" } } },
|
||||
{ filePath: "packages/empty/package.json", manifest: {} },
|
||||
{ filePath: "packages/unnamed/package.json", manifest: { name: "" } },
|
||||
]), []);
|
||||
});
|
||||
|
||||
it("reports every dangling declaration across manifests", () => {
|
||||
const violations = validate([
|
||||
{ filePath: "packages/one/package.json", manifest: { dependencies: { one: "workspace:*" } } },
|
||||
{ filePath: "packages/two/package.json", manifest: { optionalDependencies: { two: "workspace:*" } } },
|
||||
]);
|
||||
assert.equal(violations.length, 2);
|
||||
});
|
||||
|
||||
it("rejects package directories outside workspace globs", () => {
|
||||
const violations = validate([
|
||||
{ filePath: "plugins/unlisted/package.json", manifest: { name: "unlisted" } },
|
||||
], ["packages/*"]);
|
||||
assert.equal(violations.length, 1);
|
||||
assert.match(violations[0], /plugins\/unlisted\/package\.json/);
|
||||
assert.match(violations[0], /not covered/);
|
||||
});
|
||||
|
||||
it("is included in the blocking static gate inventory", () => {
|
||||
assert.equal(readStaticGateChecks().includes("scripts/check-workspace-package-graph.mjs"), true);
|
||||
});
|
||||
});
|
||||
138
scripts/check-workspace-package-graph.mjs
Normal file
138
scripts/check-workspace-package-graph.mjs
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
FNXC:WorkspaceBootstrap 2026-08-11-21:28:
|
||||
FN-8994 found that a dangling workspace:* dependency or an unglobbed workspace
|
||||
package stays invisible until a cold pnpm install fails in an isolated worktree.
|
||||
Scan the root manifest as an importer even though it matches no workspace glob,
|
||||
while resolving targets only from glob-covered workspace packages.
|
||||
*/
|
||||
import { globSync, readFileSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
export const DEPENDENCY_BLOCKS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
||||
const PACKAGE_DIRECTORY_PREFIXES = ["packages/", "plugins/"];
|
||||
const EXCLUDED_PACKAGE_PATHS = ["**/node_modules/**", "**/dist/**"];
|
||||
|
||||
function isWorkspaceProtocol(value) {
|
||||
return typeof value === "string" && value.startsWith("workspace:");
|
||||
}
|
||||
|
||||
function isPackageDirectory(filePath) {
|
||||
return PACKAGE_DIRECTORY_PREFIXES.some((prefix) => filePath.startsWith(prefix));
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkspaceBootstrap 2026-08-11-21:41:
|
||||
FN-8994's validator runs in pretest and the blocking gate, whose supported Node
|
||||
floor is 22.4. Use this small package-glob matcher instead of node:path.matchesGlob,
|
||||
which was added after that floor and would make a valid cold bootstrap fail.
|
||||
*/
|
||||
export function matchesWorkspaceGlob(directory, workspaceGlob) {
|
||||
const pattern = workspaceGlob
|
||||
.replaceAll(/[|\\{}()[\]^$+?.]/g, "\\$&")
|
||||
.replaceAll("**", "\u0000")
|
||||
.replaceAll("*", "[^/]*")
|
||||
.replaceAll("\u0000", ".*");
|
||||
return new RegExp(`^${pattern}$`).test(directory);
|
||||
}
|
||||
|
||||
function isGlobMatched(filePath, workspaceGlobs) {
|
||||
const directory = dirname(filePath).replaceAll("\\", "/");
|
||||
return workspaceGlobs.some((workspaceGlob) => matchesWorkspaceGlob(directory, workspaceGlob));
|
||||
}
|
||||
|
||||
function overrideEntries(manifest) {
|
||||
return [manifest?.pnpm?.overrides, manifest?.overrides]
|
||||
.filter((overrides) => overrides && typeof overrides === "object")
|
||||
.flatMap((overrides) => Object.entries(overrides));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate in-memory manifests so the policy can be tested without repository I/O.
|
||||
* Root manifests are consumers only: they are excluded from package-directory
|
||||
* coverage and cannot publish a workspace resolution target.
|
||||
*/
|
||||
export function validateWorkspacePackageGraph({ manifests = [], workspaceGlobs = [] }) {
|
||||
const violations = [];
|
||||
const workspaceManifests = manifests.filter(({ filePath, isRoot }) => !isRoot && isGlobMatched(filePath, workspaceGlobs));
|
||||
const workspaceNames = new Set(
|
||||
workspaceManifests.map(({ manifest }) => manifest?.name).filter((name) => typeof name === "string" && name.trim()),
|
||||
);
|
||||
|
||||
for (const { filePath, manifest = {}, isRoot = false } of manifests) {
|
||||
if (!isRoot && isPackageDirectory(filePath) && !isGlobMatched(filePath, workspaceGlobs)) {
|
||||
violations.push(`${filePath}: package directory is not covered by any pnpm-workspace.yaml packages glob`);
|
||||
}
|
||||
|
||||
for (const blockName of DEPENDENCY_BLOCKS) {
|
||||
const dependencies = manifest?.[blockName];
|
||||
if (!dependencies || typeof dependencies !== "object") continue;
|
||||
for (const [packageName, version] of Object.entries(dependencies)) {
|
||||
if (isWorkspaceProtocol(version) && !workspaceNames.has(packageName)) {
|
||||
violations.push(`${filePath}: ${blockName}.${packageName} uses workspace: but no glob-covered workspace package publishes ${packageName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [packageName, version] of overrideEntries(manifest)) {
|
||||
if (isWorkspaceProtocol(version) && !workspaceNames.has(packageName)) {
|
||||
violations.push(`${filePath}: overrides.${packageName} uses workspace: but no glob-covered workspace package publishes ${packageName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function readWorkspacePackageGraph({ root = process.cwd(), readFile = readFileSync, glob = globSync } = {}) {
|
||||
let workspaceConfig;
|
||||
try {
|
||||
workspaceConfig = parseYaml(readFile(`${root}/pnpm-workspace.yaml`, "utf8"));
|
||||
} catch (error) {
|
||||
return { manifests: [], workspaceGlobs: [], violations: [`pnpm-workspace.yaml: invalid YAML (${error instanceof Error ? error.message : String(error)})`] };
|
||||
}
|
||||
const workspaceGlobs = Array.isArray(workspaceConfig?.packages) ? workspaceConfig.packages.filter((value) => typeof value === "string") : [];
|
||||
const manifestPaths = new Set(["package.json"]);
|
||||
for (const workspaceGlob of workspaceGlobs) {
|
||||
for (const filePath of glob(`${workspaceGlob}/package.json`, { cwd: root, exclude: EXCLUDED_PACKAGE_PATHS })) manifestPaths.add(filePath);
|
||||
}
|
||||
for (const filePath of glob("{packages,plugins}/**/package.json", { cwd: root, exclude: EXCLUDED_PACKAGE_PATHS })) manifestPaths.add(filePath);
|
||||
|
||||
const manifests = [];
|
||||
const readErrors = [];
|
||||
for (const filePath of [...manifestPaths].sort()) {
|
||||
try {
|
||||
manifests.push({
|
||||
filePath,
|
||||
isRoot: filePath === "package.json",
|
||||
manifest: JSON.parse(readFile(`${root}/${filePath}`, "utf8")),
|
||||
});
|
||||
} catch (error) {
|
||||
readErrors.push(`${filePath}: invalid JSON (${error instanceof Error ? error.message : String(error)})`);
|
||||
}
|
||||
}
|
||||
return { manifests, workspaceGlobs, violations: [...readErrors, ...validateWorkspacePackageGraph({ manifests, workspaceGlobs })] };
|
||||
}
|
||||
|
||||
export function scanWorkspacePackageGraph(options = {}) {
|
||||
return readWorkspacePackageGraph(options).violations;
|
||||
}
|
||||
|
||||
export function formatFailureMessage(violations) {
|
||||
return [
|
||||
"[check-workspace-package-graph] workspace: dependencies must resolve to glob-covered workspace packages.",
|
||||
"Every package directory under packages/ or plugins/ must be included by pnpm-workspace.yaml.",
|
||||
...violations.map((violation) => `- ${violation}`),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function main() {
|
||||
const violations = scanWorkspacePackageGraph();
|
||||
if (!violations.length) return 0;
|
||||
console.error(formatFailureMessage(violations));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) process.exitCode = main();
|
||||
Reference in New Issue
Block a user