From fe74ba7e32809b2fa18f4a5cd8dec75f6746f0e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 19:14:54 +0000 Subject: [PATCH] fix(cli): anchor `objectui check`'s ignore patterns at every depth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packages/cli/src/commands/check.ts` passed `ignore: ['node_modules/**', 'dist/**', '.git/**']` to `globSync`. `glob` matches `ignore` patterns against the path relative to `cwd`, so an unanchored `dist/**` / `node_modules/**` excludes only a directory of that name at the scan root — every nested `packages//dist/`, `examples//dist/`, `apps//dist/` (and their `node_modules/`) was still scanned. In a built workspace this re-reads the author's own schemas a second time from build output, roughly doubling every reported count (objectui#6320). Widen both patterns to anchor at any depth: `'**/dist/**'` and `'**/node_modules/**'`. Confirmed first (per the dispatch order) that no example, template, or docs fixture in this repository authors a schema under a directory literally named `dist` — see the PR body for the grep and its control term. Adds `check-nested-dist-ignore.test.ts`: plants a nested `dist/` and a nested `node_modules/` fixture and asserts neither is scanned, while a root-level `dist/` / `node_modules/` remains excluded (the regression guard for the fix itself — widening a pattern is exactly where an exclusion can accidentally stop covering the case it already handled). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012CZgmFFzqA9cX8tBMhvpFe --- .changeset/6320-check-nested-dist-ignore.md | 24 ++++ .../check-nested-dist-ignore.test.ts | 134 ++++++++++++++++++ packages/cli/src/commands/check.ts | 14 +- 3 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 .changeset/6320-check-nested-dist-ignore.md create mode 100644 packages/cli/src/__tests__/check-nested-dist-ignore.test.ts diff --git a/.changeset/6320-check-nested-dist-ignore.md b/.changeset/6320-check-nested-dist-ignore.md new file mode 100644 index 0000000000..9f99ca1fc5 --- /dev/null +++ b/.changeset/6320-check-nested-dist-ignore.md @@ -0,0 +1,24 @@ +--- +'@object-ui/cli': patch +--- + +Fix `objectui check` scanning build output because its ignore list only excluded a +root-level `dist/` / `node_modules/` (objectui#6320). + +`packages/cli/src/commands/check.ts` passed `ignore: ['node_modules/**', 'dist/**', +'.git/**']` to `globSync`. `glob` matches `ignore` patterns against the path relative to +`cwd`, so an unanchored `dist/**` / `node_modules/**` excludes only a directory of that +name at the scan root — every nested `packages//dist/`, `examples//dist/`, +`apps//dist/` (and their `node_modules/`) was still scanned. In a built workspace +this means `objectui check` re-reads the author's own schemas a second time from build +output, roughly doubling every count it reports (measured on this repository: 617 → 1047 +files globbed after a full build) with nothing in the output explaining why. + +The ignore patterns are now anchored at every depth (`'**/dist/**'`, `'**/node_modules/**'`), +matching the fix's stated intent: exclude build output and installed dependencies wherever +they live, not only at the project root. A root-level `dist/` / `node_modules/` remains +excluded, unchanged. + +Confirmed before widening: no example, template, or docs fixture in this repository +authors a schema under a directory literally named `dist` — the widened pattern excludes +only generated content. diff --git a/packages/cli/src/__tests__/check-nested-dist-ignore.test.ts b/packages/cli/src/__tests__/check-nested-dist-ignore.test.ts new file mode 100644 index 0000000000..a1957047ab --- /dev/null +++ b/packages/cli/src/__tests__/check-nested-dist-ignore.test.ts @@ -0,0 +1,134 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `objectui check`'s ignore list matches `glob`'s `ignore` patterns against + * the path RELATIVE TO `cwd` (objectui#6320). An unanchored ignore pattern + * (root-level "dist" or "node_modules" only) therefore excludes only a + * directory of that name AT THE SCAN ROOT — every nested build-output or + * dependency directory one level or more down (a package's own "dist", + * an example's own "node_modules", and so on) was still scanned, so a built + * workspace re-read the author's own schemas a second time, from build + * output. Measured on this repository: every reported count roughly doubles + * once packages are built. + * + * This file pins two things at once, because widening a pattern is exactly + * where an exclusion can accidentally stop covering the case it already + * handled: + * - a NESTED `dist/` / `node_modules/` is now excluded (the fix); + * - a ROOT-level `dist/` / `node_modules/` is STILL excluded (the regression + * guard for the fix itself). + * + * Fixtures live under `os.tmpdir()`, never in the repo tree: `check()` globs + * every JSON file under the directory it is handed, so a fixture committed + * inside this workspace would be scanned by every other run of the command as + * well — including the repo's own `pnpm check` (see `check-schema-marker.test.ts`). + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { tmpdir } from 'node:os'; + +import { check } from '../commands/check.js'; + +let cwd: string; +let lines: string[]; +let restoreLog: () => void; + +/** + * The CSI sequences chalk may add. The escape byte is built with + * `String.fromCharCode` rather than spelled into the source, so this file + * holds no raw control character (objectui AGENTS.md byte discipline). + */ +const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'); + +function plainLines(): string[] { + return lines.map((l) => l.replace(ANSI, '')); +} + +function unknownTypeWarnings(): string[] { + return plainLines().filter((l) => l.includes('Unknown schema type')); +} + +/** Was a file whose basename is `name` warned about? A file the scan never + * globbed prints nothing at all, so absence here means "not scanned". */ +function warnedAbout(name: string): boolean { + return unknownTypeWarnings().some((l) => l.includes(name)); +} + +/** + * Write a fixture that is unmistakable if — and only if — `check()` globs it: + * it positively reads as an ObjectUI schema (the structural key `children`), + * and its `type` is deliberately unregistered, so a scanned copy prints an + * "Unknown schema type" warning naming its own path. Directories are created + * as needed, so `relPath` may nest arbitrarily deep. + */ +function writeProbe(relPath: string): void { + const abs = join(cwd, relPath); + mkdirSync(dirname(abs), { recursive: true }); + writeFileSync(abs, JSON.stringify({ type: 'totally-made-up-xyz', children: [] })); +} + +beforeEach(() => { + cwd = mkdtempSync(join(tmpdir(), 'objectui-check-nested-ignore-')); + lines = []; + const original = console.log; + console.log = (...args: unknown[]) => { + lines.push(args.map(String).join(' ')); + }; + // `check()` calls `process.exit(1)` itself; none of these fixtures are + // malformed JSON, but mock it anyway so a surprise does not tear the + // Vitest worker down (same convention as check-schema-marker.test.ts). + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never); + restoreLog = () => { + console.log = original; + exitSpy.mockRestore(); + }; +}); + +afterEach(() => { + restoreLog(); + rmSync(cwd, { recursive: true, force: true }); +}); + +describe('objectui check — the ignore list is anchored at every depth (objectui#6320)', () => { + it('does not scan a NESTED dist/ directory', async () => { + writeProbe('packages/x/dist/nested-dist-file.json'); + // Counter-probe, outside dist/: without it, silence above would be + // indistinguishable from a scan that read nothing at all. + writeProbe('packages/x/src/sibling-file.json'); + await check(cwd); + expect(warnedAbout('nested-dist-file.json')).toBe(false); + expect(warnedAbout('sibling-file.json')).toBe(true); + }); + + it('does not scan a NESTED node_modules/ directory', async () => { + writeProbe('packages/y/node_modules/some-dep/nested-node-modules-file.json'); + writeProbe('packages/y/src/sibling-file.json'); + await check(cwd); + expect(warnedAbout('nested-node-modules-file.json')).toBe(false); + expect(warnedAbout('sibling-file.json')).toBe(true); + }); + + it('still excludes a ROOT-level dist/ — widening must not stop covering the case it already handled', async () => { + writeProbe('dist/root-dist-file.json'); + writeProbe('src/sibling-file.json'); + await check(cwd); + expect(warnedAbout('root-dist-file.json')).toBe(false); + expect(warnedAbout('sibling-file.json')).toBe(true); + }); + + it('still excludes a ROOT-level node_modules/ — same regression guard', async () => { + writeProbe('node_modules/some-dep/root-node-modules-file.json'); + writeProbe('src/sibling-file.json'); + await check(cwd); + expect(warnedAbout('root-node-modules-file.json')).toBe(false); + expect(warnedAbout('sibling-file.json')).toBe(true); + }); +}); diff --git a/packages/cli/src/commands/check.ts b/packages/cli/src/commands/check.ts index aaab20fd11..4602e7b8f6 100644 --- a/packages/cli/src/commands/check.ts +++ b/packages/cli/src/commands/check.ts @@ -200,9 +200,17 @@ export async function check(cwd: string = process.cwd()) { console.log(chalk.bold('Object UI Schema Check')); // 1. Find all JSON/YAML files - const files = globSync('**/*.{json,yaml,yml}', { - cwd, - ignore: ['node_modules/**', 'dist/**', '.git/**'] + const files = globSync('**/*.{json,yaml,yml}', { + cwd, + // `glob` matches `ignore` patterns against the path relative to `cwd`, so + // an unanchored `dist/**`/`node_modules/**` excludes only a directory of + // that name at the scan root — every nested `packages/*/dist/`, + // `examples/*/dist/`, `apps/*/dist/` (and their `node_modules/`) is still + // scanned. `**/` anchors the match at any depth (objectui#6320): measured + // on this repository, a built tree without it reports roughly double the + // files of a clean checkout, almost all of it re-reading the author's own + // schemas from build output. + ignore: ['**/node_modules/**', '**/dist/**', '.git/**'] }); console.log(`Analyzing ${files.length} files...`);