Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 20 additions & 43 deletions scripts/ablation-dist-preflight.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,60 +145,37 @@ import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import process from 'node:process';
import { isEntrypoint } from './invoked-as.mjs';
import { WORKSPACE_FILE, parseWorkspaceGlobs, workspacePackageDirs } from './workspace-enumerator.mjs';

const REPO_ROOT = resolve(fileURLToPath(import.meta.url), '..', '..');

// Read as text, but never let a binary artifact fabricate a match.
const BINARY_EXT = new Set(['.wasm', '.node', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.zip', '.gz', '.br']);

/** Parse the `packages:` globs out of pnpm-workspace.yaml (no YAML dependency). */
export function parseWorkspaceGlobs(yamlText) {
const globs = [];
let inPackages = false;
for (const rawLine of yamlText.split('\n')) {
const line = rawLine.replace(/\s+$/, '');
if (/^packages:\s*$/.test(line)) {
inPackages = true;
continue;
}
if (inPackages) {
const item = line.match(/^\s+-\s+['"]?([^'"\s]+)['"]?\s*$/);
if (item) {
globs.push(item[1]);
continue;
}
if (line.trim() !== '') break; // next top-level key ends the list
}
}
return globs;
}
/**
* Parse the `packages:` globs out of pnpm-workspace.yaml (no YAML dependency).
*
* Re-exported from `scripts/workspace-enumerator.mjs` (#11510) rather than
* parsed here. The copy this replaces was the strictest of the nine and the
* only one that ended the list at ANY line it could not match, so a whole-line
* comment inside the `packages:` block silently truncated the workspace — this
* script would then have scanned a subset of the members and reported a clean
* preflight over it. Latent on this repo's file today; a comment in that block
* is all it needed.
*/
export { parseWorkspaceGlobs };

/** name -> repo-relative dir, for every workspace package. */
function workspacePackages(repoRoot) {
const yamlPath = join(repoRoot, 'pnpm-workspace.yaml');
let globs;
let dirs;
try {
globs = parseWorkspaceGlobs(readFileSync(yamlPath, 'utf8'));
} catch {
fail(`cannot read ${relative(repoRoot, yamlPath) || 'pnpm-workspace.yaml'} -- refusing to guess the workspace layout.`);
}
if (globs.length === 0) fail('pnpm-workspace.yaml declares no `packages:` globs -- refusing to scan an empty workspace.');

const dirs = [];
for (const glob of globs) {
if (glob.endsWith('/*')) {
const parent = join(repoRoot, glob.slice(0, -2));
let entries = [];
try {
entries = readdirSync(parent, { withFileTypes: true });
} catch {
continue; // a declared-but-absent parent is the workspace's problem, not ours
}
for (const e of entries) if (e.isDirectory()) dirs.push(join(parent, e.name));
} else {
dirs.push(join(repoRoot, glob));
}
dirs = workspacePackageDirs(repoRoot).map((rel) => join(repoRoot, rel));
} catch (err) {
fail(
`cannot enumerate the workspace from ${WORKSPACE_FILE} -- refusing to guess the layout.\n ${err?.message ?? err}`,
);
}
if (dirs.length === 0) fail(`${WORKSPACE_FILE} declares no \`packages:\` globs -- refusing to scan an empty workspace.`);

const byName = new Map();
for (const dir of dirs) {
Expand Down
116 changes: 24 additions & 92 deletions scripts/check-changeset-fixed.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,112 +12,44 @@
* - A name listed in the `fixed` group no longer exists in the workspace
*
* The script intentionally has zero third-party dependencies so it can run
* in minimal CI environments before `pnpm install`. It reads
* pnpm-workspace.yaml directly and walks the workspace globs itself.
* in minimal CI environments before `pnpm install`. The workspace walk comes
* from scripts/workspace-enumerator.mjs, which has none either, for the same
* reason.
*/

import { readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { workspacePackages } from './workspace-enumerator.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '..');

/**
* Minimal pnpm-workspace.yaml parser: extracts entries under the top-level
* `packages:` key. Supports the `- pattern` list form used by this repo and
* tolerates comments / blank lines. Avoids pulling in a YAML dependency.
* Names of all non-private workspace packages.
*
* @returns {string[]}
*/
function readWorkspacePatterns() {
const text = readFileSync(resolve(repoRoot, 'pnpm-workspace.yaml'), 'utf8');
const lines = text.split(/\r?\n/);
const patterns = [];
let inPackages = false;
for (const raw of lines) {
const line = raw.replace(/#.*$/, '').replace(/\s+$/, '');
if (!line.trim()) continue;
if (/^packages\s*:\s*$/.test(line)) {
inPackages = true;
continue;
}
if (inPackages) {
const m = /^\s+-\s+["']?([^"'\s]+)["']?\s*$/.exec(line);
if (m) {
patterns.push(m[1]);
continue;
}
// Any other non-indented key ends the packages block.
if (/^\S/.test(line)) inPackages = false;
}
}
return patterns;
}

/**
* Expand a single `pattern` like `packages/*` or `packages/services/*` into
* concrete directory paths. Only supports the `*` wildcard at any single
* path segment, which is what the repo uses.
* Membership comes from `scripts/workspace-enumerator.mjs` (#11510) — this
* repo's one parse of `pnpm-workspace.yaml`, and one of nine private copies
* before it. Two behaviours of the copy that used to live here changed, both
* toward refusing rather than under-reporting, and neither observable on this
* repo's workspace file:
*
* - a missing or empty `packages:` block used to yield `[]`, which made this
* gate report the `fixed` group in sync with a workspace of zero packages —
* green, and vacuous. It now throws.
* - a `#` was stripped unconditionally, so a member path legitimately
* containing one was silently truncated to a directory that does not exist.
* Only a whitespace-led `#` is a comment now.
*
* @param {string} pattern
* @returns {string[]}
*/
function expandPattern(pattern) {
const segments = pattern.split('/');
/** @type {string[]} */
let dirs = [repoRoot];
for (const seg of segments) {
/** @type {string[]} */
const next = [];
for (const dir of dirs) {
if (seg === '*') {
let entries;
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('.')) {
next.push(join(dir, entry.name));
}
}
} else {
const candidate = join(dir, seg);
try {
if (statSync(candidate).isDirectory()) next.push(candidate);
} catch {
/* missing - skip */
}
}
}
dirs = next;
}
return dirs;
}

/** @returns {string[]} names of all non-private workspace packages */
function listPublicPackageNames() {
const patterns = readWorkspacePatterns();
const seen = new Set();
const names = [];
for (const pattern of patterns) {
for (const dir of expandPattern(pattern)) {
const pkgPath = join(dir, 'package.json');
let pkg;
try {
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
} catch {
continue;
}
if (!pkg.name || pkg.private === true) continue;
if (seen.has(pkg.name)) continue;
seen.add(pkg.name);
names.push(pkg.name);
}
const names = new Set();
for (const { manifest } of workspacePackages(repoRoot)) {
if (!manifest.name || manifest.private === true) continue;
names.add(manifest.name);
}
return names.sort();
return [...names].sort();
}

function readFixedGroups() {
Expand Down
69 changes: 29 additions & 40 deletions scripts/check-dev-prereqs.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -237,6 +237,11 @@ import { existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync,
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'url';
import {
WorkspaceEnumerationError,
selfTest as workspaceEnumeratorSelfTest,
workspaceMemberDirs,
} from './workspace-enumerator.mjs';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');

Expand DownExpand Up@@ -265,48 +270,27 @@ class CoverageError extends Error {}

/**
* Workspace member directories, from pnpm-workspace.yaml — the workspace's own
* declaration of what it contains. Only `<dir>/*` and literal paths are
* understood; anything else throws instead of silently covering less.
* declaration of what it contains.
*
* The parse and the glob expansion come from
* `scripts/workspace-enumerator.mjs` (#11510), this repo's one reading of that
* file. Its refusals are the ones this gate already made — a pattern richer
* than `<dir>` or `<dir>/*` throws rather than quietly covering fewer packages,
* which is what would make this gate pass vacuously — and they are re-thrown as
* `CoverageError` so this file's own failure vocabulary is unchanged.
*
* Importing it does not give this gate a path population: the enumerator
* declares none, deliberately, so the reasoning in this file's header (CI runs
* `--self-test` only, so a workspace-wide declaration here would name this gate
* for every packages/ card in the tree) still holds exactly as written.
*/
function workspaceDirs(root) {
const file = path.join(root, 'pnpm-workspace.yaml');
if (!existsSync(file)) throw new CoverageError(`${rel(root, file)} is missing — cannot enumerate workspace packages.`);

const lines = readFileSync(file, 'utf-8').split('\n');
const start = lines.findIndex((l) => /^packages:\s*$/.test(l));
if (start === -1) throw new CoverageError(`pnpm-workspace.yaml has no 'packages:' list — cannot enumerate workspace packages.`);

const patterns = [];
for (const line of lines.slice(start + 1)) {
const item = /^\s+-\s+(.+?)\s*$/.exec(line);
if (!item) {
if (/^\S/.test(line)) break; // next top-level key ends the list
continue; // blank line or comment inside the list
}
patterns.push(item[1].replace(/^['"]|['"]$/g, ''));
}

const dirs = [];
for (const pattern of patterns) {
if (pattern.startsWith('!')) continue; // exclusion: nothing to enumerate
if (pattern.includes('**') || pattern.slice(0, -2).includes('*')) {
throw new CoverageError(
`pnpm-workspace.yaml pattern '${pattern}' is not a shape this gate can expand.\n` +
` Teach scripts/check-dev-prereqs.mjs the new pattern shape — silently covering fewer\n` +
` packages would make this gate pass vacuously.`,
);
}
if (!pattern.endsWith('/*')) {
dirs.push(path.join(root, pattern));
continue;
}
const parent = path.join(root, pattern.slice(0, -2));
if (!existsSync(parent)) continue;
for (const entry of readdirSync(parent, { withFileTypes: true })) {
if (entry.isDirectory() && entry.name !== 'node_modules') dirs.push(path.join(parent, entry.name));
}
try {
return workspaceMemberDirs(root).map((dir) => path.join(root, dir));
} catch (err) {
if (err instanceof WorkspaceEnumerationError) throw new CoverageError(err.message);
throw err;
}
return dirs;
}

/** The entry point Node resolves for `import '<pkg>'`: exports["."], else main. */
Expand DownExpand Up@@ -827,13 +811,18 @@ function selfTest() {
rmSync(tmp, { recursive: true, force: true });
}

// The shared workspace enumerator is a plain module with no CI invocation of
// its own (#11510 — being a gate is exactly what it must not be); every gate
// that consolidated onto it folds in its checks.
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));

if (failures.length > 0) {
console.error(`\n✗ check:dev-prereqs --self-test — ${failures.length} failure(s)\n`);
for (const f of failures) console.error(` ${f}`);
console.error('');
return 1;
}
console.log('✓ check:dev-prereqs --self-test — every verdict reachable, exclusions and freshness coverage pinned (16 cases).');
console.log('✓ check:dev-prereqs --self-test — every verdict reachable, exclusions and freshness coverage pinned (16 cases), plus the shared workspace enumerator.');
return 0;
}

Expand Down
Loading
Loading