From a798a197d7a02113a8372d8143f23ec5b62752db Mon Sep 17 00:00:00 2001 From: LoganDark <4723091+LoganDark@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:40:03 -0700 Subject: [PATCH 1/2] Allow esbuild to reuse mangled property names in certain cases --- cmd/esbuild/service.go | 38 +++ internal/bundler/bundler.go | 8 +- .../bundler_tests/bundler_default_test.go | 117 +++++++++ internal/bundler_tests/bundler_test.go | 2 +- .../snapshots/snapshots_default.txt | 55 +++++ internal/config/config.go | 14 +- internal/linker/linker.go | 223 ++++++++++++++++-- lib/shared/common.ts | 42 ++++ lib/shared/stdio_protocol.ts | 4 + lib/shared/types.ts | 8 + pkg/api/api.go | 70 +++--- pkg/api/api_impl.go | 92 +++++--- pkg/cli/cli_impl.go | 137 ++++++----- pkg/cli/mangle_cache.go | 119 ++++++++-- scripts/js-api-tests.js | 173 ++++++++++++++ scripts/ts-type-tests.js | 33 +++ 16 files changed, 961 insertions(+), 174 deletions(-) diff --git a/cmd/esbuild/service.go b/cmd/esbuild/service.go index 26360132d6c..64bbd7020db 100644 --- a/cmd/esbuild/service.go +++ b/cmd/esbuild/service.go @@ -605,6 +605,7 @@ func (service *serviceType) handleBuildRequest(id uint32, request map[string]int options.AbsWorkingDir = request["absWorkingDir"].(string) options.NodePaths = decodeStringArray(request["nodePaths"].([]interface{})) options.MangleCache, _ = request["mangleCache"].(map[string]interface{}) + options.MangleNamespaceCaches = decodeNamespaceCaches(request) for _, entry := range entries { entry := entry.([]interface{}) @@ -665,6 +666,9 @@ func (service *serviceType) handleBuildRequest(id uint32, request map[string]int if options.MangleCache != nil { response["mangleCache"] = result.MangleCache } + if options.MangleNamespaceCaches != nil { + encodeNamespaceCaches(result.MangleNamespaceCaches, response) + } if writeToStdout && len(result.OutputFiles) == 1 { response["writeToStdout"] = result.OutputFiles[0].Contents } @@ -1161,6 +1165,7 @@ func (service *serviceType) handleTransformRequest(id uint32, request map[string return encodeErrorPacket(id, err) } options.MangleCache, _ = request["mangleCache"].(map[string]interface{}) + options.MangleNamespaceCaches = decodeNamespaceCaches(request) transformInput := input if inputFS { @@ -1218,6 +1223,9 @@ func (service *serviceType) handleTransformRequest(id uint32, request map[string if result.MangleCache != nil { response["mangleCache"] = result.MangleCache } + if result.MangleNamespaceCaches != nil { + encodeNamespaceCaches(result.MangleNamespaceCaches, response) + } return encodePacket(packet{ id: id, @@ -1428,3 +1436,33 @@ func decodeMessageToPrivate(obj map[string]interface{}) logger.Msg { } return msg } + +// Decodes namespace caches from a protocol request. The protocol uses +// map[string]interface{} but the Go API uses map[string]map[string]interface{}. +func decodeNamespaceCaches(request map[string]interface{}) map[string]map[string]interface{} { + nsCaches, ok := request["mangleNamespaceCaches"].(map[string]interface{}) + if !ok { + return nil + } + converted := make(map[string]map[string]interface{}, len(nsCaches)) + for nsKey, nsValue := range nsCaches { + if nsMap, ok := nsValue.(map[string]interface{}); ok { + converted[nsKey] = nsMap + } + } + return converted +} + +// Encodes namespace caches into a protocol response. Converts +// map[string]map[string]interface{} to map[string]interface{} +// because the protocol encoder only handles the latter type. +func encodeNamespaceCaches(nsCaches map[string]map[string]interface{}, response map[string]interface{}) { + if nsCaches == nil { + return + } + converted := make(map[string]interface{}, len(nsCaches)) + for k, v := range nsCaches { + converted[k] = v + } + response["mangleNamespaceCaches"] = converted +} diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index ad74dfcb0e4..580287d0b5c 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -3009,7 +3009,7 @@ type Linker func( dataForSourceMaps func() []DataForSourceMap, ) []graph.OutputFile -func (b *Bundle) Compile(log logger.Log, timer *helpers.Timer, mangleCache map[string]interface{}, link Linker) ([]graph.OutputFile, string) { +func (b *Bundle) Compile(log logger.Log, timer *helpers.Timer, mangleCache map[string]interface{}, mangleNamespaceCaches map[string]map[string]interface{}, link Linker) ([]graph.OutputFile, string) { timer.Begin("Compile phase") defer timer.End("Compile phase") @@ -3023,9 +3023,10 @@ func (b *Bundle) Compile(log logger.Log, timer *helpers.Timer, mangleCache map[s cssUsedLocalNames := make(map[string]bool) options.ExclusiveMangleCacheUpdate = func(cb func( mangleCache map[string]interface{}, + mangleNamespaceCaches map[string]map[string]interface{}, cssUsedLocalNames map[string]bool, )) { - cb(mangleCache, cssUsedLocalNames) + cb(mangleCache, mangleNamespaceCaches, cssUsedLocalNames) } files := make([]graph.InputFile, len(b.files)) @@ -3061,12 +3062,13 @@ func (b *Bundle) Compile(log logger.Log, timer *helpers.Timer, mangleCache map[s optionsClone := options optionsClone.ExclusiveMangleCacheUpdate = func(cb func( mangleCache map[string]interface{}, + mangleNamespaceCaches map[string]map[string]interface{}, cssUsedLocalNames map[string]bool, )) { // Serialize all accesses to the mangle cache in entry point order for determinism serializer.Enter(i) defer serializer.Leave(i) - cb(mangleCache, cssUsedLocalNames) + cb(mangleCache, mangleNamespaceCaches, cssUsedLocalNames) } resultGroups[i] = link(&optionsClone, forked, log, b.fs, b.res, files, entryPoints, diff --git a/internal/bundler_tests/bundler_default_test.go b/internal/bundler_tests/bundler_default_test.go index 0db1b3aa12f..c15ca1d404a 100644 --- a/internal/bundler_tests/bundler_default_test.go +++ b/internal/bundler_tests/bundler_default_test.go @@ -7433,6 +7433,123 @@ func TestManglePropsAvoidCollisions(t *testing.T) { }) } +func TestManglePropNamespaces(t *testing.T) { + default_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + export let typeA = { + TypeA_foo_: 1, + TypeA_bar_: 2, + } + export let typeB = { + TypeB_foo_: 3, + TypeB_bar_: 4, + } + `, + }, + entryPaths: []string{"/entry.js"}, + options: config.Options{ + Mode: config.ModePassThrough, + AbsOutputFile: "/out.js", + MangleProps: regexp.MustCompile("_$"), + ManglePropNamespaces: regexp.MustCompile(`^[A-Z][^_]*_`), + }, + }) +} + +func TestManglePropNamespacesAvoidGlobalCollision(t *testing.T) { + default_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + export let global = { + global_prop_: 1, + } + export let typeA = { + TypeA_foo_: 2, + } + export let typeB = { + TypeB_foo_: 3, + } + `, + }, + entryPaths: []string{"/entry.js"}, + options: config.Options{ + Mode: config.ModePassThrough, + AbsOutputFile: "/out.js", + MangleProps: regexp.MustCompile("_$"), + ManglePropNamespaces: regexp.MustCompile(`^[A-Z][^_]*_`), + }, + }) +} + +func TestManglePropNamespacesNoReuse(t *testing.T) { + // Properties within the same namespace must not collide + default_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + export let typeA = { + TypeA_x_: 1, + TypeA_y_: 2, + TypeA_z_: 3, + } + `, + }, + entryPaths: []string{"/entry.js"}, + options: config.Options{ + Mode: config.ModePassThrough, + AbsOutputFile: "/out.js", + MangleProps: regexp.MustCompile("_$"), + ManglePropNamespaces: regexp.MustCompile(`^[A-Z][^_]*_`), + }, + }) +} + +func TestManglePropNamespacesFullMatch(t *testing.T) { + // When the namespace regex consumes the entire property name, treat it as un-namespaced + default_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + export let obj = { + TypeA_: 1, + TypeA_foo_: 2, + TypeB_foo_: 3, + } + `, + }, + entryPaths: []string{"/entry.js"}, + options: config.Options{ + Mode: config.ModePassThrough, + AbsOutputFile: "/out.js", + MangleProps: regexp.MustCompile("_$"), + ManglePropNamespaces: regexp.MustCompile(`^[A-Z][^_]*_`), + }, + }) +} + +func TestManglePropNamespacesSuffix(t *testing.T) { + default_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + export let typeA = { + foo_TypeA_: 1, + bar_TypeA_: 2, + } + export let typeB = { + foo_TypeB_: 3, + bar_TypeB_: 4, + } + `, + }, + entryPaths: []string{"/entry.js"}, + options: config.Options{ + Mode: config.ModePassThrough, + AbsOutputFile: "/out.js", + MangleProps: regexp.MustCompile("_$"), + ManglePropNamespaces: regexp.MustCompile(`_[A-Z][^_]*_$`), + }, + }) +} + func TestManglePropsTypeScriptFeatures(t *testing.T) { default_suite.expectBundled(t, bundled{ files: map[string]string{ diff --git a/internal/bundler_tests/bundler_test.go b/internal/bundler_tests/bundler_test.go index d9fc07d1e42..6416cd19761 100644 --- a/internal/bundler_tests/bundler_test.go +++ b/internal/bundler_tests/bundler_test.go @@ -193,7 +193,7 @@ func (s *suite) __expectBundledImpl(t *testing.T, args bundled, fsKind fs.MockKi } log = logger.NewDeferLog(logKind, nil) - results, metafileJSON := bundle.Compile(log, nil, nil, linker.Link) + results, metafileJSON := bundle.Compile(log, nil, nil, nil, linker.Link) msgs = log.Done() assertLog(t, msgs, args.expectedCompileLog) diff --git a/internal/bundler_tests/snapshots/snapshots_default.txt b/internal/bundler_tests/snapshots/snapshots_default.txt index 903ef5dd019..68a25208f48 100644 --- a/internal/bundler_tests/snapshots/snapshots_default.txt +++ b/internal/bundler_tests/snapshots/snapshots_default.txt @@ -3735,6 +3735,61 @@ x._doNotMangleThis, x?._doNotMangleThis, x[y ? "_doNotMangleThis" : z], x?.[y ? var { _doNotMangleThis: x } = y; "_doNotMangleThis" in x, (y ? "_doNotMangleThis" : z) in x, (y ? z : "_doNotMangleThis") in x; +================================================================================ +TestManglePropNamespaces +---------- /out.js ---------- +export let typeA = { + a: 1, + b: 2 +}; +export let typeB = { + a: 3, + b: 4 +}; + +================================================================================ +TestManglePropNamespacesAvoidGlobalCollision +---------- /out.js ---------- +export let global = { + a: 1 +}; +export let typeA = { + b: 2 +}; +export let typeB = { + b: 3 +}; + +================================================================================ +TestManglePropNamespacesFullMatch +---------- /out.js ---------- +export let obj = { + a: 1, + b: 2, + b: 3 +}; + +================================================================================ +TestManglePropNamespacesNoReuse +---------- /out.js ---------- +export let typeA = { + a: 1, + b: 2, + c: 3 +}; + +================================================================================ +TestManglePropNamespacesSuffix +---------- /out.js ---------- +export let typeA = { + a: 1, + b: 2 +}; +export let typeB = { + a: 3, + b: 4 +}; + ================================================================================ TestMangleProps ---------- /out/entry1.js ---------- diff --git a/internal/config/config.go b/internal/config/config.go index dd9f89fbea4..8c61aec491b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -398,12 +398,13 @@ func (flag *CancelFlag) DidCancel() bool { } type Options struct { - ModuleTypeData js_ast.ModuleTypeData - Defines *ProcessedDefines - TSAlwaysStrict *TSAlwaysStrict - MangleProps *regexp.Regexp - ReserveProps *regexp.Regexp - CancelFlag *CancelFlag + ModuleTypeData js_ast.ModuleTypeData + Defines *ProcessedDefines + TSAlwaysStrict *TSAlwaysStrict + MangleProps *regexp.Regexp + ReserveProps *regexp.Regexp + ManglePropNamespaces *regexp.Regexp + CancelFlag *CancelFlag // When mangling property names, call this function with a callback and do // the property name mangling inside the callback. The callback takes an @@ -420,6 +421,7 @@ type Options struct { // has finished. ExclusiveMangleCacheUpdate func(cb func( mangleCache map[string]interface{}, + mangleNamespaceCaches map[string]map[string]interface{}, cssUsedLocalNames map[string]bool, )) diff --git a/internal/linker/linker.go b/internal/linker/linker.go index bdff3162ed0..cb3ec357523 100644 --- a/internal/linker/linker.go +++ b/internal/linker/linker.go @@ -313,7 +313,7 @@ func Link( // Stop now if there were errors if c.log.HasErrors() { - c.options.ExclusiveMangleCacheUpdate(func(map[string]interface{}, map[string]bool) { + c.options.ExclusiveMangleCacheUpdate(func(map[string]interface{}, map[string]map[string]interface{}, map[string]bool) { // Always do this so that we don't cause other entry points when there are errors }) return []graph.OutputFile{} @@ -335,10 +335,11 @@ func Link( c.timer.Begin("Waiting for mangle cache") c.options.ExclusiveMangleCacheUpdate(func( mangleCache map[string]interface{}, + mangleNamespaceCaches map[string]map[string]interface{}, cssUsedLocalNames map[string]bool, ) { c.timer.End("Waiting for mangle cache") - c.mangleProps(mangleCache) + c.mangleProps(mangleCache, mangleNamespaceCaches) c.mangleLocalCSS(cssUsedLocalNames) }) @@ -349,7 +350,7 @@ func Link( return c.generateChunksInParallel(additionalFiles) } -func (c *linkerContext) mangleProps(mangleCache map[string]interface{}) { +func (c *linkerContext) mangleProps(mangleCache map[string]interface{}, nsCaches map[string]map[string]interface{}) { c.timer.Begin("Mangle props") defer c.timer.End("Mangle props") @@ -403,22 +404,108 @@ func (c *linkerContext) mangleProps(mangleCache map[string]interface{}) { } } - // Sort by use count (note: does not currently account for live vs. dead code) - sorted := make(renamer.StableSymbolCountArray, 0, len(mergedProps)) + minifier := ast.DefaultNameMinifierJS.ShuffleByCharFreq(freq) stableSourceIndices := c.graph.StableSourceIndices - for _, ref := range mergedProps { - sorted = append(sorted, renamer.StableSymbolCount{ - StableSourceIndex: stableSourceIndices[ref.SourceIndex], - Ref: ref, - Count: c.graph.Symbols.Get(ref).UseCountEstimate, - }) + nsRegex := c.options.ManglePropNamespaces + + // If namespacing is not enabled, use the original single-lane algorithm + if nsRegex == nil { + // Sort by use count (note: does not currently account for live vs. dead code) + sorted := make(renamer.StableSymbolCountArray, 0, len(mergedProps)) + for _, ref := range mergedProps { + sorted = append(sorted, renamer.StableSymbolCount{ + StableSourceIndex: stableSourceIndices[ref.SourceIndex], + Ref: ref, + Count: c.graph.Symbols.Get(ref).UseCountEstimate, + }) + } + sort.Sort(sorted) + + nextName := 0 + for _, symbolCount := range sorted { + symbol := c.graph.Symbols.Get(symbolCount.Ref) + + // Don't change existing mappings + if existing, ok := mangleCache[symbol.OriginalName]; ok { + if existing != false { + mangledProps[symbolCount.Ref] = existing.(string) + } + continue + } + + // Generate a new name + name := minifier.NumberToMinifiedName(nextName) + nextName++ + + // Avoid reserved properties + for reservedProps[name] { + name = minifier.NumberToMinifiedName(nextName) + nextName++ + } + + // Track the new mapping + if mangleCache != nil { + mangleCache[symbol.OriginalName] = name + } + mangledProps[symbolCount.Ref] = name + } + return } - sort.Sort(sorted) - // Assign names in order of use count - minifier := ast.DefaultNameMinifierJS.ShuffleByCharFreq(freq) - nextName := 0 - for _, symbolCount := range sorted { + // Namespace-aware mangling: bucket properties by namespace + type namespacedProp struct { + ref ast.Ref + localName string // property name with namespace match removed + } + globalProps := make(renamer.StableSymbolCountArray, 0) + nsBuckets := make(map[string][]namespacedProp) + + for originalName, ref := range mergedProps { + loc := nsRegex.FindStringIndex(originalName) + if loc == nil || (loc[0] == 0 && loc[1] == len(originalName)) || + (loc[0] != 0 && loc[1] != len(originalName)) { + // No match, match consumes the entire name, or match is in the + // middle (not anchored to either side) — goes in the global lane + globalProps = append(globalProps, renamer.StableSymbolCount{ + StableSourceIndex: stableSourceIndices[ref.SourceIndex], + Ref: ref, + Count: c.graph.Symbols.Get(ref).UseCountEstimate, + }) + } else { + nsKey := originalName[loc[0]:loc[1]] + var localName string + if loc[0] == 0 { + localName = originalName[loc[1]:] // prefix namespace + } else { + localName = originalName[:loc[0]] // suffix namespace + } + nsBuckets[nsKey] = append(nsBuckets[nsKey], namespacedProp{ + ref: ref, + localName: localName, + }) + } + } + + // Reserve names from namespace caches too + // (Each namespace cache's reserved names only apply within that namespace, + // but we need to track them so they don't get assigned to the global lane) + globalReservedFromNS := make(map[string]bool) + if nsCaches != nil { + for _, nsCache := range nsCaches { + for _, remapped := range nsCache { + if remapped != false { + globalReservedFromNS[remapped.(string)] = true + } + } + } + } + + // Step 1: Assign names to the global (un-namespaced) lane + // Sort by use count (note: does not currently account for live vs. dead code) + sort.Sort(globalProps) + + nextGlobalName := 0 + for _, symbolCount := range globalProps { symbol := c.graph.Symbols.Get(symbolCount.Ref) // Don't change existing mappings @@ -429,22 +516,108 @@ func (c *linkerContext) mangleProps(mangleCache map[string]interface{}) { continue } - // Generate a new name - name := minifier.NumberToMinifiedName(nextName) - nextName++ - - // Avoid reserved properties - for reservedProps[name] { - name = minifier.NumberToMinifiedName(nextName) - nextName++ + // Generate a new name, avoiding reserved props and names used by namespaces + name := minifier.NumberToMinifiedName(nextGlobalName) + nextGlobalName++ + for reservedProps[name] || globalReservedFromNS[name] { + name = minifier.NumberToMinifiedName(nextGlobalName) + nextGlobalName++ } - // Track the new mapping if mangleCache != nil { mangleCache[symbol.OriginalName] = name } mangledProps[symbolCount.Ref] = name } + + // Collect all globally-assigned mangled names so namespace lanes avoid them + globalAssigned := make(map[string]bool) + for _, name := range mangledProps { + globalAssigned[name] = true + } + + // Step 2: Assign names per namespace lane + // Sort namespace keys for determinism + nsKeys := make([]string, 0, len(nsBuckets)) + for key := range nsBuckets { + nsKeys = append(nsKeys, key) + } + sort.Strings(nsKeys) + + for _, nsPrefix := range nsKeys { + props := nsBuckets[nsPrefix] + + // Sort by use count within this namespace (note: does not currently account for live vs. dead code) + sorted := make(renamer.StableSymbolCountArray, 0, len(props)) + for _, prop := range props { + sorted = append(sorted, renamer.StableSymbolCount{ + StableSourceIndex: stableSourceIndices[prop.ref.SourceIndex], + Ref: prop.ref, + Count: c.graph.Symbols.Get(prop.ref).UseCountEstimate, + }) + } + sort.Sort(sorted) + + // Build a map from ref to local name for this namespace + refToLocal := make(map[ast.Ref]string, len(props)) + for _, prop := range props { + refToLocal[prop.ref] = prop.localName + } + + // Get or create the namespace cache + var nsCache map[string]interface{} + if nsCaches != nil { + nsCache = nsCaches[nsPrefix] + if nsCache == nil { + nsCache = make(map[string]interface{}) + nsCaches[nsPrefix] = nsCache + } + } + + // Reserve names within this namespace from its cache + nsReserved := make(map[string]bool) + if nsCache != nil { + for localName, remapped := range nsCache { + if remapped == false { + nsReserved[localName] = true + } else { + nsReserved[remapped.(string)] = true + } + } + } + + nextNSName := 0 + for _, symbolCount := range sorted { + localName := refToLocal[symbolCount.Ref] + + // Don't change existing mappings from cache + if nsCache != nil { + if existing, ok := nsCache[localName]; ok { + if existing != false { + mangledProps[symbolCount.Ref] = existing.(string) + } + continue + } + } + + // Generate a new name, avoiding: + // - reserved props (JS keywords + --reserve-props) + // - globally-assigned names (from un-namespaced lane) + // - names already used within this namespace + name := minifier.NumberToMinifiedName(nextNSName) + nextNSName++ + for reservedProps[name] || globalAssigned[name] || nsReserved[name] { + name = minifier.NumberToMinifiedName(nextNSName) + nextNSName++ + } + + nsReserved[name] = true + if nsCache != nil { + nsCache[localName] = name + } + mangledProps[symbolCount.Ref] = name + } + } } func (c *linkerContext) mangleLocalCSS(usedLocalNames map[string]bool) { diff --git a/lib/shared/common.ts b/lib/shared/common.ts index 4c608357e28..b1588d70cba 100644 --- a/lib/shared/common.ts +++ b/lib/shared/common.ts @@ -122,6 +122,32 @@ function validateMangleCache(mangleCache: MangleCache | undefined): MangleCache return validated } +type MangleNamespaceCaches = Record + +function validateMangleNamespaceCaches(caches: MangleNamespaceCaches | undefined): MangleNamespaceCaches | undefined { + let validated: MangleNamespaceCaches | undefined + if (caches !== undefined) { + validated = Object.create(null) as MangleNamespaceCaches + for (let nsKey in caches) { + let nsCache = caches[nsKey] + if (typeof nsCache !== 'object' || nsCache === null) { + throw new Error(`Expected ${quote(nsKey)} in mangle namespace caches to be an object`) + } + let validatedNS = Object.create(null) as MangleCache + for (let key in nsCache) { + let value = nsCache[key] + if (typeof value === 'string' || value === false) { + validatedNS[key] = value + } else { + throw new Error(`Expected ${quote(key)} in namespace ${quote(nsKey)} in mangle namespace caches to map to either a string or false`) + } + } + validated[nsKey] = validatedNS + } + } + return validated +} + type CommonOptions = types.BuildOptions | types.TransformOptions function pushLogFlags(flags: string[], options: CommonOptions, keys: OptionKeys, isTTY: boolean, logLevelDefault: types.LogLevel): void { @@ -152,6 +178,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe let mangleProps = getFlag(options, keys, 'mangleProps', mustBeRegExp) let reserveProps = getFlag(options, keys, 'reserveProps', mustBeRegExp) let mangleQuoted = getFlag(options, keys, 'mangleQuoted', mustBeBoolean) + let manglePropNamespaces = getFlag(options, keys, 'manglePropNamespaces', mustBeRegExp) let minify = getFlag(options, keys, 'minify', mustBeBoolean) let minifySyntax = getFlag(options, keys, 'minifySyntax', mustBeBoolean) let minifyWhitespace = getFlag(options, keys, 'minifyWhitespace', mustBeBoolean) @@ -200,6 +227,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe if (mangleProps) flags.push(`--mangle-props=${jsRegExpToGoRegExp(mangleProps)}`) if (reserveProps) flags.push(`--reserve-props=${jsRegExpToGoRegExp(reserveProps)}`) if (mangleQuoted !== void 0) flags.push(`--mangle-quoted=${mangleQuoted}`) + if (manglePropNamespaces) flags.push(`--mangle-prop-namespaces=${jsRegExpToGoRegExp(manglePropNamespaces)}`) if (jsx) flags.push(`--jsx=${jsx}`) if (jsxFactory) flags.push(`--jsx-factory=${jsxFactory}`) @@ -247,6 +275,7 @@ function flagsForBuildOptions( absWorkingDir: string | undefined, nodePaths: string[], mangleCache: MangleCache | undefined, + mangleNamespaceCaches: MangleNamespaceCaches | undefined, } { let flags: string[] = [] let entries: [string, string][] = [] @@ -287,6 +316,7 @@ function flagsForBuildOptions( let write = getFlag(options, keys, 'write', mustBeBoolean) ?? writeDefault; // Default to true if not specified let allowOverwrite = getFlag(options, keys, 'allowOverwrite', mustBeBoolean) let mangleCache = getFlag(options, keys, 'mangleCache', mustBeObject) + let mangleNamespaceCaches = getFlag(options, keys, 'mangleNamespaceCaches', mustBeObject) keys.plugins = true; // "plugins" has already been read earlier checkForInvalidFlags(options, keys, `in ${callName}() call`) @@ -396,6 +426,7 @@ function flagsForBuildOptions( absWorkingDir, nodePaths, mangleCache: validateMangleCache(mangleCache), + mangleNamespaceCaches: validateMangleNamespaceCaches(mangleNamespaceCaches), } } @@ -407,6 +438,7 @@ function flagsForTransformOptions( ): { flags: string[], mangleCache: MangleCache | undefined, + mangleNamespaceCaches: MangleNamespaceCaches | undefined, } { let flags: string[] = [] let keys: OptionKeys = Object.create(null) @@ -419,6 +451,7 @@ function flagsForTransformOptions( let banner = getFlag(options, keys, 'banner', mustBeString) let footer = getFlag(options, keys, 'footer', mustBeString) let mangleCache = getFlag(options, keys, 'mangleCache', mustBeObject) + let mangleNamespaceCaches = getFlag(options, keys, 'mangleNamespaceCaches', mustBeObject) checkForInvalidFlags(options, keys, `in ${callName}() call`) if (sourcemap) flags.push(`--sourcemap=${sourcemap === true ? 'external' : sourcemap}`) @@ -430,6 +463,7 @@ function flagsForTransformOptions( return { flags, mangleCache: validateMangleCache(mangleCache), + mangleNamespaceCaches: validateMangleNamespaceCaches(mangleNamespaceCaches), } } @@ -709,6 +743,7 @@ export function createChannel(streamIn: StreamIn): StreamOut { let { flags, mangleCache, + mangleNamespaceCaches, } = flagsForTransformOptions(callName, options, isTTY, transformLogLevelDefault) let request: protocol.TransformRequest = { command: 'transform', @@ -719,6 +754,7 @@ export function createChannel(streamIn: StreamIn): StreamOut { : input, } if (mangleCache) request.mangleCache = mangleCache + if (mangleNamespaceCaches) request.mangleNamespaceCaches = mangleNamespaceCaches sendRequest(refs, request, (error, response) => { if (error) return callback(new Error(error), null) let errors = replaceDetailsInMessages(response!.errors, details) @@ -731,10 +767,12 @@ export function createChannel(streamIn: StreamIn): StreamOut { code: response!.code, map: response!.map, mangleCache: undefined, + mangleNamespaceCaches: undefined, legalComments: undefined, } if ('legalComments' in response!) result.legalComments = response?.legalComments if (response!.mangleCache) result.mangleCache = response?.mangleCache + if (response!.mangleNamespaceCaches) result.mangleNamespaceCaches = response?.mangleNamespaceCaches callback(null, result) } } @@ -919,6 +957,7 @@ function buildOrContextImpl( absWorkingDir, nodePaths, mangleCache, + mangleNamespaceCaches, } = flagsForBuildOptions(callName, options, isTTY, buildLogLevelDefault, writeDefault) if (write && !streamIn.hasFS) throw new Error(`The "write" option is unavailable in this environment`) @@ -937,6 +976,7 @@ function buildOrContextImpl( } if (requestPlugins) request.plugins = requestPlugins if (mangleCache) request.mangleCache = mangleCache + if (mangleNamespaceCaches) request.mangleNamespaceCaches = mangleNamespaceCaches // Factor out response handling so it can be reused for rebuilds const buildResponseToResult = ( @@ -949,12 +989,14 @@ function buildOrContextImpl( outputFiles: undefined, metafile: undefined, mangleCache: undefined, + mangleNamespaceCaches: undefined, } const originalErrors = result.errors.slice() const originalWarnings = result.warnings.slice() if (response!.outputFiles) result.outputFiles = response!.outputFiles.map(convertOutputFiles) if (response!.metafile && response!.metafile.length) result.metafile = parseJSON(response!.metafile) if (response!.mangleCache) result.mangleCache = response!.mangleCache + if (response!.mangleNamespaceCaches) result.mangleNamespaceCaches = response!.mangleNamespaceCaches if (response!.writeToStdout !== void 0) console.log(protocol.decodeUTF8(response!.writeToStdout).replace(/\n$/, '')) runOnEndCallbacks(result, (onEndErrors, onEndWarnings) => { if (originalErrors.length > 0 || onEndErrors.length > 0) { diff --git a/lib/shared/stdio_protocol.ts b/lib/shared/stdio_protocol.ts index 5f74f506df7..01facc1adbc 100644 --- a/lib/shared/stdio_protocol.ts +++ b/lib/shared/stdio_protocol.ts @@ -19,6 +19,7 @@ export interface BuildRequest { context: boolean plugins?: BuildPlugin[] mangleCache?: Record + mangleNamespaceCaches?: Record> } export interface ServeRequest { @@ -53,6 +54,7 @@ export interface BuildResponse { outputFiles?: BuildOutputFile[] metafile?: Uint8Array mangleCache?: Record + mangleNamespaceCaches?: Record> writeToStdout?: Uint8Array } @@ -113,6 +115,7 @@ export interface TransformRequest { input: Uint8Array inputFS: boolean mangleCache?: Record + mangleNamespaceCaches?: Record> } export interface TransformResponse { @@ -127,6 +130,7 @@ export interface TransformResponse { legalComments?: string mangleCache?: Record + mangleNamespaceCaches?: Record> } export interface FormatMsgsRequest { diff --git a/lib/shared/types.ts b/lib/shared/types.ts index 9e69c39f58b..4ea24d7c0f2 100644 --- a/lib/shared/types.ts +++ b/lib/shared/types.ts @@ -33,8 +33,12 @@ interface CommonOptions { reserveProps?: RegExp /** Documentation: https://esbuild.github.io/api/#mangle-props */ mangleQuoted?: boolean + /** A regex that extracts a namespace prefix from mangled property names, allowing name reuse across namespaces */ + manglePropNamespaces?: RegExp /** Documentation: https://esbuild.github.io/api/#mangle-props */ mangleCache?: Record + /** Per-namespace mangle caches, keyed by namespace prefix */ + mangleNamespaceCaches?: Record> /** Documentation: https://esbuild.github.io/api/#drop */ drop?: Drop[] /** Documentation: https://esbuild.github.io/api/#drop-labels */ @@ -229,6 +233,8 @@ export interface BuildResult | (ProvidedOptions['mangleCache'] extends Object ? never : undefined) + /** Only when "mangleNamespaceCaches" is present */ + mangleNamespaceCaches: Record> | (ProvidedOptions['mangleNamespaceCaches'] extends Object ? never : undefined) } export interface BuildFailure extends Error { @@ -285,6 +291,8 @@ export interface TransformResult | (ProvidedOptions['mangleCache'] extends Object ? never : undefined) + /** Only when "mangleNamespaceCaches" is present */ + mangleNamespaceCaches: Record> | (ProvidedOptions['mangleNamespaceCaches'] extends Object ? never : undefined) /** Only when "legalComments" is "external" */ legalComments: string | (ProvidedOptions['legalComments'] extends 'external' ? never : undefined) } diff --git a/pkg/api/api.go b/pkg/api/api.go index 9d507c79a78..ef41aa12093 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -292,20 +292,22 @@ type BuildOptions struct { Engines []Engine // Documentation: https://esbuild.github.io/api/#target Supported map[string]bool // Documentation: https://esbuild.github.io/api/#supported - MangleProps string // Documentation: https://esbuild.github.io/api/#mangle-props - ReserveProps string // Documentation: https://esbuild.github.io/api/#mangle-props - MangleQuoted MangleQuoted // Documentation: https://esbuild.github.io/api/#mangle-props - MangleCache map[string]interface{} // Documentation: https://esbuild.github.io/api/#mangle-props - Drop Drop // Documentation: https://esbuild.github.io/api/#drop - DropLabels []string // Documentation: https://esbuild.github.io/api/#drop-labels - MinifyWhitespace bool // Documentation: https://esbuild.github.io/api/#minify - MinifyIdentifiers bool // Documentation: https://esbuild.github.io/api/#minify - MinifySyntax bool // Documentation: https://esbuild.github.io/api/#minify - LineLimit int // Documentation: https://esbuild.github.io/api/#line-limit - Charset Charset // Documentation: https://esbuild.github.io/api/#charset - TreeShaking TreeShaking // Documentation: https://esbuild.github.io/api/#tree-shaking - IgnoreAnnotations bool // Documentation: https://esbuild.github.io/api/#ignore-annotations - LegalComments LegalComments // Documentation: https://esbuild.github.io/api/#legal-comments + MangleProps string // Documentation: https://esbuild.github.io/api/#mangle-props + ReserveProps string // Documentation: https://esbuild.github.io/api/#mangle-props + MangleQuoted MangleQuoted // Documentation: https://esbuild.github.io/api/#mangle-props + ManglePropNamespaces string // Documentation: https://esbuild.github.io/api/#mangle-props + MangleCache map[string]interface{} // Documentation: https://esbuild.github.io/api/#mangle-props + MangleNamespaceCaches map[string]map[string]interface{} // Documentation: https://esbuild.github.io/api/#mangle-props + Drop Drop // Documentation: https://esbuild.github.io/api/#drop + DropLabels []string // Documentation: https://esbuild.github.io/api/#drop-labels + MinifyWhitespace bool // Documentation: https://esbuild.github.io/api/#minify + MinifyIdentifiers bool // Documentation: https://esbuild.github.io/api/#minify + MinifySyntax bool // Documentation: https://esbuild.github.io/api/#minify + LineLimit int // Documentation: https://esbuild.github.io/api/#line-limit + Charset Charset // Documentation: https://esbuild.github.io/api/#charset + TreeShaking TreeShaking // Documentation: https://esbuild.github.io/api/#tree-shaking + IgnoreAnnotations bool // Documentation: https://esbuild.github.io/api/#ignore-annotations + LegalComments LegalComments // Documentation: https://esbuild.github.io/api/#legal-comments JSX JSX // Documentation: https://esbuild.github.io/api/#jsx-mode JSXFactory string // Documentation: https://esbuild.github.io/api/#jsx-factory @@ -374,9 +376,10 @@ type BuildResult struct { Errors []Message Warnings []Message - OutputFiles []OutputFile - Metafile string - MangleCache map[string]interface{} + OutputFiles []OutputFile + Metafile string + MangleCache map[string]interface{} + MangleNamespaceCaches map[string]map[string]interface{} } type OutputFile struct { @@ -428,20 +431,22 @@ type TransformOptions struct { Format Format // Documentation: https://esbuild.github.io/api/#format GlobalName string // Documentation: https://esbuild.github.io/api/#global-name - MangleProps string // Documentation: https://esbuild.github.io/api/#mangle-props - ReserveProps string // Documentation: https://esbuild.github.io/api/#mangle-props - MangleQuoted MangleQuoted // Documentation: https://esbuild.github.io/api/#mangle-props - MangleCache map[string]interface{} // Documentation: https://esbuild.github.io/api/#mangle-props - Drop Drop // Documentation: https://esbuild.github.io/api/#drop - DropLabels []string // Documentation: https://esbuild.github.io/api/#drop-labels - MinifyWhitespace bool // Documentation: https://esbuild.github.io/api/#minify - MinifyIdentifiers bool // Documentation: https://esbuild.github.io/api/#minify - MinifySyntax bool // Documentation: https://esbuild.github.io/api/#minify - LineLimit int // Documentation: https://esbuild.github.io/api/#line-limit - Charset Charset // Documentation: https://esbuild.github.io/api/#charset - TreeShaking TreeShaking // Documentation: https://esbuild.github.io/api/#tree-shaking - IgnoreAnnotations bool // Documentation: https://esbuild.github.io/api/#ignore-annotations - LegalComments LegalComments // Documentation: https://esbuild.github.io/api/#legal-comments + MangleProps string // Documentation: https://esbuild.github.io/api/#mangle-props + ReserveProps string // Documentation: https://esbuild.github.io/api/#mangle-props + MangleQuoted MangleQuoted // Documentation: https://esbuild.github.io/api/#mangle-props + ManglePropNamespaces string // Documentation: https://esbuild.github.io/api/#mangle-props + MangleCache map[string]interface{} // Documentation: https://esbuild.github.io/api/#mangle-props + MangleNamespaceCaches map[string]map[string]interface{} // Documentation: https://esbuild.github.io/api/#mangle-props + Drop Drop // Documentation: https://esbuild.github.io/api/#drop + DropLabels []string // Documentation: https://esbuild.github.io/api/#drop-labels + MinifyWhitespace bool // Documentation: https://esbuild.github.io/api/#minify + MinifyIdentifiers bool // Documentation: https://esbuild.github.io/api/#minify + MinifySyntax bool // Documentation: https://esbuild.github.io/api/#minify + LineLimit int // Documentation: https://esbuild.github.io/api/#line-limit + Charset Charset // Documentation: https://esbuild.github.io/api/#charset + TreeShaking TreeShaking // Documentation: https://esbuild.github.io/api/#tree-shaking + IgnoreAnnotations bool // Documentation: https://esbuild.github.io/api/#ignore-annotations + LegalComments LegalComments // Documentation: https://esbuild.github.io/api/#legal-comments JSX JSX // Documentation: https://esbuild.github.io/api/#jsx JSXFactory string // Documentation: https://esbuild.github.io/api/#jsx-factory @@ -470,7 +475,8 @@ type TransformResult struct { Map []byte LegalComments []byte - MangleCache map[string]interface{} + MangleCache map[string]interface{} + MangleNamespaceCaches map[string]map[string]interface{} } // Documentation: https://esbuild.github.io/api/#transform diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index c30c921568f..5a91f5d7f22 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -425,6 +425,19 @@ func validateRegex(log logger.Log, what string, value string) *regexp.Regexp { return regex } +func validateNamespaceRegex(log logger.Log, value string) *regexp.Regexp { + regex := validateRegex(log, "mangle prop namespaces", value) + if regex != nil { + s := regex.String() + if !strings.HasPrefix(s, "^") && !strings.HasSuffix(s, "$") { + log.AddError(nil, logger.Range{}, + "The \"mangle prop namespaces\" regular expression must be anchored with \"^\" or \"$\"") + return nil + } + } + return regex +} + func validateExternals(log logger.Log, fs fs.FS, paths []string) config.ExternalSettings { result := config.ExternalSettings{ PreResolve: config.ExternalMatchers{Exact: make(map[string]bool)}, @@ -875,6 +888,21 @@ func cloneMangleCache(log logger.Log, mangleCache map[string]interface{}) map[st return clone } +func cloneMangleNamespaceCaches(nsCaches map[string]map[string]interface{}) map[string]map[string]interface{} { + if nsCaches == nil { + return nil + } + clone := make(map[string]map[string]interface{}, len(nsCaches)) + for nsKey, nsCache := range nsCaches { + innerClone := make(map[string]interface{}, len(nsCache)) + for k, v := range nsCache { + innerClone[k] = v + } + clone[nsKey] = innerClone + } + return clone +} + //////////////////////////////////////////////////////////////////////////////// // Build API @@ -935,16 +963,17 @@ func contextImpl(buildOpts BuildOptions) (*internalContext, []Message) { } args := rebuildArgs{ - caches: caches, - onEndCallbacks: onEndCallbacks, - onDisposeCallbacks: onDisposeCallbacks, - logOptions: logOptions, - logWarnings: msgs, - entryPoints: entryPoints, - options: options, - mangleCache: buildOpts.MangleCache, - absWorkingDir: absWorkingDir, - write: buildOpts.Write, + caches: caches, + onEndCallbacks: onEndCallbacks, + onDisposeCallbacks: onDisposeCallbacks, + logOptions: logOptions, + logWarnings: msgs, + entryPoints: entryPoints, + options: options, + mangleCache: buildOpts.MangleCache, + mangleNamespaceCaches: buildOpts.MangleNamespaceCaches, + absWorkingDir: absWorkingDir, + write: buildOpts.Write, } return &internalContext{ @@ -1277,6 +1306,7 @@ func validateBuildOptions( LineLimit: buildOpts.LineLimit, MangleProps: validateRegex(log, "mangle props", buildOpts.MangleProps), ReserveProps: validateRegex(log, "reserve props", buildOpts.ReserveProps), + ManglePropNamespaces: validateNamespaceRegex(log, buildOpts.ManglePropNamespaces), MangleQuoted: buildOpts.MangleQuoted == MangleQuotedTrue, DropLabels: append([]string{}, buildOpts.DropLabels...), DropDebugger: (buildOpts.Drop & DropDebugger) != 0, @@ -1446,16 +1476,17 @@ type onEndCallback struct { } type rebuildArgs struct { - caches *cache.CacheSet - onEndCallbacks []onEndCallback - onDisposeCallbacks []func() - logOptions logger.OutputOptions - logWarnings []logger.Msg - entryPoints []bundler.EntryPoint - options config.Options - mangleCache map[string]interface{} - absWorkingDir string - write bool + caches *cache.CacheSet + onEndCallbacks []onEndCallback + onDisposeCallbacks []func() + logOptions logger.OutputOptions + logWarnings []logger.Msg + entryPoints []bundler.EntryPoint + options config.Options + mangleCache map[string]interface{} + mangleNamespaceCaches map[string]map[string]interface{} + absWorkingDir string + write bool } type rebuildState struct { @@ -1505,7 +1536,8 @@ func rebuildImpl(args rebuildArgs, oldHashes map[string]string) (rebuildState, m if !log.HasErrors() { // Compile the bundle result.MangleCache = cloneMangleCache(log, args.mangleCache) - results, metafile = bundle.Compile(log, timer, result.MangleCache, linker.Link) + result.MangleNamespaceCaches = cloneMangleNamespaceCaches(args.mangleNamespaceCaches) + results, metafile = bundle.Compile(log, timer, result.MangleCache, result.MangleNamespaceCaches, linker.Link) // Canceling a build generates a single error at the end of the build if args.options.CancelFlag.DidCancel() { @@ -1709,6 +1741,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult platform := validatePlatform(transformOpts.Platform) defines, injectedDefines := validateDefines(log, transformOpts.Define, transformOpts.Pure, platform, false /* isBuildAPI */, false /* minify */, transformOpts.Drop) mangleCache := cloneMangleCache(log, transformOpts.MangleCache) + mangleNamespaceCaches := cloneMangleNamespaceCaches(transformOpts.MangleNamespaceCaches) options := config.Options{ CSSPrefixData: cssPrefixData, UnsupportedJSFeatures: jsFeatures.ApplyOverrides(jsOverrides, jsMask), @@ -1743,6 +1776,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult LineLimit: transformOpts.LineLimit, MangleProps: validateRegex(log, "mangle props", transformOpts.MangleProps), ReserveProps: validateRegex(log, "reserve props", transformOpts.ReserveProps), + ManglePropNamespaces: validateNamespaceRegex(log, transformOpts.ManglePropNamespaces), MangleQuoted: transformOpts.MangleQuoted == MangleQuotedTrue, DropLabels: append([]string{}, transformOpts.DropLabels...), DropDebugger: (transformOpts.Drop & DropDebugger) != 0, @@ -1805,7 +1839,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult // Stop now if there were errors if !log.HasErrors() { // Compile the bundle - results, _ = bundle.Compile(log, timer, mangleCache, linker.Link) + results, _ = bundle.Compile(log, timer, mangleCache, mangleNamespaceCaches, linker.Link) } timer.Log(log) @@ -1838,16 +1872,18 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult // Only return the mangle cache for a successful build if log.HasErrors() { mangleCache = nil + mangleNamespaceCaches = nil } msgs := log.Done() return TransformResult{ - Errors: convertMessagesToPublic(logger.Error, msgs, options.LogPathStyle), - Warnings: convertMessagesToPublic(logger.Warning, msgs, options.LogPathStyle), - Code: code, - Map: sourceMap, - LegalComments: legalComments, - MangleCache: mangleCache, + Errors: convertMessagesToPublic(logger.Error, msgs, options.LogPathStyle), + Warnings: convertMessagesToPublic(logger.Warning, msgs, options.LogPathStyle), + Code: code, + Map: sourceMap, + LegalComments: legalComments, + MangleCache: mangleCache, + MangleNamespaceCaches: mangleNamespaceCaches, } } diff --git a/pkg/cli/cli_impl.go b/pkg/cli/cli_impl.go index d68d76d56b7..a338b9bf0c5 100644 --- a/pkg/cli/cli_impl.go +++ b/pkg/cli/cli_impl.go @@ -212,6 +212,14 @@ func parseOptionsImpl( transformOpts.ReserveProps = value } + case strings.HasPrefix(arg, "--mangle-prop-namespaces="): + value := arg[len("--mangle-prop-namespaces="):] + if buildOpts != nil { + buildOpts.ManglePropNamespaces = value + } else { + transformOpts.ManglePropNamespaces = value + } + case strings.HasPrefix(arg, "--mangle-cache=") && buildOpts != nil && kind == kindInternal: value := arg[len("--mangle-cache="):] extras.mangleCache = &value @@ -872,65 +880,66 @@ func parseOptionsImpl( } equals := map[string]bool{ - "abs-paths": true, - "allow-overwrite": true, - "asset-names": true, - "banner": true, - "bundle": true, - "certfile": true, - "charset": true, - "chunk-names": true, - "color": true, - "conditions": true, - "cors-origin": true, - "drop-labels": true, - "entry-names": true, - "footer": true, - "format": true, - "global-name": true, - "ignore-annotations": true, - "jsx-factory": true, - "jsx-fragment": true, - "jsx-import-source": true, - "jsx": true, - "keep-names": true, - "keyfile": true, - "legal-comments": true, - "loader": true, - "log-level": true, - "log-limit": true, - "main-fields": true, - "mangle-cache": true, - "mangle-props": true, - "mangle-quoted": true, - "metafile": true, - "minify-identifiers": true, - "minify-syntax": true, - "minify-whitespace": true, - "minify": true, - "outbase": true, - "outdir": true, - "outfile": true, - "packages": true, - "platform": true, - "preserve-symlinks": true, - "public-path": true, - "reserve-props": true, - "resolve-extensions": true, - "serve-fallback": true, - "serve": true, - "servedir": true, - "source-root": true, - "sourcefile": true, - "sourcemap": true, - "sources-content": true, - "splitting": true, - "target": true, - "tree-shaking": true, - "tsconfig-raw": true, - "tsconfig": true, - "watch": true, - "watch-delay": true, + "abs-paths": true, + "allow-overwrite": true, + "asset-names": true, + "banner": true, + "bundle": true, + "certfile": true, + "charset": true, + "chunk-names": true, + "color": true, + "conditions": true, + "cors-origin": true, + "drop-labels": true, + "entry-names": true, + "footer": true, + "format": true, + "global-name": true, + "ignore-annotations": true, + "jsx-factory": true, + "jsx-fragment": true, + "jsx-import-source": true, + "jsx": true, + "keep-names": true, + "keyfile": true, + "legal-comments": true, + "loader": true, + "log-level": true, + "log-limit": true, + "main-fields": true, + "mangle-cache": true, + "mangle-prop-namespaces": true, + "mangle-props": true, + "mangle-quoted": true, + "metafile": true, + "minify-identifiers": true, + "minify-syntax": true, + "minify-whitespace": true, + "minify": true, + "outbase": true, + "outdir": true, + "outfile": true, + "packages": true, + "platform": true, + "preserve-symlinks": true, + "public-path": true, + "reserve-props": true, + "resolve-extensions": true, + "serve-fallback": true, + "serve": true, + "servedir": true, + "source-root": true, + "sourcefile": true, + "sourcemap": true, + "sources-content": true, + "splitting": true, + "target": true, + "tree-shaking": true, + "tsconfig-raw": true, + "tsconfig": true, + "watch": true, + "watch-delay": true, } colon := map[string]bool{ @@ -1294,7 +1303,7 @@ func runImpl(osArgs []string, plugins []api.Plugin) int { // Also validate the mangle cache absolute path and directory ahead of time // for the same reason - var writeMangleCache func(map[string]interface{}) + var writeMangleCache func(map[string]interface{}, map[string]map[string]interface{}) if extras.mangleCache != nil { var mangleCacheAbsPath string var mangleCacheAbsDir string @@ -1308,7 +1317,7 @@ func runImpl(osArgs []string, plugins []api.Plugin) int { } mangleCacheAbsPath = absPath mangleCacheAbsDir = realFS.Dir(absPath) - buildOptions.MangleCache, mangleCacheOrder = parseMangleCache(osArgs, realFS, *extras.mangleCache) + buildOptions.MangleCache, buildOptions.MangleNamespaceCaches, mangleCacheOrder = parseMangleCache(osArgs, realFS, *extras.mangleCache) if buildOptions.MangleCache == nil { return 1 // Stop now if parsing failed } @@ -1316,7 +1325,7 @@ func runImpl(osArgs []string, plugins []api.Plugin) int { // Don't fail in this case since the error will be reported by "api.Build" } - writeMangleCache = func(mangleCache map[string]interface{}) { + writeMangleCache = func(mangleCache map[string]interface{}, namespaceCaches map[string]map[string]interface{}) { if mangleCache == nil || realFSErr != nil { return // Don't write out the metafile on build errors } @@ -1330,7 +1339,7 @@ func runImpl(osArgs []string, plugins []api.Plugin) int { logger.PrintErrorToStderr(osArgs, fmt.Sprintf( "Failed to create output directory: %s", err.Error())) } else { - bytes := printMangleCache(mangleCache, mangleCacheOrder, buildOptions.Charset == api.CharsetASCII) + bytes := printMangleCache(mangleCache, namespaceCaches, mangleCacheOrder, buildOptions.Charset == api.CharsetASCII) if err := ioutil.WriteFile(mangleCacheAbsPath, bytes, 0666); err != nil { logger.PrintErrorToStderr(osArgs, fmt.Sprintf( "Failed to write to output file: %s", err.Error())) @@ -1351,7 +1360,7 @@ func runImpl(osArgs []string, plugins []api.Plugin) int { // Write the mangle cache to the file system if writeMangleCache != nil { - writeMangleCache(result.MangleCache) + writeMangleCache(result.MangleCache, result.MangleNamespaceCaches) } return api.OnEndResult{}, nil diff --git a/pkg/cli/mangle_cache.go b/pkg/cli/mangle_cache.go index 05fe75e42ef..fdf41cc6c28 100644 --- a/pkg/cli/mangle_cache.go +++ b/pkg/cli/mangle_cache.go @@ -4,6 +4,10 @@ package cli // decisions. It's a flat map where the keys are strings and the values are // either strings or the boolean value "false". This is the case both in JSON // and in Go (so the "interface{}" values are also either strings or "false"). +// +// Namespace caches are stored at the top level with a "#" prefix on the key +// (e.g. "#TypeA_"), since "#" cannot start a valid mangled property name. +// Their values are objects with the same string-or-false format. import ( "fmt" @@ -20,7 +24,7 @@ import ( "github.com/evanw/esbuild/internal/resolver" ) -func parseMangleCache(osArgs []string, fs fs.FS, absPath string) (map[string]interface{}, []string) { +func parseMangleCache(osArgs []string, fs fs.FS, absPath string) (map[string]interface{}, map[string]map[string]interface{}, []string) { // Log problems with the mangle cache to stderr log := logger.NewStderrLog(logger.OutputOptionsForArgs(osArgs)) defer log.Done() @@ -35,13 +39,13 @@ func parseMangleCache(osArgs []string, fs fs.FS, absPath string) (map[string]int if err != nil { // It's ok if it's just missing if err == syscall.ENOENT { - return make(map[string]interface{}), []string{} + return make(map[string]interface{}), nil, []string{} } // Otherwise, report the error log.AddError(nil, logger.Range{}, fmt.Sprintf("Failed to read from mangle cache file %q: %s", prettyPath, originalError.Error())) - return nil, nil + return nil, nil, nil } // Use our JSON parser so we get pretty-printed error messages @@ -54,7 +58,7 @@ func parseMangleCache(osArgs []string, fs fs.FS, absPath string) (map[string]int result, ok := js_parser.ParseJSON(log, source, js_parser.JSONOptions{}) if !ok || log.HasErrors() { // Stop if there were any errors so we don't continue and then overwrite this file - return nil, nil + return nil, nil, nil } tracker := logger.MakeLineColumnTracker(&source) @@ -63,16 +67,51 @@ func parseMangleCache(osArgs []string, fs fs.FS, absPath string) (map[string]int if !ok { log.AddError(&tracker, logger.Range{Loc: result.Loc}, "Expected a top-level object in mangle cache file") - return nil, nil + return nil, nil, nil } mangleCache := make(map[string]interface{}, len(root.Properties)) + var namespaceCaches map[string]map[string]interface{} order := make([]string, 0, len(root.Properties)) for _, property := range root.Properties { key := helpers.UTF16ToString(property.Key.Data.(*js_ast.EString).Value) order = append(order, key) + // Keys starting with "#" are namespace caches + if strings.HasPrefix(key, "#") { + nsKey := key[1:] // Strip the "#" prefix + nsObj, ok := property.ValueOrNil.Data.(*js_ast.EObject) + if !ok { + log.AddError(&tracker, logger.Range{Loc: property.ValueOrNil.Loc}, + fmt.Sprintf("Expected %q in mangle cache file to be an object", key)) + continue + } + nsCache := make(map[string]interface{}, len(nsObj.Properties)) + for _, innerProp := range nsObj.Properties { + innerKey := helpers.UTF16ToString(innerProp.Key.Data.(*js_ast.EString).Value) + switch v := innerProp.ValueOrNil.Data.(type) { + case *js_ast.EBoolean: + if v.Value { + log.AddError(&tracker, js_lexer.RangeOfIdentifier(source, innerProp.ValueOrNil.Loc), + fmt.Sprintf("Expected %q in %q in mangle cache file to map to either a string or false", innerKey, key)) + } else { + nsCache[innerKey] = false + } + case *js_ast.EString: + nsCache[innerKey] = helpers.UTF16ToString(v.Value) + default: + log.AddError(&tracker, logger.Range{Loc: innerProp.ValueOrNil.Loc}, + fmt.Sprintf("Expected %q in %q in mangle cache file to map to either a string or false", innerKey, key)) + } + } + if namespaceCaches == nil { + namespaceCaches = make(map[string]map[string]interface{}) + } + namespaceCaches[nsKey] = nsCache + continue + } + switch v := property.ValueOrNil.Data.(type) { case *js_ast.EBoolean: if v.Value { @@ -92,34 +131,52 @@ func parseMangleCache(osArgs []string, fs fs.FS, absPath string) (map[string]int } if log.HasErrors() { - return nil, nil + return nil, nil, nil } - return mangleCache, order + return mangleCache, namespaceCaches, order } -func printMangleCache(mangleCache map[string]interface{}, originalOrder []string, asciiOnly bool) []byte { +func printMangleCache(mangleCache map[string]interface{}, namespaceCaches map[string]map[string]interface{}, originalOrder []string, asciiOnly bool) []byte { j := helpers.Joiner{} j.AddString("{") + // Build a combined map of all keys for ordering purposes. + // Namespace keys are stored with a "#" prefix in the file. + allKeys := make(map[string]bool, len(mangleCache)+len(namespaceCaches)) + for key := range mangleCache { + allKeys[key] = true + } + for nsKey := range namespaceCaches { + allKeys["#"+nsKey] = true + } + + // Also preserve any "#"-prefixed keys from the original order that + // might not be in the current namespace caches (e.g. emptied out) + for _, key := range originalOrder { + if strings.HasPrefix(key, "#") { + allKeys[key] = true + } + } + // Determine the order to print the keys in order := originalOrder - if len(mangleCache) > len(order) { - order = make([]string, 0, len(mangleCache)) + if len(allKeys) > len(order) { + order = make([]string, 0, len(allKeys)) if sort.StringsAreSorted(originalOrder) { // If they came sorted, keep them sorted - for key := range mangleCache { + for key := range allKeys { order = append(order, key) } sort.Strings(order) } else { // Otherwise add all new keys to the end, and only sort the new keys - originalKeys := make(map[string]bool, len(originalOrder)) + originalKeySet := make(map[string]bool, len(originalOrder)) for _, key := range originalOrder { - originalKeys[key] = true + originalKeySet[key] = true } order = append(order, originalOrder...) - for key := range mangleCache { - if !originalKeys[key] { + for key := range allKeys { + if !originalKeySet[key] { order = append(order, key) } } @@ -137,6 +194,38 @@ func printMangleCache(mangleCache map[string]interface{}, originalOrder []string } j.AddBytes(helpers.QuoteForJSON(key, asciiOnly)) + // Handle namespace cache keys (prefixed with "#") + if strings.HasPrefix(key, "#") { + nsKey := key[1:] + nsCache := namespaceCaches[nsKey] + if len(nsCache) > 0 { + j.AddString(": {") + innerKeys := make([]string, 0, len(nsCache)) + for innerKey := range nsCache { + innerKeys = append(innerKeys, innerKey) + } + sort.Strings(innerKeys) + for ii, innerKey := range innerKeys { + if ii > 0 { + j.AddString(",\n ") + } else { + j.AddString("\n ") + } + j.AddBytes(helpers.QuoteForJSON(innerKey, asciiOnly)) + if value := nsCache[innerKey]; value != false { + j.AddString(": ") + j.AddBytes(helpers.QuoteForJSON(value.(string), asciiOnly)) + } else { + j.AddString(": false") + } + } + j.AddString("\n }") + } else { + j.AddString(": {}") + } + continue + } + // Print the value if value := mangleCache[key]; value != false { j.AddString(": ") diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 06b90c2cd92..5f203de23b2 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -90,6 +90,179 @@ let buildTests = { assert.deepStrictEqual(result.mangleCache, { x_: 'FIXED', y_: 'a', z_: false }) }, + async manglePropNamespacesBuild({ esbuild }) { + var result = await esbuild.build({ + stdin: { + contents: `x = { TypeA_foo_: 1, TypeA_bar_: 2, TypeB_foo_: 3, TypeB_bar_: 4 }`, + }, + mangleProps: /_$/, + manglePropNamespaces: /^[A-Z][^_]*_/, + write: false, + }) + // Properties in different namespaces should reuse mangled names + assert.strictEqual(result.outputFiles[0].text, 'x = { a: 1, b: 2, a: 3, b: 4 };\n') + }, + + async manglePropNamespacesSuffixBuild({ esbuild }) { + var result = await esbuild.build({ + stdin: { + contents: `x = { foo_TypeA_: 1, bar_TypeA_: 2, foo_TypeB_: 3, bar_TypeB_: 4 }`, + }, + mangleProps: /_$/, + manglePropNamespaces: /_[A-Z][^_]*_$/, + write: false, + }) + // Suffix namespaces should also reuse mangled names + assert.strictEqual(result.outputFiles[0].text, 'x = { a: 1, b: 2, a: 3, b: 4 };\n') + }, + + async manglePropNamespacesAvoidGlobalBuild({ esbuild }) { + var result = await esbuild.build({ + stdin: { + contents: `x = { global_: 1, TypeA_foo_: 2, TypeB_foo_: 3 }`, + }, + mangleProps: /_$/, + manglePropNamespaces: /^[A-Z][^_]*_/, + write: false, + }) + // Namespaced properties should not collide with global ones + assert.strictEqual(result.outputFiles[0].text, 'x = { a: 1, b: 2, b: 3 };\n') + }, + + async mangleNamespaceCachesBuild({ esbuild }) { + var result = await esbuild.build({ + stdin: { + contents: `x = { TypeA_foo_: 1, TypeB_foo_: 2 }`, + }, + mangleProps: /_$/, + manglePropNamespaces: /^[A-Z][^_]*_/, + mangleCache: {}, + mangleNamespaceCaches: {}, + write: false, + }) + assert.strictEqual(result.outputFiles[0].text, 'x = { a: 1, a: 2 };\n') + assert.deepStrictEqual(result.mangleCache, {}) + assert.deepStrictEqual(result.mangleNamespaceCaches, { + 'TypeA_': { 'foo_': 'a' }, + 'TypeB_': { 'foo_': 'a' }, + }) + }, + + async mangleNamespaceCachesRoundTrip({ esbuild }) { + // First build populates both global and namespace caches + var result1 = await esbuild.build({ + stdin: { + contents: `x = { global_: 0, TypeA_foo_: 1, TypeB_foo_: 2 }`, + }, + mangleProps: /_$/, + manglePropNamespaces: /^[A-Z][^_]*_/, + mangleCache: {}, + mangleNamespaceCaches: {}, + write: false, + }) + assert.strictEqual(result1.outputFiles[0].text, 'x = { a: 0, b: 1, b: 2 };\n') + assert.deepStrictEqual(result1.mangleCache, { global_: 'a' }) + assert.deepStrictEqual(result1.mangleNamespaceCaches, { + 'TypeA_': { 'foo_': 'b' }, + 'TypeB_': { 'foo_': 'b' }, + }) + + // Second build with new properties should honor the cached mappings + var result2 = await esbuild.build({ + stdin: { + contents: `x = { global_: 0, other_: 9, TypeA_foo_: 1, TypeA_baz_: 2, TypeB_foo_: 3 }`, + }, + mangleProps: /_$/, + manglePropNamespaces: /^[A-Z][^_]*_/, + mangleCache: result1.mangleCache, + mangleNamespaceCaches: result1.mangleNamespaceCaches, + write: false, + }) + assert.strictEqual(result2.outputFiles[0].text, 'x = { a: 0, c: 9, b: 1, d: 2, b: 3 };\n') + assert.deepStrictEqual(result2.mangleCache, { global_: 'a', other_: 'c' }) + assert.deepStrictEqual(result2.mangleNamespaceCaches, { + 'TypeA_': { 'foo_': 'b', 'baz_': 'd' }, + 'TypeB_': { 'foo_': 'b' }, + }) + }, + + async mangleNamespaceCachesWithoutMangleCache({ esbuild }) { + // Namespace caches should work even without mangleCache + var result = await esbuild.build({ + stdin: { + contents: `x = { TypeA_foo_: 1, TypeB_foo_: 2 }`, + }, + mangleProps: /_$/, + manglePropNamespaces: /^[A-Z][^_]*_/, + mangleNamespaceCaches: {}, + write: false, + }) + assert.strictEqual(result.outputFiles[0].text, 'x = { a: 1, a: 2 };\n') + assert.strictEqual(result.mangleCache, undefined) + assert.deepStrictEqual(result.mangleNamespaceCaches, { + 'TypeA_': { 'foo_': 'a' }, + 'TypeB_': { 'foo_': 'a' }, + }) + }, + + async mangleNamespaceCachesSuffixRoundTrip({ esbuild }) { + // Suffix namespaces should round-trip through caches correctly + var result1 = await esbuild.build({ + stdin: { + contents: `x = { foo_TypeA_: 1, bar_TypeA_: 2, foo_TypeB_: 3 }`, + }, + mangleProps: /_$/, + manglePropNamespaces: /_[A-Z][^_]*_$/, + mangleCache: {}, + mangleNamespaceCaches: {}, + write: false, + }) + assert.deepStrictEqual(result1.mangleNamespaceCaches, { + '_TypeA_': { 'foo': 'a', 'bar': 'b' }, + '_TypeB_': { 'foo': 'a' }, + }) + + // Second build should honor cached suffix mappings + var result2 = await esbuild.build({ + stdin: { + contents: `x = { foo_TypeA_: 1, baz_TypeA_: 2, foo_TypeB_: 3 }`, + }, + mangleProps: /_$/, + manglePropNamespaces: /_[A-Z][^_]*_$/, + mangleCache: result1.mangleCache, + mangleNamespaceCaches: result1.mangleNamespaceCaches, + write: false, + }) + assert.deepStrictEqual(result2.mangleNamespaceCaches, { + '_TypeA_': { 'foo': 'a', 'bar': 'b', 'baz': 'c' }, + '_TypeB_': { 'foo': 'a' }, + }) + }, + + async manglePropNamespacesTransform({ esbuild }) { + var { code } = await esbuild.transform(`x = { TypeA_foo_: 1, TypeB_foo_: 2 }`, { + mangleProps: /_$/, + manglePropNamespaces: /^[A-Z][^_]*_/, + }) + assert.strictEqual(code, 'x = { a: 1, a: 2 };\n') + }, + + async mangleNamespaceCachesTransform({ esbuild }) { + var { code, mangleCache, mangleNamespaceCaches } = await esbuild.transform( + `x = { TypeA_foo_: 1, TypeB_foo_: 2 }`, { + mangleProps: /_$/, + manglePropNamespaces: /^[A-Z][^_]*_/, + mangleCache: {}, + mangleNamespaceCaches: {}, + }) + assert.strictEqual(code, 'x = { a: 1, a: 2 };\n') + assert.deepStrictEqual(mangleCache, {}) + assert.deepStrictEqual(mangleNamespaceCaches, { + 'TypeA_': { 'foo_': 'a' }, + 'TypeB_': { 'foo_': 'a' }, + }) + }, + async windowsBackslashPathTest({ esbuild, testDir }) { let entry = path.join(testDir, 'entry.js'); let nested = path.join(testDir, 'nested.js'); diff --git a/scripts/ts-type-tests.js b/scripts/ts-type-tests.js index a7c9fbc1cc7..cbfaf8d333e 100644 --- a/scripts/ts-type-tests.js +++ b/scripts/ts-type-tests.js @@ -47,6 +47,15 @@ const testsWithoutErrors = { esbuild.transform('', { mangleCache: {} }) .then(result => result.mangleCache['x']) `, + mangleNamespaceCaches: ` + import * as esbuild from 'esbuild' + esbuild.buildSync({ mangleNamespaceCaches: {} }).mangleNamespaceCaches['ns']['x'] + esbuild.build({ mangleNamespaceCaches: {} }) + .then(result => result.mangleNamespaceCaches['ns']['x']) + esbuild.transformSync('', { mangleNamespaceCaches: {} }).mangleNamespaceCaches['ns']['x'] + esbuild.transform('', { mangleNamespaceCaches: {} }) + .then(result => result.mangleNamespaceCaches['ns']['x']) + `, legalCommentsExternal: ` import * as esbuild from 'esbuild' esbuild.transformSync('', { legalComments: 'external' }).legalComments.length @@ -72,6 +81,12 @@ const testsWithoutErrors = { .then(context => context.rebuild()) .then(result => result.mangleCache['x']) `, + contextMangleNamespaceCaches: ` + import * as esbuild from 'esbuild' + esbuild.context({ mangleNamespaceCaches: {} }) + .then(context => context.rebuild()) + .then(result => result.mangleNamespaceCaches['ns']['x']) + `, contextWriteFalseOutputFiles: ` import * as esbuild from 'esbuild' esbuild.context({ write: false }) @@ -337,6 +352,24 @@ const testsWithErrors = { esbuild.transformSync('', {}).mangleCache['x'] `, + // mangleNamespaceCaches + mangleNamespaceCachesBuild_undefined: ` + import * as esbuild from 'esbuild' + esbuild.build({}).then(result => result.mangleNamespaceCaches['ns']['x']) + `, + mangleNamespaceCachesBuildSync_undefined: ` + import * as esbuild from 'esbuild' + esbuild.buildSync({}).mangleNamespaceCaches['ns']['x'] + `, + mangleNamespaceCachesTransform_undefined: ` + import * as esbuild from 'esbuild' + esbuild.transform('', {}).then(result => result.mangleNamespaceCaches['ns']['x']) + `, + mangleNamespaceCachesTransformSync_undefined: ` + import * as esbuild from 'esbuild' + esbuild.transformSync('', {}).mangleNamespaceCaches['ns']['x'] + `, + // legalComments legalCommentsTransform_undefined: ` import * as esbuild from 'esbuild' From 6d1363254bbd9dabee2cb93ee43150e63f088072 Mon Sep 17 00:00:00 2001 From: LoganDark <4723091+LoganDark@users.noreply.github.com> Date: Sat, 18 Apr 2026 08:08:44 -0700 Subject: [PATCH 2/2] Disambiguate prefix and suffix namespace matches Patterns like ^abc|abc$ could have caused abc123 and 123abc to be cached as a single name; treat them like separate namespaces for correctness. --- internal/linker/linker.go | 5 +++-- scripts/js-api-tests.js | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/internal/linker/linker.go b/internal/linker/linker.go index cb3ec357523..f9f28cc7480 100644 --- a/internal/linker/linker.go +++ b/internal/linker/linker.go @@ -472,11 +472,12 @@ func (c *linkerContext) mangleProps(mangleCache map[string]interface{}, nsCaches Count: c.graph.Symbols.Get(ref).UseCountEstimate, }) } else { - nsKey := originalName[loc[0]:loc[1]] - var localName string + var nsKey, localName string if loc[0] == 0 { + nsKey = "^" + originalName[loc[0]:loc[1]] localName = originalName[loc[1]:] // prefix namespace } else { + nsKey = originalName[loc[0]:loc[1]] + "$" localName = originalName[:loc[0]] // suffix namespace } nsBuckets[nsKey] = append(nsBuckets[nsKey], namespacedProp{ diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 5f203de23b2..73bf617a540 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -143,8 +143,8 @@ let buildTests = { assert.strictEqual(result.outputFiles[0].text, 'x = { a: 1, a: 2 };\n') assert.deepStrictEqual(result.mangleCache, {}) assert.deepStrictEqual(result.mangleNamespaceCaches, { - 'TypeA_': { 'foo_': 'a' }, - 'TypeB_': { 'foo_': 'a' }, + '^TypeA_': { 'foo_': 'a' }, + '^TypeB_': { 'foo_': 'a' }, }) }, @@ -163,8 +163,8 @@ let buildTests = { assert.strictEqual(result1.outputFiles[0].text, 'x = { a: 0, b: 1, b: 2 };\n') assert.deepStrictEqual(result1.mangleCache, { global_: 'a' }) assert.deepStrictEqual(result1.mangleNamespaceCaches, { - 'TypeA_': { 'foo_': 'b' }, - 'TypeB_': { 'foo_': 'b' }, + '^TypeA_': { 'foo_': 'b' }, + '^TypeB_': { 'foo_': 'b' }, }) // Second build with new properties should honor the cached mappings @@ -181,8 +181,8 @@ let buildTests = { assert.strictEqual(result2.outputFiles[0].text, 'x = { a: 0, c: 9, b: 1, d: 2, b: 3 };\n') assert.deepStrictEqual(result2.mangleCache, { global_: 'a', other_: 'c' }) assert.deepStrictEqual(result2.mangleNamespaceCaches, { - 'TypeA_': { 'foo_': 'b', 'baz_': 'd' }, - 'TypeB_': { 'foo_': 'b' }, + '^TypeA_': { 'foo_': 'b', 'baz_': 'd' }, + '^TypeB_': { 'foo_': 'b' }, }) }, @@ -200,8 +200,8 @@ let buildTests = { assert.strictEqual(result.outputFiles[0].text, 'x = { a: 1, a: 2 };\n') assert.strictEqual(result.mangleCache, undefined) assert.deepStrictEqual(result.mangleNamespaceCaches, { - 'TypeA_': { 'foo_': 'a' }, - 'TypeB_': { 'foo_': 'a' }, + '^TypeA_': { 'foo_': 'a' }, + '^TypeB_': { 'foo_': 'a' }, }) }, @@ -218,8 +218,8 @@ let buildTests = { write: false, }) assert.deepStrictEqual(result1.mangleNamespaceCaches, { - '_TypeA_': { 'foo': 'a', 'bar': 'b' }, - '_TypeB_': { 'foo': 'a' }, + '_TypeA_$': { 'foo': 'a', 'bar': 'b' }, + '_TypeB_$': { 'foo': 'a' }, }) // Second build should honor cached suffix mappings @@ -234,8 +234,8 @@ let buildTests = { write: false, }) assert.deepStrictEqual(result2.mangleNamespaceCaches, { - '_TypeA_': { 'foo': 'a', 'bar': 'b', 'baz': 'c' }, - '_TypeB_': { 'foo': 'a' }, + '_TypeA_$': { 'foo': 'a', 'bar': 'b', 'baz': 'c' }, + '_TypeB_$': { 'foo': 'a' }, }) }, @@ -258,8 +258,8 @@ let buildTests = { assert.strictEqual(code, 'x = { a: 1, a: 2 };\n') assert.deepStrictEqual(mangleCache, {}) assert.deepStrictEqual(mangleNamespaceCaches, { - 'TypeA_': { 'foo_': 'a' }, - 'TypeB_': { 'foo_': 'a' }, + '^TypeA_': { 'foo_': 'a' }, + '^TypeB_': { 'foo_': 'a' }, }) },