Overview
@JS(namespace: …) classes silently drop their @JS static func and @JS static var declarations from the generated bridge-js.d.ts. The JS runtime is unaffected (the class definition still has the static members and the namespace tree references it by symbol), but TypeScript consumers see an incomplete type and cannot call static factories through typeof MyNamespace.MyClass without a hand-written augmentation.
// Before: static members are missing from the namespace entry
__Swift: {
Foundation: {
Greeter: {
new(name: string): Greeter;
}
},
},
// After
__Swift: {
Foundation: {
Greeter: {
new(name: string): Greeter;
makeDefault(): Greeter;
readonly defaultGreeting: string;
}
},
},
Root cause
BridgeJSLink.generateTypeScript writes the export type Exports block using two different paths for classes:
- Non-namespaced classes (
renderExportedClass in BridgeJSLink.swift): builds dtsExportEntry by iterating klass.methods.filter(\.effects.isStatic) and the static subset of klass.properties. The produced entry is appended to data.dtsExportLines and includes constructor + static methods + static properties.
- Namespaced classes (inline
renderClassEntry closure inside generateTypeScript, passed to NamespaceBuilder.buildHierarchicalExportsType): only emitted the constructor. Static methods and static properties were completely ignored.
Because renderExportedClass is still called for every class (to produce the JS class body), the JS side has static makeDefault() etc. as expected, and the createExports namespace tree references the class symbol by name. That's why runtime calls to exports.__Swift.Foundation.Greeter.makeDefault() work even with a missing static declaration in .d.ts.
Fix
Mirror the non-namespaced path inside the renderClassEntry closure so namespaced classes emit their statics alongside new(...):
for method in klass.methods where method.effects.isStatic {
printer.write(
"\(method.name)\(self.renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));"
)
}
for property in klass.properties where property.isStatic {
let readonly = property.isReadonly ? "readonly " : ""
printer.write("\(readonly)\(property.name): \(property.type.tsType);")
}
13 lines added to BridgeJSLink.swift, no other source changes.
Regression coverage
Snapshot tests (Plugins/BridgeJS/Tests/):
Namespaces.swift already had two namespaced classes but none exercised static methods or properties. Extended __Swift.Foundation.Greeter with a static factory and a static readonly property. The Namespaces.swift and Namespaces.Global.swift snapshot sets now cover the fixed output across both the codegen and the linker test suites.
E2E test (Tests/BridgeJSRuntimeTests/, Tests/prelude.mjs):
Extended the existing @JS(namespace: "__Swift.Foundation") class UUID with @JS static func fromValue(_:) -> UUID and @JS static var placeholder: String, then added assertions in prelude.mjs that call both via exports.__Swift.Foundation.UUID.fromValue(...) and exports.__Swift.Foundation.UUID.placeholder. This proves the generated JavaScript routes calls to the correct static thunks at runtime, not just that the .d.ts types are well-formed.
Follow-up: path consolidation
With the fix in place, the inline renderClassEntry closure became a duplicate of what renderExportedClass already returns as dtsExportEntry. A follow-up commit replaces the closure with a direct renderExportedClass call, discarding the JS and instance-interface outputs and keeping only dtsExportEntry. This makes both paths share a single source of truth and threads throws through buildHierarchicalExportsType, populateTypeScriptExportLines, and generateTypeScript.
Verification
swift test --package-path ./Plugins/BridgeJS --disable-experimental-prebuilts — 105/105 pass on the default swift-syntax (600.0.1).
BRIDGEJS_OVERRIDE_SWIFT_SYNTAX_VERSION=602.0.0 swift test --package-path ./Plugins/BridgeJS --disable-experimental-prebuilts — 106/106 pass.
./Utilities/format.swift — no additional diffs.
./Utilities/bridge-js-generate.sh — AoT bindings updated for the new UUID static method and property added to the e2e test.