Add #147: subpath binding via the exports map (--subpaths) - #153
Merged
Conversation
…gistry) Modern packages expose distinct modules per subpath (@mui/material/styles, per-part Radix, base-ui ./accordion/./button, clsx ./lite), each with its own .d.ts behind an exports["./sub"] condition. #104 bound only the main `.` entry; --subpaths now binds every concrete subpath too, each stamped @module("pkg/sub"), with shared types emitted ONCE. - resolve.mjs: packageEntries() enumerates the exports map — main `.` first (via the robust typesEntry), then each concrete `./sub` with a resolvable `types` condition (skips `./*` wildcards, CSS, package.json), deduped by file. resolveInput returns subEntries (suffix-based so --from rebases them). - extract.mjs: extractModule now walks MULTIPLE entries in ONE program + ONE shared registry (opts.entries = [{from, entry}]); each binding carries its own `from`; a type referenced from several subpaths is deduped by type.id, homed by its declaring file. Post-passes run once. Single-entry callers get entries=[{from,entry}] -> the loop runs once -> byte-identical (verified: 102 goldens unchanged). - emit.mjs: no change — @module already stamped from ir.import.from per-binding. - cli.mjs: --subpaths flag + entries wiring + help. Off by default, so existing output and the benchmark are byte-identical. A symbol re-exported under the same name from multiple subpaths binds once (main `.` first); different-named subpaths (the common case) each bind with their subpath specifier. Fixture: subpath-binding (main Button + ./styles createTheme sharing Theme). 4 smoke checks. Verified live on clsx (./lite). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hadow visible Review follow-up on #147. The 4 CodeQL alerts were PRE-EXISTING (introduced in the v1.0.0 commit 6aea704, verified via git) and only re-attributed to this PR because packageEntries shifted their line numbers into the changed region. Fixing them here since they gate the merge (and pkg.pr.new): - ReDoS (high): the scoped-package-name regex `/(@[^/]+\/[^@]+)@.*/` is a polynomial backtracking shape. Replaced with linear string ops (the version `@` is just the first `@` after the scope's `/`). Verified byte-identical to the old regex across scoped/unscoped/versioned specs. - shell-injection (medium x3): the two `execSync(\`npm install ${spec} …\`)` calls interpolated input into a shell string. Switched to `execFileSync('npm', [args])` (no shell) with REACT_TYPE_DEPS as an argv array — a package spec can no longer be interpreted as a shell command. Behaviour is identical (no shell features were used). Also, review points 2 & 3 on the subpath feature itself: - #147 point 2: a same-name DIFFERENT symbol across subpaths (clsx `.` vs `./lite` `clsx`) is now REPORTED as skipped (`subpath-name-shadowed`) instead of dropped silently — a benign re-export (SAME symbol) stays silent. Tracked via a per-name first-symbol map. - #147 point 3: fixture extended with a `./button` subpath that RE-EXPORTS the main Button (same symbol -> binds once as @module("demo"), no skip) + an `alt.d.ts` with a different Button for the collision path. 2 new smoke checks cover both. npm-install paths aren't exercised by the offline suite; the change is a mechanical shell->argv refactor. Goldens/compile/benchmark all unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md # test/smoke.mjs
Benchmark: ✅ PASS
|
commit: |
jagguji
added a commit
that referenced
this pull request
Jul 22, 2026
The repo has **9 open CodeQL alerts on `main`** (visible in the default-branch scan). None come from any feature PR — they get re-attributed onto large-regeneration PRs (#153, #156) by CodeQL's "changes too large" re-scan, showing up as a red aggregate **CodeQL** check even though the `Analyze` jobs pass. This clears the root debt so that check goes green. ## Fixes **Code — `js/file-system-race` (high ×3) + `js/identity-replacement` (medium)** - `test/lib/diff.mjs` — `readdirSync(dir, { withFileTypes: true })` instead of `readdir` + a separate `statSync` (the stat is a check-then-read TOCTOU: the file could vanish between the two syscalls). - `src/cli.mjs` — `unlink` directly inside the existing try/catch instead of `existsSync`-then-`unlink` (the check-then-unlink is the race; the catch already handles "already gone"). - `benchmark/run.mjs` — read the prior lock-stamp via try/catch instead of `existsSync`-then-`readFileSync`; and drop the `.replace(/^_/, '_')` in `slugOf`, which was an **identity no-op** (`"@s/p"` → `"_s_p"` already starts with `_`). **Workflows — `actions/missing-workflow-permissions` (medium ×3) + `actions/unpinned-tag` (medium ×2)** - `ci.yml` — add `permissions: contents: read` to the `test` / `compile` / `fixture-guard` jobs (least-privilege; the `preview` job already declared its own). - `benchmark.yml` + `yama-review.yml` — pin `marocchino/sticky-pull-request-comment` and `juspay/yama` to their release commit SHAs, with the `# v3` / `# v2.7.1` version as trailing comments (the CodeQL-recommended form). *Note: `yama-review.yml` is generated by juspay/yama's setup script, so a future regen may need the pin re-applied.* ## Safety All behaviour-preserving — verified: **104 goldens match + compile**, **benchmark byte-identical** (the `slugOf` and lock-stamp changes are proven by the baselines still resolving), and the workflows parse. No `src/` runtime logic changed (the `cli.mjs` edit is the `--clean` stale-file removal, same effect). After this merges, the pre-existing alerts clear repo-wide and the red **CodeQL** check on #156 (and future large-regen PRs) goes green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-on to #104 (main-entry
exportsresolution). Closes #147.Problem
Modern packages expose distinct modules per subpath —
@mui/material/styles,date-fns/format, per-part Radix, base-ui's./accordion/./button, clsx's./lite— each with its own.d.tsbehind anexports["./sub"]condition. #104 resolves only the main.entry, so those subpaths were never bound.Fix —
--subpaths(unified registry, Option A)Bind every concrete subpath too, each stamped
@module("pkg/sub"), while emitting shared types once:resolve.mjs—packageEntries()enumerates theexportsmap: main.first (via the robusttypesEntry), then each concrete./subwith a resolvabletypescondition. Wildcard ("./*"), CSS, andpackage.jsonsubpaths are skipped; entries deduped by file.resolveInputreturnssubEntries(suffix-based, so--fromrebases them).extract.mjs—extractModulenow walks multiple entries in ONE TypeScript program + ONE shared registry (opts.entries = [{from, entry}]). Each binding carries its ownfrom; a type referenced from several subpaths is deduped (keyed bytype.id, homed by its declaring file — not by which subpath saw it). Post-passes run once. Single-entry callers getentries=[{from,entry}]→ the loop runs once → byte-identical (my safety net; the 102 goldens confirm).emit.mjs— no change:@moduleis already stamped fromir.import.fromper-binding.cli.mjs—--subpathsflag + wiring + help.Result
Verified live on clsx (
--subpathsdetectsclsx/lite).Off by default
--subpathsis opt-in, so existing single-entry output — and the benchmark (byte-identical on all 9 packages) — is unchanged. The survey that drove this: hono (73 subpaths), base-ui (42), clsx (1) have real code subpaths, so default-on would explode those baselines; the pure-CSS subpaths (day-picker, react-rating) are auto-skipped by thetypes-condition filter.Verification
@module, shared-type dedup, enumerator).subpath-binding(mainButton+./stylescreateThemesharingTheme).Documented limitation
A symbol exported under the same name by multiple subpaths (clsx's
clsxin both.and./lite) binds once, main.wins. This is correct for re-exports and for different-named subpaths (base-ui/@mui— the common case); the same-name-across-subpaths case keeps the main binding rather than disambiguating (a possible future refinement).🤖 Generated with Claude Code