GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — BridgeJS: Emit static methods and properties on namespaced class entries by krodak · Pull Request #717 · swiftwasm/JavaScriptKit

Conversation

@krodak

@krodak krodak commented Apr 9, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

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:

  1. 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.
  2. 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.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@krodak
BridgeJS: Emit static methods and properties on namespaced class entries
6d2c070
The TypeScript `.d.ts` namespace-entry builder for `@JS(namespace:)`
classes only emitted the constructor, silently dropping any
`@JS static func` or `@JS static var` declared on the class. The
equivalent path for non-namespaced classes
(`renderExportedClass` → `dtsExportEntryPrinter`) iterates
`klass.methods.filter(\.effects.isStatic)` and the static subset of
`klass.properties`, so the output mismatched between the two paths.

The generated JS class still carries the static members via
`declarationPrefixKeyword: "static"` in `renderExportedClass`, and the
namespace tree references it by symbol, so the JavaScript runtime
works. TypeScript consumers, however, see an incomplete type and cannot
call the static factory through `typeof MyNamespace.MyClass` without
a hand-written augmentation.

Mirror the non-namespaced path inside the `renderClassEntry` closure in
`generateTypeScript` so namespaced classes emit their static methods
and static properties alongside `new(...)`.

Extended `Namespaces.swift` to exercise the codepath by adding a
static factory and a static readonly property on the existing
`__Swift.Foundation.Greeter` class. The Namespaces snapshot set
captures the fixed output.
@krodak krodak self-assigned this Apr 9, 2026
@krodak
krodak marked this pull request as draft April 9, 2026 12:05
krodak added 3 commits April 9, 2026 14:07
@krodak
BridgeJS: Consolidate namespaced and non-namespaced class DTS entry r…
9a56b28
…endering

The `renderClassEntry` closure passed to
`buildHierarchicalExportsType` manually rebuilt the same
`ClassName: { new(...); staticMethod(); ... }` block that
`renderExportedClass` already produces as its `dtsExportEntry` return
value. The two paths had to be kept in sync by hand.

Replace the inline closure with a direct call to `renderExportedClass`
that discards the JS and DTS-type outputs and returns only the
`dtsExportEntry` slice. This makes the class namespace entry for
namespaced and non-namespaced classes identical by construction.

Thread `throws` through `buildHierarchicalExportsType`,
`populateTypeScriptExportLines`, and `generateTypeScript` to
accommodate the fact that `renderExportedClass` is throwing. Hoist the
`buildHierarchicalExportsType` call out of the `printer.indent` closure
so the `try` expression is in a throwing context.
@krodak
BridgeJS: Add e2e test for static method and property on namespaced c…
d1ed185
…lass

Extends the existing `__Swift.Foundation.UUID` class (which already
has an `@JS init` and an `@JS func`) with a static factory
`fromValue(_:)` and a static readonly property `placeholder`, then
asserts both are reachable via `exports.__Swift.Foundation.UUID.fromValue`
and `exports.__Swift.Foundation.UUID.placeholder` in `prelude.mjs`.

This is the e2e counterpart to the snapshot regression added in the
previous commit — it proves the generated JavaScript actually routes
calls to the correct static thunks at runtime, not just that the
`.d.ts` types are well-formed.
@krodak
BridgeJS: Avoid double-rendering namespaced class DTS export entries
575b2bf
`renderExportedClass` was being called twice for every namespaced class:
once in `collectLinkData` (to produce the JS body and instance
interface) and once more inside the `renderClassEntry` closure (to
produce the export-entry for `buildHierarchicalExportsType`), with the
JS and instance-interface outputs discarded on the second call.

Store the `dtsExportEntry` slice in `LinkData.namespacedClassDtsExportEntries`
during the single `collectLinkData` pass, keyed by class name. The
`renderClassEntry` closure is now a non-throwing dictionary lookup, so
`buildHierarchicalExportsType`, `populateTypeScriptExportLines`, and
`generateTypeScript` revert to non-throwing.
@krodak
krodak marked this pull request as ready for review April 9, 2026 13:15
@krodak
krodak requested a review from kateinoigakukun April 9, 2026 13:15
@krodak
BridgeJS: Fix static property call expression for namespaced classes
57c1fd8
Static properties on `@JS(namespace:)` classes used
`property.callName()` to build the Swift call expression. That method
consults `property.staticContext`, which is stored as
`.className(abiName)` (e.g. `__Swift_Foundation_UUID`), and emits
`\(abiName).\(propertyName)`. The ABI-mangled name is not a valid Swift
identifier so the generated thunk failed to compile.

Static methods were unaffected because `renderSingleExportedMethod`
passes `klass.swiftCallName` directly.

Fix `PropertyRenderingContext.callName(for:)` for the `.classStatic`
case to bypass `property.callName()` and build the expression directly
from `klass.swiftCallName`. This preserves the ABI symbol name
(which still comes through `context.className` → `klass.abiName` →
`property.getterAbiName(className:)`) while producing a valid Swift
call expression in the thunk body.

Also remove the now-unnecessary comment from `prelude.mjs` that
described what the test was checking (the assertion itself is
self-explanatory).
kateinoigakukun
kateinoigakukun approved these changes Apr 9, 2026
View reviewed changes
@krodak
krodak merged commit 46182e2 into swiftwasm:main Apr 9, 2026
13 checks passed

Uh oh!

There was an error while loading. Please reload this page.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@kateinoigakukun kateinoigakukun kateinoigakukun approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@krodak krodak

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@krodak @kateinoigakukun
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.