Skip to content

fix(resolution): workspace globs reach nested members, and a negated glob leaves members out - #2638

Merged
colbymchenry merged 2 commits into
mainfrom
claude/nested-workspace-globs
Oct 18, 2026
Merged

colbymchenry merged 2 commits into
mainfrom
claude/nested-workspace-globs

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

What

src/resolution/workspace-packages.ts → expandWorkspaceGlob read only the folders directly under the text before a glob's first *, and took a negated glob for a folder name. On main (53ad0d4):

  • Nested members were missed. pnpm's packages/**/* (folo) and npm's libs/**/* (bitwarden) list members at any depth. folo's @follow/utils is packages/internal/utils, two levels down, so it was no member. isExternalImport('@follow/utils/lru-cache', …) returned true, and 2,411 @follow/* imports counted as outside packages'.
  • A wildcard inside a folder name matched nothing. packages/plugin-* listed a folder called packages/plugin-, and apps/*/web stopped at apps/*.
  • Negations did nothing. !**/example, react-native's !private/helloworld and refine's !examples/with-nx left those packages members.

No issue reported this; it turned up while validating #2637. There, folo's offsetCache.get / measurementsCache.get stayed unlinked after a sync: their class comes from @follow/utils/lru-cache, which counted as an outside import, so the failed calls were never parked under the module.

Fix

Globs match folder by folder, like a small glob engine:

  • *, ? and {a,b} within a name, and ** across folders.
  • The walk is breadth-first with sorted names, so a member nearer the root keeps a package name a deeper copy also declares.
  • A wildcard never enters a dot-folder, node_modules or bower_components. A ** also skips test and tests folders, as pnpm's default ignores do: their fixture packages aren't members, and their names (react, lodash) would make imports of the real package the project's.
  • A folder the glob names literally is always entered (e2e/tests/* still works).
  • tools/** includes tools itself, as npm and pnpm match tools/**/package.json.
  • What a negated glob matches is not a member, wherever the negation is listed.
  • The walk is bounded: 8 levels below the glob's literal prefix and 10,000 folders per glob.

The first line of expandWorkspaceGlob is unchanged. #2009 edits exactly that line, so whichever lands second resolves a one-line conflict.

Overlap with open PRs: #2535 (workspace exports/main entries, #2502) and #2461 (file: dependencies, #2456) also edit this file, in the header comment and other functions. Whichever lands second gets a small conflict in the header's bullet list.

With #2535, folo's @follow/* imports would also resolve to their src/ files. This PR only makes them members: their files sit under src/ behind exports, which directory-based subpath resolution doesn't reach.

Validation

Fresh-index A/B vs main

Byte-identical nodes, edges and files in all 10 repos:

repo layout members main → fix refs
folo pnpm packages/**/* + !**/example 13 → 25 (@follow/utils, @follow/components, …) 349 failed refs through @follow/* bindings move to a module key (calls get 39, set 34, …; imports 19); nothing else
bitwarden npm libs/**/* 44 → 55 (libs/tools/export-vault-core, …) identical
react-native npm !private/helloworld 31 → 30 identical
refine-examples pnpm with 8 negations 209 → 169 (the excluded examples) identical
medusa (partial copy) npm integration-tests/**/* 1 → 1 identical
vben, excalidraw explicit globs same identical
milo-ui, ghostfolio, infrahub no workspaces none identical

So the newly found members change no fresh edge here:

Sync vs a fresh index (folo)

Every 7th file is held out, emptied, or deleted and then restored. Each arm's synced graph is compared with its own fresh index; numbers are differing dump lines.

scenario main fix relinked
gut (emptied, then restored) 174 170 offsetCache.get, measurementsCache.get
drop (deleted, then restored) 198 194 the same two
hold (added later) 62 62 —

Those are the two folo residues #2637 left. Their class comes from @follow/utils/lru-cache, which is now a project module, so the failed calls wait under module:lru-cache. Every other residue bucket is identical on both arms. The drop scenario's delete-step sync fails with FOREIGN KEY constraint failed on both arms; that is #2565, fixed in the open #2574.

Tests

__tests__/workspace-nested-globs.test.ts has 7 tests; 5 fail on main 53ad0d4 (run in a detached worktree):

  • pnpm packages/**/*;
  • npm libs/**/*, with tools/** counting tools itself;
  • packages/plugin-* and {apps,sites}/*/web;
  • negations, quoted and bare, listed before and after the globs they cut;
  • end to end, a call through import { fmt as format } from '@acme/utils' links to the nested member's fmt.

The 2 that pass on main guard the new walk:

  • installed packages, hidden folders and a ** walk's test/fixtures/react are no members, while e2e/tests/* is;
  • the member nearest the root keeps a duplicated name.

Mutations:

  • Without the test/tests skip under **, the fixtures guard fails.
  • Path order instead of nearest-first fails the duplicate-name guard.

Full suite (Windows) on this branch: 593 files passed (6 skipped), 7,262 tests passed, 0 failed (82 skipped).

The branch merges cleanly with #2639 (TS array-annotated receivers).

🤖 Generated with Claude Code

colbymchenry and others added 2 commits October 18, 2026 15:24
…glob leaves members out

pnpm's `packages/**/*` (folo) and npm's `libs/**/*` (bitwarden) list
member packages at any depth, `packages/plugin-*` and `apps/*/web` name
them by part of a folder name or by a folder below the wildcard, and a
negated glob (`!**/example`, `!examples/with-nx`) leaves some out. The
workspace loader listed only the folders directly under the text before
the first `*` and took a negation for a folder name, so folo's
`@follow/utils`, two levels under `packages/`, was no member and every
import of it counted as an outside package's.

Globs now match folder by folder: `*`, `?` and `{a,b}` within a name,
`**` across folders, breadth-first so a member nearer the root keeps a
name a deeper copy also declares. A wildcard never enters a dot-folder,
`node_modules` or `bower_components`, and a `**` skips `test` and `tests`
folders as pnpm's default ignores do; a folder the glob names literally
is always entered. What a negated glob matches is no member.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant