Skip to content

fix(transformer): ambient declare should not block dot define replacement #23224

Description

@camc314

Summary

ReplaceGlobalDefines currently treats TypeScript ambient declarations like declare let self: ServiceWorkerGlobalScope as local runtime bindings. Because of that, a define such as self.__WB_MANIFEST is not replaced.

This affects service worker code that follows the Workbox/Vite PWA pattern:

declare let self: ServiceWorkerGlobalScope;

precacheAndRoute(self.__WB_MANIFEST);

When using Rolldown/Oxc define replacement directly, self.__WB_MANIFEST is left unchanged. However, Vite 7/esbuild behavior erases the ambient declare and replaces the define.

Reproduction

Input:

declare let self: ServiceWorkerGlobalScope;

precacheAndRoute(self.__WB_MANIFEST);

Define config:

{
  "self.__WB_MANIFEST": "[]"
}

Current output:

precacheAndRoute(self.__WB_MANIFEST);

Expected output:

precacheAndRoute([]);

Why this matters

declare let self: ServiceWorkerGlobalScope is TypeScript-only. It does not create a runtime binding and should not shadow the global self.

Without the declaration, TypeScript complains about service-worker-specific globals and custom injected properties like self.__WB_MANIFEST. With the declaration present, Oxc/Rolldown currently fails to replace the manifest injection point.

Using globalThis.__WB_MANIFEST would avoid the shadowing issue, but changing existing Workbox/Vite PWA-compatible code from self.__WB_MANIFEST to globalThis.__WB_MANIFEST is a breaking change for downstream users.

Behavior in other tools

Tested on June 10, 2026:

  • vite@7.3.5
  • esbuild@0.28.0
  • @swc/core@1.15.41
  • rollup@4.61.1
  • @rollup/plugin-replace@6.0.3

esbuild

esbuild replaces the ambient self case:

declare let self: ServiceWorkerGlobalScope;
precacheAndRoute(self.__WB_MANIFEST);

let other;
precacheAndRoute(other.__WB_MANIFEST);

with define:

{
  "self.__WB_MANIFEST": "[]",
  "other.__WB_MANIFEST": "[]"
}

outputs:

var define_self_WB_MANIFEST_default = [];
precacheAndRoute(define_self_WB_MANIFEST_default);
let other;
precacheAndRoute(other.__WB_MANIFEST);

So declare let self does not block replacement, while a real runtime let other does.

Rollup

Rollup core does not have an esbuild-style define option, so there is no direct Rollup core behavior to compare.

The closest common Rollup equivalent is @rollup/plugin-replace, which is scope-insensitive textual replacement. With input:

declare let self: ServiceWorkerGlobalScope;
precacheAndRoute(self.__WB_MANIFEST);

let other;
precacheAndRoute(other.__WB_MANIFEST);

and replacement values:

{
  "self.__WB_MANIFEST": "[]",
  "other.__WB_MANIFEST": "[]"
}

it outputs:

precacheAndRoute([]);
precacheAndRoute([]);

It also replaces function-parameter shadows:

export function f(self) {
  precacheAndRoute(self.__WB_MANIFEST);
}

outputs:

function f(self) {
    precacheAndRoute([]);
}

export { f };

So @rollup/plugin-replace is closer to SWC jsc.transform.optimizer.globals.vars: it is aggressive and does not preserve shadowing semantics.

SWC

SWC behavior depends on the mechanism:

  • jsc.transform.optimizer.globals.vars is more aggressive and replaced even locally declared roots like let other; other.__WB_MANIFEST.
  • jsc.minify.compress.global_defs replaced top-level cases, but did not replace a function parameter shadow such as:
function f(self) {
  precacheAndRoute(self.__WB_MANIFEST);
}

Proposed behavior

For define replacement, an identifier root should be considered replaceable when it is either:

  1. unresolved/global, or
  2. resolved only to an ambient TypeScript declaration (declare)

Real runtime bindings should still block replacement:

let self;
precacheAndRoute(self.__WB_MANIFEST); // should not replace

function f(self) {
  precacheAndRoute(self.__WB_MANIFEST); // should not replace
}

This matches esbuild/Vite behavior while preserving normal shadowing semantics.

Metadata

Metadata

Assignees

Labels

A-transformerArea - Transformer / Transpiler

Type

Fields

Priority

None yet

Effort

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions