Skip to content

Commit 05a81ac

Browse files
nicksrandallclaude
andauthored
feat: Support namespace-based external marking (#1188)
* feat: Support namespace-based external marking Allow marking entire package namespaces as external using the external query parameter. For example, using ?external=@radix-ui will mark all @radix-ui/* packages as external. This simplifies configuration when working with packages that have many sub-packages from the same namespace, eliminating the need to list each package individually. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * chore: Remove accidentally committed binary file Remove the esm.sh binary that was accidentally included in the previous commit. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent dc750a7 commit 05a81ac

File tree

6 files changed

+53
-4
lines changed

6 files changed

+53
-4
lines changed

server/build.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,8 +569,8 @@ func (ctx *BuildContext) buildModule(analyzeMode bool) (meta *BuildMeta, include
569569
}
570570

571571
// bundles all dependencies in `bundle` mode, apart from peerDependencies and `?external` flag
572-
if ctx.bundleMode == BundleDeps && !ctx.args.External.Has(toPackageName(specifier)) && !implicitExternal.Has(specifier) {
573-
pkgName := toPackageName(specifier)
572+
pkgName := toPackageName(specifier)
573+
if ctx.bundleMode == BundleDeps && !ctx.args.External.Has(pkgName) && !isPackageInExternalNamespace(pkgName, ctx.args.External) && !implicitExternal.Has(specifier) {
574574
_, ok := pkgJson.PeerDependencies[pkgName]
575575
if !ok {
576576
return esbuild.OnResolveResult{}, nil

server/build_args.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,19 @@ func resolveBuildArgs(npmrc *NpmRC, installDir string, args *BuildArgs, esm EsmP
226226
}
227227
continue
228228
}
229+
// Check if this is a namespace pattern (e.g., @radix-ui)
230+
isNamespace := strings.HasPrefix(name, "@") && !strings.Contains(name[1:], "/")
231+
if isNamespace {
232+
// Add all packages from this namespace that are in deps
233+
for _, dep := range deps.Values() {
234+
if strings.HasPrefix(dep, name+"/") {
235+
external = append(external, dep)
236+
}
237+
}
238+
// Also keep the namespace itself in external for pattern matching
239+
external = append(external, name)
240+
continue
241+
}
229242
// if the subModule externalizes the package entry
230243
if name == esm.PkgName && esm.SubPath != "" {
231244
external = append(external, name)

server/build_resolver.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,8 @@ func (ctx *BuildContext) resolveExternalModule(specifier string, kind esbuild.Re
685685
}()
686686

687687
// check `?external`
688-
if ctx.externalAll || ctx.args.External.Has(toPackageName(specifier)) {
688+
packageName := toPackageName(specifier)
689+
if ctx.externalAll || ctx.args.External.Has(packageName) || isPackageInExternalNamespace(packageName, ctx.args.External) {
689690
resolvedPath = specifier
690691
return
691692
}

server/dts_transform.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ func transformDTS(ctx *BuildContext, dts string, buildArgsPrefix string, marker
189189
}
190190

191191
// respect `?external` query
192-
if ctx.externalAll || ctx.args.External.Has(depPkgName) {
192+
if ctx.externalAll || ctx.args.External.Has(depPkgName) || isPackageInExternalNamespace(depPkgName, ctx.args.External) {
193193
return specifier, nil
194194
}
195195

server/path.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/Masterminds/semver/v3"
1111
"github.com/esm-dev/esm.sh/internal/npm"
12+
"github.com/ije/gox/set"
1213
"github.com/ije/gox/utils"
1314
)
1415

@@ -270,3 +271,18 @@ func toPackageName(specifier string) string {
270271
name, _, _, _ := splitEsmPath(specifier)
271272
return name
272273
}
274+
275+
// isPackageInExternalNamespace checks if a package belongs to an external namespace
276+
// For example, if "@radix-ui" is in external, then "@radix-ui/react-dropdown" would match
277+
func isPackageInExternalNamespace(pkgName string, external set.ReadOnlySet[string]) bool {
278+
for _, ext := range external.Values() {
279+
// Check if ext is a namespace (starts with @ and has no /)
280+
if strings.HasPrefix(ext, "@") && !strings.Contains(ext[1:], "/") {
281+
// Check if the package belongs to this namespace
282+
if strings.HasPrefix(pkgName, ext+"/") {
283+
return true
284+
}
285+
}
286+
}
287+
return false
288+
}

test/build-args/external.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,22 @@ Deno.test("external nodejs builtin modules", async () => {
9292
assertEquals(res3.status, 200);
9393
assertStringIncludes(await res3.text(), ` from "node:buffer"`);
9494
});
95+
96+
Deno.test("external namespace packages", async () => {
97+
// Test that using @radix-ui as external marks all @radix-ui/* packages as external
98+
{
99+
const res = await fetch("http://localhost:8080/@radix-ui/[email protected]?external=@radix-ui");
100+
res.body?.cancel();
101+
assertEquals(res.status, 200);
102+
const modulePath = res.headers.get("x-esm-path");
103+
const res2 = await fetch("http://localhost:8080" + modulePath);
104+
const code = await res2.text();
105+
// All @radix-ui packages should be external
106+
assertStringIncludes(code, 'from"@radix-ui/react-compose-refs"');
107+
assertStringIncludes(code, 'from"@radix-ui/react-context"');
108+
assertStringIncludes(code, 'from"@radix-ui/react-id"');
109+
assertStringIncludes(code, 'from"@radix-ui/react-menu"');
110+
assertStringIncludes(code, 'from"@radix-ui/react-primitive"');
111+
assertStringIncludes(code, 'from"@radix-ui/react-use-controllable-state"');
112+
}
113+
});

0 commit comments

Comments
 (0)