Skip to content

fix(cdn): exclude packages from CDN resolver including transitive deps#36

Merged
bennypowers merged 2 commits into
mainfrom
fix/cdn-exclude-transitive-deps
Apr 29, 2026
Merged

fix(cdn): exclude packages from CDN resolver including transitive deps#36
bennypowers merged 2 commits into
mainfrom
fix/cdn-exclude-transitive-deps

Conversation

@bennypowers

@bennypowers bennypowers commented Apr 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add WithExclude to CDN resolver, filtering excluded packages from both direct and transitive dependencies
  • Fix pre-existing race condition in PackageCache.GetOrLoad where fast path read entry fields while sync.Once loader was still running
  • Wire exclude through WASM generate() API and update TypeScript GenerateOptions type

Details

The CDN resolver lacked exclude support entirely. When using generate() with the exclude option, excluded packages still appeared in the output import map as transitive dependencies of non-excluded packages. This broke monorepo workflows where workspace packages should be served locally rather than from CDN.

The race fix stores the loader function in cacheEntry so any goroutine's entry.load() call uses the correct loader via sync.Once synchronization, rather than the old pattern where the fast path could read entry.pkg before the loader finished.

Closes #32

Test plan

  • TestResolverWithExclude/without_exclude_both_present -- baseline, both packages resolved
  • TestResolverWithExclude/transitive_deps_produce_scopes -- proves lib-b appears in scopes before exclusion
  • TestResolverWithExclude/exclude_direct_dependency -- lib-b removed from imports
  • TestResolverWithExclude/exclude_transitive_dependency -- lib-b removed from imports and scopes when only lib-a is a direct dep
  • make lint passes (0 issues)
  • make test passes (331 tests, -race enabled)

Summary by CodeRabbit

  • New Features

    • Added package exclusion functionality to dependency resolution, enabling users to specify packages that should be excluded from import maps, including their transitive dependencies.
  • Tests

    • Added test coverage for package exclusion scenarios, including direct and transitive dependency filtering.
  • Refactor

    • Optimized internal cache loading architecture.

The CDN resolver lacked exclude support entirely. When using generate()
with the exclude option, excluded packages still appeared in the output
import map if they were transitive dependencies of non-excluded packages.

Add WithExclude builder to CDN resolver, filtering excluded packages
from both direct dependencies in ResolvePackageJSON and transitive
dependencies in resolvePackage. Wire exclude through WASM generate()
and update TypeScript GenerateOptions type.

Also fix a pre-existing race condition in PackageCache.GetOrLoad where
the fast path could read entry.pkg/entry.err while another goroutine's
sync.Once loader was still running. Store the loader function in the
cache entry so any goroutine calling entry.load() uses the correct
loader via sync.Once synchronization.

Closes #32

Assisted-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@bennypowers has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 53 minutes and 50 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0b26c420-f2a7-4bf2-831d-0e374c2c8a71

📥 Commits

Reviewing files that changed from the base of the PR and between 9afdbff and f34c7fe.

📒 Files selected for processing (2)
  • cdn/cache.go
  • resolve/cdn/cdn_test.go
📝 Walkthrough

Walkthrough

This pull request implements package exclusion support for CDN-based import map generation. The changes add an exclude option to the public API, implement filtering logic in the resolver to remove excluded packages from both direct and transitive dependencies, refactor cache synchronization logic, and extend the WASM interface to support the new exclusion feature alongside comprehensive test coverage.

Changes

Cohort / File(s) Summary
Cache synchronization refactor
cdn/cache.go
Refactors GetOrLoad to delegate lazy-loading and sync.Once synchronization to a new cacheEntry.load() method, removing manual branching logic and centralizing per-key loading behavior.
Public API extension
npm/src/mappa.ts
Extends GenerateOptions interface with optional exclude: string[] property to allow callers to specify packages that should be omitted from output.
Resolver exclusion implementation
resolve/cdn/cdn.go
Adds excludePackages configuration field, introduces WithExclude setter method, and implements filtering logic to remove excluded packages from initial dependency resolution and transitive dependency traversal using slices.Contains.
Exclusion test suite
resolve/cdn/cdn_test.go
Introduces TestResolverWithExclude validating that excluded packages are filtered from both direct dependencies in im.Imports and transitive dependencies across all scope entries.
Test fixture data
resolve/cdn/testdata/lib-{a,b}-{package,registry}/*
Adds test data files (package.json and registry response.json) for lib-a and lib-b packages with conditional module exports and dependency declarations to support exclusion testing.
WASM surface integration
wasm/main.go
Extends generateOptions to include exclude []string, parses the exclude list from JS arguments, and propagates it to the CDN resolver via WithExclude when provided.

Sequence Diagram

sequenceDiagram
    actor Client
    participant WASM as WASM Layer
    participant Resolver as Resolver
    participant Cache as PackageCache
    participant Registry as Registry

    Client->>WASM: generate(deps, {exclude: [...]})
    WASM->>WASM: parseGenerateOptions(exclude)
    WASM->>Resolver: WithExclude(packages)
    Resolver->>Resolver: store excludePackages
    WASM->>Resolver: ResolvePackageJSON(deps)
    Resolver->>Resolver: filter direct deps (remove excluded)
    Resolver->>Resolver: resolvePackage(each dep, transitive)
    alt Package in transitive path?
        Resolver->>Resolver: check slices.Contains(excludePackages)
        Resolver->>Resolver: skip if excluded
    else Package not excluded
        Resolver->>Cache: GetOrLoad(pkg)
        Cache->>Registry: fetch if needed
        Registry-->>Cache: version data
        Cache-->>Resolver: return pkg info
    end
    Resolver->>Resolver: build im.Imports & im.Scopes
    Resolver-->>WASM: ImportMap (no excluded pkgs)
    WASM-->>Client: ImportMap
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • PR #31: Implements complementary package exclusion logic via WithExclude and filters packages during resolution to prevent excluded packages from appearing in output.
  • PR #33: Extends the GenerateOptions interface with exclude option and propagates exclusion handling through WASM and resolver layers.
  • PR #7: Refactors the same GetOrLoad concurrency pattern in cdn/cache.go through centralized cacheEntry and sync.Once-driven lazy loading.

Poem

🐰 A rabbit hops through filtered trees,
Excluding branches with such ease—
No transitive paths shall slip through here,
Cached wisely, once per load so clear!
The warren's imports, pure and bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding package exclusion to the CDN resolver with support for transitive dependencies.
Linked Issues check ✅ Passed The pull request comprehensively addresses issue #32 by implementing exclusion filtering at both direct and transitive dependency levels throughout the resolver.
Out of Scope Changes check ✅ Passed All changes relate to the core objectives: CDN resolver exclusion implementation, race condition fix in PackageCache, WASM API integration, and supporting tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cdn-exclude-transitive-deps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 53 minutes and 50 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@bennypowers

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Apr 29, 2026

Copy link
Copy Markdown

Build Artifacts

OS x64 arm64
Linux mappa-linux-x64 mappa-linux-arm64
macOS mappa-darwin-x64 mappa-darwin-arm64
Windows mappa-win32-x64 mappa-win32-arm64

Built from a62e59e @ fix/cdn-exclude-transitive-deps

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cdn/cache.go (1)

138-148: ⚠️ Potential issue | 🔴 Critical

Fix eviction order in GetOrLoad to avoid under-capacity behavior and edge panic.

At Line 143, eviction runs after inserting the new entry with >=. This can evict too early (effective capacity becomes maxSize-1) and can panic at Line 144 when maxSize == 1 and order is still empty.

🐛 Proposed fix
-	// Create new entry with loader
-	entry = &cacheEntry{loader: loader}
-	c.entries[key] = entry
-
-	// Evict oldest if at capacity
-	if len(c.entries) >= c.maxSize {
+	// Evict oldest if at capacity
+	if len(c.entries) >= c.maxSize {
 		oldest := c.order[0]
 		c.order = c.order[1:]
 		delete(c.entries, oldest)
 	}
+	// Create new entry with loader
+	entry = &cacheEntry{loader: loader}
+	c.entries[key] = entry
 	c.order = append(c.order, key)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cdn/cache.go` around lines 138 - 148, The eviction logic in GetOrLoad
currently runs after inserting the new entry and uses >= which can underflow
capacity and panic; move the eviction block to run before creating/inserting the
new entry and change the check to compare equality (if len(c.entries) ==
c.maxSize) so you evict exactly one oldest key from c.order (oldest :=
c.order[0]; c.order = c.order[1:]; delete(c.entries, oldest)) prior to setting
c.entries[key] = entry, then append the new key to c.order; reference symbols:
GetOrLoad, c.entries, c.order, c.maxSize, key, entry.
🧹 Nitpick comments (2)
resolve/cdn/cdn.go (1)

189-201: Consider copying packages in WithExclude to keep resolver instances immutable.

Right now excludePackages points at the caller-provided slice. A later mutation by the caller can alter resolver behavior unexpectedly.

♻️ Proposed hardening
 func (r *Resolver) WithExclude(packages []string) *Resolver {
+	excluded := append([]string(nil), packages...)
 	return &Resolver{
 		fetcher:         r.fetcher,
 		provider:        r.provider,
 		registry:        r.registry,
 		template:        r.template,
 		cache:           r.cache,
 		logger:          r.logger,
 		conditions:      r.conditions,
 		includeDev:      r.includeDev,
-		excludePackages: packages,
+		excludePackages: excluded,
 		maxDepth:        r.maxDepth,
 		resolveScope:    r.resolveScope,
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resolve/cdn/cdn.go` around lines 189 - 201, WithExclude currently assigns the
caller's slice directly to the Resolver.excludePackages field which preserves
aliasing; change WithExclude to allocate a new slice and copy the elements into
it before storing (e.g., create make([]string, len(packages)); copy(...)) so the
returned Resolver owns an independent slice and cannot be mutated by the caller;
update the return construction in WithExclude to use that copied slice
(reference symbols: WithExclude, excludePackages, Resolver).
resolve/cdn/cdn_test.go (1)

251-256: Move inline PackageJSON inputs to fixtures for consistency with repo test style.

These inline inputs work, but this repo’s test guidance prefers fixture-based inputs/goldens in testdata/ for maintainability and reuse.

As per coding guidelines, "Don't inline source code in tests; use fixture files instead with input files and expected output".

Also applies to: 273-277, 312-316

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resolve/cdn/cdn_test.go` around lines 251 - 256, The test currently
constructs packagejson.PackageJSON inline (pkg := &packagejson.PackageJSON{...})
which violates the repo style; instead create JSON fixture files under testdata
(one per case covering the instances at the noted locations) containing the same
Dependencies map, then update the tests to read and unmarshal those fixtures
into a packagejson.PackageJSON (e.g., os.ReadFile + json.Unmarshal into a
variable named pkg) before asserting; replace the three inline occurrences (the
pkg := &packagejson.PackageJSON blocks at the referenced spots) with code that
loads the corresponding fixture file and unmarshals it into
packagejson.PackageJSON so tests use testdata fixtures consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@resolve/cdn/cdn_test.go`:
- Around line 304-307: The test's exclusion assertions on im.Imports (and
similarly on im.Scopes) are too narrow — they only check exact keys "lib-b" and
"lib-b/helpers" and miss other subpaths; update the checks in the loop over
im.Imports (and the loop over im.Scopes around lines 326-336) to reject any key
that equals "lib-b" or has the prefix "lib-b/" (e.g., use a prefix check such as
strings.HasPrefix(k, "lib-b") or strings.HasPrefix(k, "lib-b/") to fail the test
on any subpath), and apply the same prefix-based logic to the scopes loop to
ensure no "lib-b" subpaths are emitted.

---

Outside diff comments:
In `@cdn/cache.go`:
- Around line 138-148: The eviction logic in GetOrLoad currently runs after
inserting the new entry and uses >= which can underflow capacity and panic; move
the eviction block to run before creating/inserting the new entry and change the
check to compare equality (if len(c.entries) == c.maxSize) so you evict exactly
one oldest key from c.order (oldest := c.order[0]; c.order = c.order[1:];
delete(c.entries, oldest)) prior to setting c.entries[key] = entry, then append
the new key to c.order; reference symbols: GetOrLoad, c.entries, c.order,
c.maxSize, key, entry.

---

Nitpick comments:
In `@resolve/cdn/cdn_test.go`:
- Around line 251-256: The test currently constructs packagejson.PackageJSON
inline (pkg := &packagejson.PackageJSON{...}) which violates the repo style;
instead create JSON fixture files under testdata (one per case covering the
instances at the noted locations) containing the same Dependencies map, then
update the tests to read and unmarshal those fixtures into a
packagejson.PackageJSON (e.g., os.ReadFile + json.Unmarshal into a variable
named pkg) before asserting; replace the three inline occurrences (the pkg :=
&packagejson.PackageJSON blocks at the referenced spots) with code that loads
the corresponding fixture file and unmarshals it into packagejson.PackageJSON so
tests use testdata fixtures consistently.

In `@resolve/cdn/cdn.go`:
- Around line 189-201: WithExclude currently assigns the caller's slice directly
to the Resolver.excludePackages field which preserves aliasing; change
WithExclude to allocate a new slice and copy the elements into it before storing
(e.g., create make([]string, len(packages)); copy(...)) so the returned Resolver
owns an independent slice and cannot be mutated by the caller; update the return
construction in WithExclude to use that copied slice (reference symbols:
WithExclude, excludePackages, Resolver).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 39257635-07df-439a-a751-e7cd4a26e535

📥 Commits

Reviewing files that changed from the base of the PR and between 1954571 and 9afdbff.

📒 Files selected for processing (9)
  • cdn/cache.go
  • npm/src/mappa.ts
  • resolve/cdn/cdn.go
  • resolve/cdn/cdn_test.go
  • resolve/cdn/testdata/lib-a-package/package.json
  • resolve/cdn/testdata/lib-a-registry/response.json
  • resolve/cdn/testdata/lib-b-package/package.json
  • resolve/cdn/testdata/lib-b-registry/response.json
  • wasm/main.go

Comment thread resolve/cdn/cdn_test.go
Use strings.HasPrefix for lib-b exclusion checks instead of exact key
matching, so any subpath is caught.

Move eviction before insertion in GetOrLoad to match Set() ordering and
prevent panic on empty c.order with small maxSize. Change >= to == since
the check now runs before the new entry is added.

Assisted-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bennypowers bennypowers merged commit a79a8a2 into main Apr 29, 2026
11 checks passed
@bennypowers bennypowers deleted the fix/cdn-exclude-transitive-deps branch April 29, 2026 15:22
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.

CDN generate resolves excluded packages as transitive deps

1 participant