From 2fc75d460a7bf74ed259301dd54e34a363a6a392 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:47:53 +0100 Subject: [PATCH 1/4] Bump actions/checkout from 6 to 7 (#770) Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 37e5bce78..1ce6ebe60 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: JAVASCRIPTKIT_WASI_BACKEND: ${{ matrix.entry.wasi-backend }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Export matrix env if: ${{ matrix.entry.env != '' && matrix.entry.env != null }} run: | @@ -79,7 +79,7 @@ jobs: container: image: ${{ matrix.entry.image }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Node.js uses: actions/setup-node@v6 with: @@ -105,7 +105,7 @@ jobs: xcode: Xcode_26.0.1 runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: swift build --product BridgeJSTool env: DEVELOPER_DIR: /Applications/${{ matrix.xcode }}.app/Contents/Developer/ @@ -116,7 +116,7 @@ jobs: prettier: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: '20' @@ -128,7 +128,7 @@ jobs: container: image: swift:6.3 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: ./Utilities/format.swift - name: Check for formatting changes run: | @@ -141,7 +141,7 @@ jobs: check-bridgejs-generated: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: ./.github/actions/install-swift with: download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2026-05-27-a/swift-DEVELOPMENT-SNAPSHOT-2026-05-27-a-ubuntu22.04.tar.gz @@ -158,7 +158,7 @@ jobs: build-examples: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: ./.github/actions/install-swift with: download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2026-05-27-a/swift-DEVELOPMENT-SNAPSHOT-2026-05-27-a-ubuntu22.04.tar.gz From 3c520b0c5ee690f2d441ea78aa3f3758d6f818f3 Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Tue, 23 Jun 2026 12:52:28 +0200 Subject: [PATCH 2/4] BridgeJS: Emit static members in declare global class declarations The `declare global { namespace ... }` class stub rendered every method without `static` and filtered properties to instance-only, so a `@JS static func` on a namespaced class was typed as an instance method and a `@JS static var` was omitted from the generated `.d.ts`. This was a type-only defect: the emitted JavaScript already exposes these members statically (and the class's namespace export entry types them correctly), so only TypeScript consumers were affected. Split static and instance members in this path: emit static methods with `static` and include static properties. This has been incorrect since the global namespace class stub was introduced, not a regression. The `Namespaces.Global` snapshot already exercises a namespaced class with `static func`/`static var` but had recorded the wrong output, so it is updated to the corrected declarations. --- .../BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift | 12 +++++++----- .../BridgeJSLinkTests/Namespaces.Global.d.ts | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 8c9c20a14..01c390f26 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -3143,17 +3143,19 @@ extension BridgeJSLink { let sortedMethods = klass.methods.sorted { $0.name < $1.name } for method in sortedMethods { + let staticKeyword = method.effects.isStatic ? "static " : "" let methodSignature = - "\(method.name)\(renderTSSignatureCallback(method.parameters, method.returnType, method.effects));" + "\(staticKeyword)\(method.name)\(renderTSSignatureCallback(method.parameters, method.returnType, method.effects));" printer.write(methodSignature) } - let sortedProperties = klass.properties.filter { !$0.isStatic }.sorted { - $0.name < $1.name - } + let sortedProperties = klass.properties.sorted { $0.name < $1.name } for property in sortedProperties { + let staticKeyword = property.isStatic ? "static " : "" let readonly = property.isReadonly ? "readonly " : "" - printer.write("\(readonly)\(property.name): \(property.type.tsType);") + printer.write( + "\(staticKeyword)\(readonly)\(property.name): \(property.type.tsType);" + ) } printer.write("release(): void;") diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.d.ts index 1353220bc..d9af0c8eb 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.d.ts @@ -34,7 +34,8 @@ declare global { class Greeter { constructor(name: string); greet(): string; - makeDefault(): Greeter; + static makeDefault(): Greeter; + static readonly defaultGreeting: string; release(): void; } class UUID { From 45088be53a11a96871a261e31bb753fe39bcddac Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Mon, 22 Jun 2026 17:24:51 +0200 Subject: [PATCH 3/4] BridgeJS: Include Swift doc comments in generated d.ts Propagate Swift `///` and `/** */` documentation on exported declarations into the generated TypeScript declarations as JSDoc, so editors surface hover docs for the bridged API. The exporter now captures the leading doc comment for functions, classes, methods, properties, constructors, structs, and enums into the skeleton, and the linker renders it as a single JSDoc block. The Swift DocC field list is mapped as the inverse of the TS2Swift importer: the leading description becomes the JSDoc body, `- Parameters:`/`- Parameter x:` become `@param`, `- Returns:` becomes `@returns`, and `- Throws:` becomes `@throws`. Existing default-value annotations are merged into the same block so a parameter never emits two comment blocks; declarations without doc comments produce byte-identical output to before. --- .../Generated/JavaScript/BridgeJS.json | 1 + .../BridgeJSCore/SwiftToSkeleton.swift | 84 +++- .../Sources/BridgeJSLink/BridgeJSLink.swift | 297 ++++++++++-- .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 45 +- .../Inputs/MacroSwift/DocComments.swift | 91 ++++ .../Inputs/MacroSwift/Namespaces.swift | 5 + .../BridgeJSCodegenTests/DocComments.json | 436 ++++++++++++++++++ .../BridgeJSCodegenTests/DocComments.swift | 317 +++++++++++++ .../Namespaces.Global.json | 3 + .../BridgeJSCodegenTests/Namespaces.json | 3 + .../BridgeJSLinkTests/DocComments.d.ts | 140 ++++++ .../BridgeJSLinkTests/DocComments.js | 421 +++++++++++++++++ .../BridgeJSLinkTests/Namespaces.Global.d.ts | 22 + .../BridgeJSLinkTests/Namespaces.d.ts | 11 + 14 files changed, 1808 insertions(+), 68 deletions(-) create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/DocComments.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DocComments.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DocComments.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DocComments.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DocComments.js diff --git a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json index 6f1fc940c..c4a3a8c9d 100644 --- a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json +++ b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json @@ -16,6 +16,7 @@ "methods" : [ { "abiName" : "bjs_PlayBridgeJS_updateDetailed", + "documentation" : "Structured entry point used by the playground so JS doesn't need to parse diagnostics.", "effects" : { "isAsync" : false, "isStatic" : false, diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index a6afe2779..ed7ebcd1b 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -1266,10 +1266,56 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { returnType: returnType, effects: effects, namespace: finalNamespace, - staticContext: staticContext + staticContext: staticContext, + documentation: extractDocumentation(from: node) ) } + /// Returns the doc comment (`///` or `/** */`) attached to a declaration, with + /// markers stripped and DocC field lists (`- Parameters:`, `- Returns:`) preserved. + private func extractDocumentation(from node: some SyntaxProtocol) -> String? { + var run: [String] = [] + for piece in node.leadingTrivia { + switch piece { + case .docLineComment(let text): + var line = Substring(text) + if line.hasPrefix("///") { line = line.dropFirst(3) } + if line.first == " " { line = line.dropFirst() } + if line.last == "\r" { line = line.dropLast() } + run.append(String(line)) + case .docBlockComment(let text): + run.append(contentsOf: stripBlockComment(text)) + case .newlines(let count), .carriageReturns(let count), .carriageReturnLineFeeds(let count): + if count >= 2 { run.removeAll() } + case .lineComment, .blockComment: + run.removeAll() + default: + continue + } + } + // Trim boundary blank lines so line (`///`) and block (`/** */`) comments + // produce a consistent skeleton value. + while run.first?.trimmingCharacters(in: .whitespaces).isEmpty == true { run.removeFirst() } + while run.last?.trimmingCharacters(in: .whitespaces).isEmpty == true { run.removeLast() } + return run.isEmpty ? nil : run.joined(separator: "\n") + } + + private func stripBlockComment(_ text: String) -> [String] { + var body = Substring(text) + if body.hasPrefix("/**") { body = body.dropFirst(3) } + if body.hasSuffix("*/") { body = body.dropLast(2) } + return body.split(separator: "\n", omittingEmptySubsequences: false).map { raw -> String in + var line = raw[...] + if line.last == "\r" { line = line.dropLast() } + while let first = line.first, first == " " || first == "\t" { line = line.dropFirst() } + if line.first == "*" { + line = line.dropFirst() + if line.first == " " { line = line.dropFirst() } + } + return String(line) + } + } + private func collectEffects(signature: FunctionSignatureSyntax, isStatic: Bool = false) -> Effects? { let isAsync = signature.effectSpecifiers?.asyncSpecifier != nil var isThrows = false @@ -1360,7 +1406,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { let constructor = ExportedConstructor( abiName: "bjs_\(classAbiName)_init", parameters: parameters, - effects: effects + effects: effects, + documentation: extractDocumentation(from: node) ) exportedClassByName[classKey]?.constructor = constructor @@ -1383,7 +1430,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { let constructor = ExportedConstructor( abiName: "bjs_\(structAbiName)_init", parameters: parameters, - effects: effects + effects: effects, + documentation: extractDocumentation(from: node) ) exportedStructByName[structKey]?.constructor = constructor @@ -1490,7 +1538,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { isReadonly: isReadonly, isStatic: isStatic, namespace: finalNamespace, - staticContext: staticContext + staticContext: staticContext, + documentation: extractDocumentation(from: node) ) if case .enumBody(_, let key) = state { @@ -1537,7 +1586,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { methods: [], properties: [], namespace: effectiveNamespace, - identityMode: classIdentityMode + identityMode: classIdentityMode, + documentation: extractDocumentation(from: node) ) let uniqueKey = makeKey(name: name, namespace: effectiveNamespace) @@ -1657,7 +1707,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { namespace: effectiveNamespace, emitStyle: emitStyle, staticMethods: [], - staticProperties: [] + staticProperties: [], + documentation: extractDocumentation(from: node) ) let enumUniqueKey = makeKey(name: name, namespace: effectiveNamespace) @@ -1774,7 +1825,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { name: name, methods: [], properties: [], - namespace: effectiveNamespace + namespace: effectiveNamespace, + documentation: extractDocumentation(from: node) ) stateStack.push(state: .protocolBody(name: name, key: protocolUniqueKey)) @@ -1798,7 +1850,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { name: name, methods: methods, properties: exportedProtocolByName[protocolUniqueKey]?.properties ?? [], - namespace: effectiveNamespace + namespace: effectiveNamespace, + documentation: extractDocumentation(from: node) ) exportedProtocolByName[protocolUniqueKey] = exportedProtocol @@ -1874,7 +1927,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { isReadonly: true, isStatic: false, namespace: effectiveNamespace, - staticContext: nil + staticContext: nil, + documentation: extractDocumentation(from: varDecl) ) properties.append(property) } @@ -1888,7 +1942,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { explicitAccessControl: explicitAccessControl, properties: properties, methods: [], - namespace: effectiveNamespace + namespace: effectiveNamespace, + documentation: extractDocumentation(from: node) ) exportedStructByName[structUniqueKey] = exportedStruct @@ -1981,7 +2036,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { returnType: returnType, effects: effects, namespace: namespace, - staticContext: nil + staticContext: nil, + documentation: extractDocumentation(from: node) ) } @@ -2022,7 +2078,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { let exportedProperty = ExportedProtocolProperty( name: propertyName, type: propertyType, - isReadonly: isReadonly + isReadonly: isReadonly, + documentation: extractDocumentation(from: node) ) if var currentProtocol = exportedProtocolByName[protocolKey] { @@ -2033,7 +2090,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { name: currentProtocol.name, methods: currentProtocol.methods, properties: properties, - namespace: currentProtocol.namespace + namespace: currentProtocol.namespace, + documentation: currentProtocol.documentation ) exportedProtocolByName[protocolKey] = currentProtocol } diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 01c390f26..a24fde09b 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -936,14 +936,19 @@ public struct BridgeJSLink { for skeleton in exportedSkeletons { for proto in skeleton.protocols { + printer.write(lines: renderJSDoc(documentation: proto.documentation, parameters: [])) printer.write("export interface \(proto.name) {") printer.indent { for method in proto.methods { + printer.write( + lines: renderJSDoc(documentation: method.documentation, parameters: method.parameters) + ) printer.write( "\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));" ) } for property in proto.properties { + printer.write(lines: renderJSDoc(documentation: property.documentation, parameters: [])) let propertySignature = property.isReadonly ? "readonly \(property.name): \(resolveTypeScriptType(property.type));" @@ -977,12 +982,19 @@ public struct BridgeJSLink { printer.write("export type \(enumObjectName) = typeof \(fullEnumValuesPath) & {") printer.indent { for function in enumDefinition.staticMethods { + printer.write( + lines: renderJSDoc( + documentation: function.documentation, + parameters: function.parameters + ) + ) printer.write( "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" ) } for property in enumDefinition.staticProperties { let readonly = property.isReadonly ? "readonly " : "" + printer.write(lines: renderJSDoc(documentation: property.documentation, parameters: [])) printer.write("\(readonly)\(property.name): \(resolveTypeScriptType(property.type));") } } @@ -999,6 +1011,9 @@ public struct BridgeJSLink { exportedSkeletons: exportedSkeletons, renderTSSignatureCallback: { parameters, returnType, effects in self.renderTSSignature(parameters: parameters, returnType: returnType, effects: effects) + }, + renderDocCallback: { documentation, parameters in + self.renderJSDoc(documentation: documentation, parameters: parameters) } ) printer.write(lines: namespaceDeclarationsLines) @@ -1014,8 +1029,16 @@ public struct BridgeJSLink { renderClassEntry: { klass in data.namespacedClassDtsExportEntries[klass.name] ?? [] }, - renderFunctionSignature: { function in - "\(function.name)\(self.renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" + renderFunctionEntry: { function in + self.renderJSDoc(documentation: function.documentation, parameters: function.parameters) + + [ + "\(function.name)\(self.renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" + ] + }, + renderPropertyEntry: { property in + let readonly = property.isReadonly ? "readonly " : "" + return self.renderJSDoc(documentation: property.documentation, parameters: []) + + ["\(readonly)\(property.name): \(property.type.tsType);"] } ) printer.write("export type Exports = {") @@ -1574,12 +1597,6 @@ public struct BridgeJSLink { .replacingOccurrences(of: "\"", with: "\\\"") } - /// Helper method to append JSDoc comments for parameters with default values - private func appendJSDocIfNeeded(for parameters: [Parameter], to lines: inout [String]) { - let jsDocLines = DefaultValueUtils.formatJSDoc(for: parameters) - lines.append(contentsOf: jsDocLines) - } - func renderExportedStruct( _ structDefinition: ExportedStruct ) throws -> (js: [String], dtsType: [String], dtsExportEntry: [String]) { @@ -1589,15 +1606,24 @@ public struct BridgeJSLink { let staticProperties = structDefinition.properties.filter { $0.isStatic } let dtsTypePrinter = CodeFragmentPrinter() + for line in renderJSDoc(documentation: structDefinition.documentation, parameters: []) { + dtsTypePrinter.write(line) + } dtsTypePrinter.write("export interface \(structName) {") let instanceProps = structDefinition.properties.filter { !$0.isStatic } dtsTypePrinter.indent { for property in instanceProps { let tsType = resolveTypeScriptType(property.type) + for line in renderJSDoc(documentation: property.documentation, parameters: []) { + dtsTypePrinter.write(line) + } dtsTypePrinter.write("\(property.name): \(tsType);") } for method in structDefinition.methods where !method.effects.isStatic { - let jsDocLines = DefaultValueUtils.formatJSDoc(for: method.parameters) + let jsDocLines = renderJSDoc( + documentation: method.documentation, + parameters: method.parameters + ) dtsTypePrinter.write(lines: jsDocLines) let signature = renderTSSignature( parameters: method.parameters, @@ -1659,7 +1685,10 @@ public struct BridgeJSLink { dtsExportEntryPrinter.write("\(structName): {") dtsExportEntryPrinter.indent { if let constructor = structDefinition.constructor { - let jsDocLines = DefaultValueUtils.formatJSDoc(for: constructor.parameters) + let jsDocLines = renderJSDoc( + documentation: constructor.documentation, + parameters: constructor.parameters + ) dtsExportEntryPrinter.write(lines: jsDocLines) dtsExportEntryPrinter.write( "init\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftStruct(structDefinition.swiftCallName), effects: constructor.effects));" @@ -1667,10 +1696,16 @@ public struct BridgeJSLink { } for property in staticProperties { let readonly = property.isReadonly ? "readonly " : "" + for line in renderJSDoc(documentation: property.documentation, parameters: []) { + dtsExportEntryPrinter.write(line) + } dtsExportEntryPrinter.write("\(readonly)\(property.name): \(resolveTypeScriptType(property.type));") } for method in staticMethods { - let jsDocLines = DefaultValueUtils.formatJSDoc(for: method.parameters) + let jsDocLines = renderJSDoc( + documentation: method.documentation, + parameters: method.parameters + ) dtsExportEntryPrinter.write(lines: jsDocLines) dtsExportEntryPrinter.write( "\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));" @@ -1770,6 +1805,10 @@ public struct BridgeJSLink { let printer = CodeFragmentPrinter() let enumValuesName = enumDefinition.valuesName + for line in renderJSDoc(documentation: enumDefinition.documentation, parameters: []) { + printer.write(line) + } + switch enumDefinition.emitStyle { case .tsEnum: switch enumDefinition.enumType { @@ -1883,7 +1922,7 @@ extension BridgeJSLink { ) var dtsLines: [String] = [] - appendJSDocIfNeeded(for: function.parameters, to: &dtsLines) + dtsLines.append(contentsOf: renderJSDoc(documentation: function.documentation, parameters: function.parameters)) dtsLines.append( "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" @@ -1935,7 +1974,7 @@ extension BridgeJSLink { var dtsLines: [String] = [] - appendJSDocIfNeeded(for: function.parameters, to: &dtsLines) + dtsLines.append(contentsOf: renderJSDoc(documentation: function.documentation, parameters: function.parameters)) dtsLines.append( "static \(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" @@ -1966,7 +2005,7 @@ extension BridgeJSLink { var dtsLines: [String] = [] - appendJSDocIfNeeded(for: function.parameters, to: &dtsLines) + dtsLines.append(contentsOf: renderJSDoc(documentation: function.documentation, parameters: function.parameters)) dtsLines.append( "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" @@ -2082,6 +2121,9 @@ extension BridgeJSLink { let dtsTypePrinter = CodeFragmentPrinter() let dtsExportEntryPrinter = CodeFragmentPrinter() + for line in renderJSDoc(documentation: klass.documentation, parameters: []) { + dtsTypePrinter.write(line) + } dtsTypePrinter.write("export interface \(klass.name) extends SwiftHeapObject {") dtsExportEntryPrinter.write("\(klass.name): {") jsPrinter.write("class \(klass.name) extends SwiftHeapObject {") @@ -2134,7 +2176,10 @@ extension BridgeJSLink { } dtsExportEntryPrinter.indent { - let jsDocLines = DefaultValueUtils.formatJSDoc(for: constructor.parameters) + let jsDocLines = renderJSDoc( + documentation: constructor.documentation, + parameters: constructor.parameters + ) for line in jsDocLines { dtsExportEntryPrinter.write(line) } @@ -2167,6 +2212,12 @@ extension BridgeJSLink { } dtsExportEntryPrinter.indent { + for line in renderJSDoc( + documentation: method.documentation, + parameters: method.parameters + ) { + dtsExportEntryPrinter.write(line) + } dtsExportEntryPrinter.write( "\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));" ) @@ -2194,6 +2245,12 @@ extension BridgeJSLink { } dtsTypePrinter.indent { + for line in renderJSDoc( + documentation: method.documentation, + parameters: method.parameters + ) { + dtsTypePrinter.write(line) + } dtsTypePrinter.write( "\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));" ) @@ -2281,6 +2338,9 @@ extension BridgeJSLink { // Add instance property to TypeScript interface definition let readonly = property.isReadonly ? "readonly " : "" dtsPrinter.indent { + for line in renderJSDoc(documentation: property.documentation, parameters: []) { + dtsPrinter.write(line) + } dtsPrinter.write("\(readonly)\(property.name): \(property.type.tsType);") } } @@ -2708,6 +2768,7 @@ extension BridgeJSLink { var functionDtsLines: [(name: String, lines: [String])] = [] var classDtsLines: [(name: String, lines: [String])] = [] var enumDtsLines: [(name: String, line: String)] = [] + var staticPropertyDtsLines: [(name: String, lines: [String])] = [] var propertyJsLines: [String] = [] } @@ -2791,7 +2852,8 @@ extension BridgeJSLink { fileprivate func buildHierarchicalExportsType( exportedSkeletons: [ExportedSkeleton], renderClassEntry: (ExportedClass) -> [String], - renderFunctionSignature: (ExportedFunction) -> String + renderFunctionEntry: (ExportedFunction) -> [String], + renderPropertyEntry: (ExportedProperty) -> [String] ) -> [String] { let printer = CodeFragmentPrinter() let rootNode = NamespaceNode(name: "") @@ -2802,7 +2864,8 @@ extension BridgeJSLink { populateTypeScriptExportLines( node: node, renderClassEntry: renderClassEntry, - renderFunctionSignature: renderFunctionSignature + renderFunctionEntry: renderFunctionEntry, + renderPropertyEntry: renderPropertyEntry ) } @@ -2814,11 +2877,11 @@ extension BridgeJSLink { private func populateTypeScriptExportLines( node: NamespaceNode, renderClassEntry: (ExportedClass) -> [String], - renderFunctionSignature: (ExportedFunction) -> String + renderFunctionEntry: (ExportedFunction) -> [String], + renderPropertyEntry: (ExportedProperty) -> [String] ) { for function in node.content.functions { - let signature = renderFunctionSignature(function) - node.content.functionDtsLines.append((function.name, [signature])) + node.content.functionDtsLines.append((function.name, renderFunctionEntry(function))) } for klass in node.content.classes { @@ -2826,6 +2889,10 @@ extension BridgeJSLink { node.content.classDtsLines.append((klass.name, entry)) } + for property in node.content.staticProperties { + node.content.staticPropertyDtsLines.append((property.name, renderPropertyEntry(property))) + } + for enumDef in node.content.enums { node.content.enumDtsLines.append((enumDef.name, "\(enumDef.name): \(enumDef.objectTypeName)")) } @@ -2834,7 +2901,8 @@ extension BridgeJSLink { populateTypeScriptExportLines( node: childNode, renderClassEntry: renderClassEntry, - renderFunctionSignature: renderFunctionSignature + renderFunctionEntry: renderFunctionEntry, + renderPropertyEntry: renderPropertyEntry ) } } @@ -2962,9 +3030,8 @@ extension BridgeJSLink { printer.write(line) } - for property in childNode.content.staticProperties.sorted(by: { $0.name < $1.name }) { - let readonly = property.isReadonly ? "readonly " : "" - printer.write("\(readonly)\(property.name): \(property.type.tsType);") + for (_, lines) in childNode.content.staticPropertyDtsLines.sorted(by: { $0.name < $1.name }) { + printer.write(lines: lines) } for (_, lines) in childNode.content.functionDtsLines.sorted(by: { $0.name < $1.name }) { @@ -3030,7 +3097,8 @@ extension BridgeJSLink { /// - Returns: Array of TypeScript declaration lines defining the global namespace structure func namespaceDeclarations( exportedSkeletons: [ExportedSkeleton], - renderTSSignatureCallback: @escaping ([Parameter], BridgeType, Effects) -> String + renderTSSignatureCallback: @escaping ([Parameter], BridgeType, Effects) -> String, + renderDocCallback: @escaping (String?, [Parameter]) -> [String] ) -> [String] { let printer = CodeFragmentPrinter() @@ -3052,7 +3120,8 @@ extension BridgeJSLink { printer: printer, exposeToGlobal: true, exportedSkeletons: exportedSkeletons, - renderTSSignatureCallback: renderTSSignatureCallback + renderTSSignatureCallback: renderTSSignatureCallback, + renderDocCallback: renderDocCallback ) printer.unindent() printer.write("}") @@ -3071,7 +3140,8 @@ extension BridgeJSLink { printer: printer, exposeToGlobal: false, exportedSkeletons: exportedSkeletons, - renderTSSignatureCallback: renderTSSignatureCallback + renderTSSignatureCallback: renderTSSignatureCallback, + renderDocCallback: renderDocCallback ) } } @@ -3085,7 +3155,8 @@ extension BridgeJSLink { printer: CodeFragmentPrinter, exposeToGlobal: Bool, exportedSkeletons: [ExportedSkeleton], - renderTSSignatureCallback: @escaping ([Parameter], BridgeType, Effects) -> String + renderTSSignatureCallback: @escaping ([Parameter], BridgeType, Effects) -> String, + renderDocCallback: @escaping (String?, [Parameter]) -> [String] ) { func hasContent(node: NamespaceNode) -> Bool { // Enums and structs are always included @@ -3129,6 +3200,7 @@ extension BridgeJSLink { if exposeToGlobal { let sortedClasses = childNode.content.classes.sorted { $0.name < $1.name } for klass in sortedClasses { + printer.write(lines: renderDocCallback(klass.documentation, [])) printer.write("class \(klass.name) {") printer.indent { if let constructor = klass.constructor { @@ -3138,6 +3210,9 @@ extension BridgeJSLink { } let constructorSignature = "constructor(\(paramSignatures.joined(separator: ", ")));" + printer.write( + lines: renderDocCallback(constructor.documentation, constructor.parameters) + ) printer.write(constructorSignature) } @@ -3146,6 +3221,7 @@ extension BridgeJSLink { let staticKeyword = method.effects.isStatic ? "static " : "" let methodSignature = "\(staticKeyword)\(method.name)\(renderTSSignatureCallback(method.parameters, method.returnType, method.effects));" + printer.write(lines: renderDocCallback(method.documentation, method.parameters)) printer.write(methodSignature) } @@ -3153,6 +3229,7 @@ extension BridgeJSLink { for property in sortedProperties { let staticKeyword = property.isStatic ? "static " : "" let readonly = property.isReadonly ? "readonly " : "" + printer.write(lines: renderDocCallback(property.documentation, [])) printer.write( "\(staticKeyword)\(readonly)\(property.name): \(property.type.tsType);" ) @@ -3167,6 +3244,7 @@ extension BridgeJSLink { // Generate enum definitions within declare global namespace let sortedEnums = childNode.content.enums.sorted { $0.name < $1.name } for enumDefinition in sortedEnums { + printer.write(lines: renderDocCallback(enumDefinition.documentation, [])) let style: EnumEmitStyle = enumDefinition.emitStyle let enumValuesName = enumDefinition.valuesName switch enumDefinition.enumType { @@ -3275,6 +3353,7 @@ extension BridgeJSLink { let sortedStructs = childNode.content.structs.sorted { $0.name < $1.name } for structDef in sortedStructs { let instanceProps = structDef.properties.filter { !$0.isStatic } + printer.write(lines: renderDocCallback(structDef.documentation, [])) printer.write("export interface \(structDef.name) {") printer.indent { for property in instanceProps { @@ -3282,6 +3361,7 @@ extension BridgeJSLink { property.type, exportedSkeletons: exportedSkeletons ) + printer.write(lines: renderDocCallback(property.documentation, [])) printer.write("\(property.name): \(tsType);") } } @@ -3294,11 +3374,13 @@ extension BridgeJSLink { for function in sortedFunctions { let signature = "function \(function.name)\(renderTSSignatureCallback(function.parameters, function.returnType, function.effects));" + printer.write(lines: renderDocCallback(function.documentation, function.parameters)) printer.write(signature) } let sortedProperties = childNode.content.staticProperties.sorted { $0.name < $1.name } for property in sortedProperties { let readonly = property.isReadonly ? "var " : "let " + printer.write(lines: renderDocCallback(property.documentation, [])) printer.write("\(readonly)\(property.name): \(property.type.tsType);") } } @@ -3309,7 +3391,8 @@ extension BridgeJSLink { printer: printer, exposeToGlobal: exposeToGlobal, exportedSkeletons: exportedSkeletons, - renderTSSignatureCallback: renderTSSignatureCallback + renderTSSignatureCallback: renderTSSignatureCallback, + renderDocCallback: renderDocCallback ) printer.unindent() @@ -3668,24 +3751,6 @@ enum DefaultValueUtils { .replacingOccurrences(of: "\"", with: "\\\"") } - /// Generates JSDoc comment lines for parameters with default values - static func formatJSDoc(for parameters: [Parameter]) -> [String] { - let paramsWithDefaults = parameters.filter { $0.hasDefault } - guard !paramsWithDefaults.isEmpty else { - return [] - } - - var jsDocLines: [String] = ["/**"] - for param in paramsWithDefaults { - if let defaultValue = param.defaultValue { - let defaultDoc = format(defaultValue, as: .typescript) - jsDocLines.append(" * @param \(param.name) - Optional parameter (default: \(defaultDoc))") - } - } - jsDocLines.append(" */") - return jsDocLines - } - /// Generates a JavaScript parameter list with default values static func formatParameterList(_ parameters: [Parameter]) -> String { return parameters.map { param in @@ -3698,6 +3763,144 @@ enum DefaultValueUtils { } } +extension BridgeJSLink { + /// Renders the JSDoc block for an exported declaration, mapping the Swift DocC + /// comment to `@param`/`@returns`/`@throws` and merging any default-value notes. + /// Returns an empty array when there is nothing to document. + fileprivate func renderJSDoc(documentation: String?, parameters: [Parameter]) -> [String] { + let parsed = documentation.map(DocCComment.init(parsing:)) ?? DocCComment() + + var tagLines: [String] = [] + for parameter in parameters { + let docText = parsed.parameter(named: parameter.name) + let defaultValue = parameter.defaultValue.map { DefaultValueUtils.format($0, as: .typescript) } + switch (docText, defaultValue) { + case let (.some(text), .some(value)): + tagLines.append("@param \(parameter.name) \(text) (default: \(value))") + case let (.some(text), .none): + tagLines.append("@param \(parameter.name) \(text)") + case let (.none, .some(value)): + tagLines.append("@param \(parameter.name) - Optional parameter (default: \(value))") + case (.none, .none): + continue + } + } + if let returns = parsed.returns { + tagLines.append(returns.isEmpty ? "@returns" : "@returns \(returns)") + } + if let thrown = parsed.throws { + tagLines.append(thrown.isEmpty ? "@throws" : "@throws \(thrown)") + } + + guard !parsed.description.isEmpty || !tagLines.isEmpty else { return [] } + + // `*/` in the doc text would prematurely close the JSDoc block comment. + func escape(_ text: String) -> String { text.replacingOccurrences(of: "*/", with: "*\\/") } + + var lines: [String] = ["/**"] + lines.append(contentsOf: parsed.description.map { $0.isEmpty ? " *" : " * \(escape($0))" }) + lines.append(contentsOf: tagLines.map { " * \(escape($0))" }) + lines.append(" */") + return lines + } +} + +/// A parsed Swift DocC comment: a description block plus its `- Parameters:`, +/// `- Returns:`, and `- Throws:` field items. +private struct DocCComment { + var description: [String] = [] + var parameters: [(name: String, text: String)] = [] + var returns: String? + var `throws`: String? + + init() {} + + init(parsing text: String) { + enum Target { case description, parameter(Int), returns, `throws`, none } + var target: Target = .description + + func append(continuation line: String) { + let trimmed = line.trimmingCharacters(in: .whitespaces) + switch target { + case .description: description.append(line) + case .parameter(let index) where !trimmed.isEmpty: parameters[index].text += " \(trimmed)" + case .returns where !trimmed.isEmpty: returns = [returns, trimmed].compactMap { $0 }.joined(separator: " ") + case .throws where !trimmed.isEmpty: + `throws` = [`throws`, trimmed].compactMap { $0 }.joined(separator: " ") + default: return + } + } + + func addParameter(_ name: String, _ desc: String) { + parameters.append((name: name, text: desc)) + target = .parameter(parameters.count - 1) + } + + func isInParameterList(_ target: Target) -> Bool { + switch target { + case .none, .parameter: return true + default: return false + } + } + + for rawLine in text.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) { + let trimmed = rawLine.trimmingCharacters(in: .whitespaces) + if trimmed == "- Parameters:" { + target = .none + } else if let (name, desc) = Self.listItem(trimmed, keyword: "Parameter") { + addParameter(name, desc) + } else if let desc = Self.field(trimmed, keyword: "Returns") { + returns = desc + target = .returns + } else if let desc = Self.field(trimmed, keyword: "Throws") { + `throws` = desc + target = .throws + } else if isInParameterList(target), let (name, desc) = Self.bareItem(trimmed) { + addParameter(name, desc) + } else { + append(continuation: rawLine) + } + } + + while description.first?.trimmingCharacters(in: .whitespaces).isEmpty == true { description.removeFirst() } + while description.last?.trimmingCharacters(in: .whitespaces).isEmpty == true { description.removeLast() } + } + + func parameter(named name: String) -> String? { + parameters.first { $0.name == name }?.text + } + + /// Matches `- Keyword name: description`. + private static func listItem(_ line: String, keyword: String) -> (String, String)? { + guard line.hasPrefix("- \(keyword) ") else { return nil } + return splitNameAndDescription(String(line.dropFirst("- \(keyword) ".count))) + } + + /// Matches `- name: description` (a sub-item of a `- Parameters:` block). + private static func bareItem(_ line: String) -> (String, String)? { + guard line.hasPrefix("- ") else { return nil } + guard let (name, desc) = splitNameAndDescription(String(line.dropFirst(2))), !name.contains(" ") else { + return nil + } + return (name, desc) + } + + /// Matches `- Keyword: description`, returning the (possibly empty) description. + private static func field(_ line: String, keyword: String) -> String? { + if line.hasPrefix("- \(keyword): ") { + return String(line.dropFirst("- \(keyword): ".count)).trimmingCharacters(in: .whitespaces) + } + return line == "- \(keyword):" ? "" : nil + } + + private static func splitNameAndDescription(_ rest: String) -> (String, String)? { + guard let colon = rest.firstIndex(of: ":") else { return nil } + let name = String(rest[.. String { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/DocComments.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/DocComments.swift new file mode 100644 index 000000000..8cdc72d4d --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/DocComments.swift @@ -0,0 +1,91 @@ +/// Returns a greeting for a user. +/// - Parameters: +/// - name: The user's name. +/// - greeting: The greeting word to use. +/// - Returns: The composed greeting message. +@JS func greet(name: String, greeting: String = "Hello") -> String { + return "\(greeting), \(name)!" +} + +/// Adds two numbers together. +/// - Parameter a: The first addend. +/// - Parameter b: The second addend. +/// - Returns: The sum of the inputs. +@JS func add(a: Int, b: Int) -> Int { a + b } + +/// +/// Has blank doc lines around the summary; boundaries should be trimmed. +/// +@JS func trimmed() {} + +/** + * Says hello to the world. + * + * Demonstrates that block doc comments are supported too. + */ +@JS func hello() {} + +/// Parses an integer from text. +/// - Parameter text: The text to parse. +/// - Returns: The parsed integer. +/// - Throws: A `JSException` when the text is not a valid integer. +@JS func parseInt(text: String) throws(JSException) -> Int { 0 } + +/// A greeter that keeps the target name. +@JS class Greeter { + /// The configured name. + @JS var name: String + + /// Create a greeter. + /// - Parameter name: The name to greet. + @JS init(name: String) { + self.name = name + } + + /// Returns a greeting for the configured name. + /// - Returns: The greeting message. + @JS func greet() -> String { + return "Hello, " + self.name + "!" + } +} + +/// A 2D point in space. +@JS struct Point { + /// The horizontal position. + let x: Double + /// The vertical position. + let y: Double +} + +/// A primary color channel. +@JS enum Color { + case red + case green + case blue + + /// The default channel. + @JS static var fallback: String { "red" } + + /// Returns the canonical name for a channel label. + /// - Parameter label: The raw label. + /// - Returns: The canonical channel name. + @JS static func canonical(label: String) -> String { label } +} + +/// Receives lifecycle callbacks. +@JS protocol Listener { + /// The listener's display name. + var name: String { get } + + /// Called when an event fires. + /// - Parameter id: The event identifier. + func onEvent(id: Int) +} + +/// Doubles a value, in a namespace. +/// - Parameter value: The value to double. +/// - Returns: Twice the input. +@JS(namespace: "MathUtils") func double(value: Int) -> Int { value * 2 } + +/// Returns the JSDoc terminator */ embedded mid-sentence. +@JS func terminator() -> String { "*/" } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Namespaces.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Namespaces.swift index 7cd63c698..7ad3037b9 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Namespaces.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Namespaces.swift @@ -1,7 +1,10 @@ @JS func plainFunction() -> String { "plain" } +/// A namespaced free function. +/// - Returns: A fixed namespaced string. @JS(namespace: "MyModule.Utils") func namespacedFunction() -> String { "namespaced" } +/// A greeter living in a namespace. @JS(namespace: "__Swift.Foundation") class Greeter { var name: String @@ -9,6 +12,8 @@ self.name = name } + /// Produces a greeting for the configured name. + /// - Returns: The greeting message. @JS func greet() -> String { return "Hello, " + self.name + "!" } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DocComments.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DocComments.json new file mode 100644 index 000000000..c69fca509 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DocComments.json @@ -0,0 +1,436 @@ +{ + "exported" : { + "classes" : [ + { + "constructor" : { + "abiName" : "bjs_Greeter_init", + "documentation" : "Create a greeter.\n- Parameter name: The name to greet.", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "name", + "name" : "name", + "type" : { + "string" : { + + } + } + } + ] + }, + "documentation" : "A greeter that keeps the target name.", + "methods" : [ + { + "abiName" : "bjs_Greeter_greet", + "documentation" : "Returns a greeting for the configured name.\n- Returns: The greeting message.", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "greet", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + } + ], + "name" : "Greeter", + "properties" : [ + { + "documentation" : "The configured name.", + "isReadonly" : false, + "isStatic" : false, + "name" : "name", + "type" : { + "string" : { + + } + } + } + ], + "swiftCallName" : "Greeter" + } + ], + "enums" : [ + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "red" + }, + { + "associatedValues" : [ + + ], + "name" : "green" + }, + { + "associatedValues" : [ + + ], + "name" : "blue" + } + ], + "documentation" : "A primary color channel.", + "emitStyle" : "const", + "name" : "Color", + "staticMethods" : [ + { + "abiName" : "bjs_Color_static_canonical", + "documentation" : "Returns the canonical name for a channel label.\n- Parameter label: The raw label.\n- Returns: The canonical channel name.", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "canonical", + "parameters" : [ + { + "label" : "label", + "name" : "label", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + }, + "staticContext" : { + "enumName" : { + "_0" : "Color" + } + } + } + ], + "staticProperties" : [ + { + "documentation" : "The default channel.", + "isReadonly" : true, + "isStatic" : true, + "name" : "fallback", + "staticContext" : { + "enumName" : { + "_0" : "Color" + } + }, + "type" : { + "string" : { + + } + } + } + ], + "swiftCallName" : "Color", + "tsFullPath" : "Color" + } + ], + "exposeToGlobal" : false, + "functions" : [ + { + "abiName" : "bjs_greet", + "documentation" : "Returns a greeting for a user.\n- Parameters:\n - name: The user's name.\n - greeting: The greeting word to use.\n- Returns: The composed greeting message.", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "greet", + "parameters" : [ + { + "label" : "name", + "name" : "name", + "type" : { + "string" : { + + } + } + }, + { + "defaultValue" : { + "string" : { + "_0" : "Hello" + } + }, + "label" : "greeting", + "name" : "greeting", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_add", + "documentation" : "Adds two numbers together.\n- Parameter a: The first addend.\n- Parameter b: The second addend.\n- Returns: The sum of the inputs.", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "add", + "parameters" : [ + { + "label" : "a", + "name" : "a", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "label" : "b", + "name" : "b", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "abiName" : "bjs_trimmed", + "documentation" : "Has blank doc lines around the summary; boundaries should be trimmed.", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "trimmed", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_hello", + "documentation" : "Says hello to the world.\n\nDemonstrates that block doc comments are supported too.", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "hello", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_parseInt", + "documentation" : "Parses an integer from text.\n- Parameter text: The text to parse.\n- Returns: The parsed integer.\n- Throws: A `JSException` when the text is not a valid integer.", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : true + }, + "name" : "parseInt", + "parameters" : [ + { + "label" : "text", + "name" : "text", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "abiName" : "bjs_MathUtils_double", + "documentation" : "Doubles a value, in a namespace.\n- Parameter value: The value to double.\n- Returns: Twice the input.", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "double", + "namespace" : [ + "MathUtils" + ], + "parameters" : [ + { + "label" : "value", + "name" : "value", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "abiName" : "bjs_terminator", + "documentation" : "Returns the JSDoc terminator *\/ embedded mid-sentence.", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "terminator", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + } + ], + "protocols" : [ + { + "documentation" : "Receives lifecycle callbacks.", + "methods" : [ + { + "abiName" : "bjs_Listener_onEvent", + "documentation" : "Called when an event fires.\n- Parameter id: The event identifier.", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "onEvent", + "parameters" : [ + { + "label" : "id", + "name" : "id", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "Listener", + "properties" : [ + { + "documentation" : "The listener's display name.", + "isReadonly" : true, + "name" : "name", + "type" : { + "string" : { + + } + } + } + ] + } + ], + "structs" : [ + { + "documentation" : "A 2D point in space.", + "methods" : [ + + ], + "name" : "Point", + "properties" : [ + { + "documentation" : "The horizontal position.", + "isReadonly" : true, + "isStatic" : false, + "name" : "x", + "type" : { + "double" : { + + } + } + }, + { + "documentation" : "The vertical position.", + "isReadonly" : true, + "isStatic" : false, + "name" : "y", + "type" : { + "double" : { + + } + } + } + ], + "swiftCallName" : "Point" + } + ] + }, + "moduleName" : "TestModule", + "usedExternalModules" : [ + + ] +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DocComments.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DocComments.swift new file mode 100644 index 000000000..eaed9e413 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DocComments.swift @@ -0,0 +1,317 @@ +struct AnyListener: Listener, _BridgedSwiftProtocolWrapper { + let jsObject: JSObject + + func onEvent(id: Int) -> Void { + let jsObjectValue = jsObject.bridgeJSLowerParameter() + let idValue = id.bridgeJSLowerParameter() + _extern_onEvent(jsObjectValue, idValue) + } + + var name: String { + get { + let jsObjectValue = jsObject.bridgeJSLowerParameter() + let ret = bjs_Listener_name_get(jsObjectValue) + return String.bridgeJSLiftReturn(ret) + } + } + + static func bridgeJSLiftParameter(_ value: Int32) -> Self { + return AnyListener(jsObject: JSObject(id: UInt32(bitPattern: value))) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_Listener_onEvent") +fileprivate func _extern_onEvent_extern(_ jsObject: Int32, _ id: Int32) -> Void +#else +fileprivate func _extern_onEvent_extern(_ jsObject: Int32, _ id: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _extern_onEvent(_ jsObject: Int32, _ id: Int32) -> Void { + return _extern_onEvent_extern(jsObject, id) +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_Listener_name_get") +fileprivate func bjs_Listener_name_get_extern(_ jsObject: Int32) -> Int32 +#else +fileprivate func bjs_Listener_name_get_extern(_ jsObject: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func bjs_Listener_name_get(_ jsObject: Int32) -> Int32 { + return bjs_Listener_name_get_extern(jsObject) +} + +extension Color: _BridgedSwiftCaseEnum { + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerParameter() -> Int32 { + return bridgeJSRawValue + } + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftReturn(_ value: Int32) -> Color { + return bridgeJSLiftParameter(value) + } + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftParameter(_ value: Int32) -> Color { + return Color(bridgeJSRawValue: value)! + } + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerReturn() -> Int32 { + return bridgeJSLowerParameter() + } + + @_spi(BridgeJS) @usableFromInline init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .red + case 1: + self = .green + case 2: + self = .blue + default: + return nil + } + } + + @_spi(BridgeJS) @usableFromInline var bridgeJSRawValue: Int32 { + switch self { + case .red: + return 0 + case .green: + return 1 + case .blue: + return 2 + } + } +} + +@_expose(wasm, "bjs_Color_static_canonical") +@_cdecl("bjs_Color_static_canonical") +public func _bjs_Color_static_canonical(_ labelBytes: Int32, _ labelLength: Int32) -> Void { + #if arch(wasm32) + let ret = Color.canonical(label: String.bridgeJSLiftParameter(labelBytes, labelLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Color_static_fallback_get") +@_cdecl("bjs_Color_static_fallback_get") +public func _bjs_Color_static_fallback_get() -> Void { + #if arch(wasm32) + let ret = Color.fallback + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension Point: _BridgedSwiftStruct { + @_spi(BridgeJS) @_transparent public static func bridgeJSStackPop() -> Point { + let y = Double.bridgeJSStackPop() + let x = Double.bridgeJSStackPop() + return Point(x: x, y: y) + } + + @_spi(BridgeJS) @_transparent public consuming func bridgeJSStackPush() { + self.x.bridgeJSStackPush() + self.y.bridgeJSStackPush() + } + + init(unsafelyCopying jsObject: JSObject) { + _bjs_struct_lower_Point(jsObject.bridgeJSLowerParameter()) + self = Self.bridgeJSStackPop() + } + + func toJSObject() -> JSObject { + let __bjs_self = self + __bjs_self.bridgeJSStackPush() + return JSObject(id: UInt32(bitPattern: _bjs_struct_lift_Point())) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_Point") +fileprivate func _bjs_struct_lower_Point_extern(_ objectId: Int32) -> Void +#else +fileprivate func _bjs_struct_lower_Point_extern(_ objectId: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_struct_lower_Point(_ objectId: Int32) -> Void { + return _bjs_struct_lower_Point_extern(objectId) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_lift_Point") +fileprivate func _bjs_struct_lift_Point_extern() -> Int32 +#else +fileprivate func _bjs_struct_lift_Point_extern() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_struct_lift_Point() -> Int32 { + return _bjs_struct_lift_Point_extern() +} + +@_expose(wasm, "bjs_greet") +@_cdecl("bjs_greet") +public func _bjs_greet(_ nameBytes: Int32, _ nameLength: Int32, _ greetingBytes: Int32, _ greetingLength: Int32) -> Void { + #if arch(wasm32) + let ret = greet(name: String.bridgeJSLiftParameter(nameBytes, nameLength), greeting: String.bridgeJSLiftParameter(greetingBytes, greetingLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_add") +@_cdecl("bjs_add") +public func _bjs_add(_ a: Int32, _ b: Int32) -> Int32 { + #if arch(wasm32) + let ret = add(a: Int.bridgeJSLiftParameter(a), b: Int.bridgeJSLiftParameter(b)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_trimmed") +@_cdecl("bjs_trimmed") +public func _bjs_trimmed() -> Void { + #if arch(wasm32) + trimmed() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_hello") +@_cdecl("bjs_hello") +public func _bjs_hello() -> Void { + #if arch(wasm32) + hello() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_parseInt") +@_cdecl("bjs_parseInt") +public func _bjs_parseInt(_ textBytes: Int32, _ textLength: Int32) -> Int32 { + #if arch(wasm32) + do { + let ret = try parseInt(text: String.bridgeJSLiftParameter(textBytes, textLength)) + return ret.bridgeJSLowerReturn() + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: error.description) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return 0 + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_MathUtils_double") +@_cdecl("bjs_MathUtils_double") +public func _bjs_MathUtils_double(_ value: Int32) -> Int32 { + #if arch(wasm32) + let ret = double(value: Int.bridgeJSLiftParameter(value)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_terminator") +@_cdecl("bjs_terminator") +public func _bjs_terminator() -> Void { + #if arch(wasm32) + let ret = terminator() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Greeter_init") +@_cdecl("bjs_Greeter_init") +public func _bjs_Greeter_init(_ nameBytes: Int32, _ nameLength: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = Greeter(name: String.bridgeJSLiftParameter(nameBytes, nameLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Greeter_greet") +@_cdecl("bjs_Greeter_greet") +public func _bjs_Greeter_greet(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = Greeter.bridgeJSLiftParameter(_self).greet() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Greeter_name_get") +@_cdecl("bjs_Greeter_name_get") +public func _bjs_Greeter_name_get(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = Greeter.bridgeJSLiftParameter(_self).name + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Greeter_name_set") +@_cdecl("bjs_Greeter_name_set") +public func _bjs_Greeter_name_set(_ _self: UnsafeMutableRawPointer, _ valueBytes: Int32, _ valueLength: Int32) -> Void { + #if arch(wasm32) + Greeter.bridgeJSLiftParameter(_self).name = String.bridgeJSLiftParameter(valueBytes, valueLength) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Greeter_deinit") +@_cdecl("bjs_Greeter_deinit") +public func _bjs_Greeter_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension Greeter: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_Greeter_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_Greeter_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_Greeter_wrap") +fileprivate func _bjs_Greeter_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_Greeter_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_Greeter_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_Greeter_wrap_extern(pointer) +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.Global.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.Global.json index 4b6b720f1..ef9e0b758 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.Global.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.Global.json @@ -21,9 +21,11 @@ } ] }, + "documentation" : "A greeter living in a namespace.", "methods" : [ { "abiName" : "bjs___Swift_Foundation_Greeter_greet", + "documentation" : "Produces a greeting for the configured name.\n- Returns: The greeting message.", "effects" : { "isAsync" : false, "isStatic" : false, @@ -262,6 +264,7 @@ }, { "abiName" : "bjs_MyModule_Utils_namespacedFunction", + "documentation" : "A namespaced free function.\n- Returns: A fixed namespaced string.", "effects" : { "isAsync" : false, "isStatic" : false, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.json index 3c07b7dcf..397d1123c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.json @@ -21,9 +21,11 @@ } ] }, + "documentation" : "A greeter living in a namespace.", "methods" : [ { "abiName" : "bjs___Swift_Foundation_Greeter_greet", + "documentation" : "Produces a greeting for the configured name.\n- Returns: The greeting message.", "effects" : { "isAsync" : false, "isStatic" : false, @@ -262,6 +264,7 @@ }, { "abiName" : "bjs_MyModule_Utils_namespacedFunction", + "documentation" : "A namespaced free function.\n- Returns: A fixed namespaced string.", "effects" : { "isAsync" : false, "isStatic" : false, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DocComments.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DocComments.d.ts new file mode 100644 index 000000000..359d719d1 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DocComments.d.ts @@ -0,0 +1,140 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +/** + * Receives lifecycle callbacks. + */ +export interface Listener { + /** + * Called when an event fires. + * @param id The event identifier. + */ + onEvent(id: number): void; + /** + * The listener's display name. + */ + readonly name: string; +} + +/** + * A primary color channel. + */ +export const ColorValues: { + readonly Red: 0; + readonly Green: 1; + readonly Blue: 2; +}; +export type ColorTag = typeof ColorValues[keyof typeof ColorValues]; + +/** + * A 2D point in space. + */ +export interface Point { + /** + * The horizontal position. + */ + x: number; + /** + * The vertical position. + */ + y: number; +} +export type ColorObject = typeof ColorValues & { + /** + * Returns the canonical name for a channel label. + * @param label The raw label. + * @returns The canonical channel name. + */ + canonical(label: string): string; + /** + * The default channel. + */ + readonly fallback: string; +}; + +/// Represents a Swift heap object like a class instance or an actor instance. +export interface SwiftHeapObject { + /// Release the heap object. + /// + /// Note: Calling this method will release the heap object and it will no longer be accessible. + release(): void; +} +/** + * A greeter that keeps the target name. + */ +export interface Greeter extends SwiftHeapObject { + /** + * Returns a greeting for the configured name. + * @returns The greeting message. + */ + greet(): string; + /** + * The configured name. + */ + name: string; +} +export type Exports = { + Greeter: { + /** + * Create a greeter. + * @param name The name to greet. + */ + new(name: string): Greeter; + } + /** + * Returns a greeting for a user. + * @param name The user's name. + * @param greeting The greeting word to use. (default: "Hello") + * @returns The composed greeting message. + */ + greet(name: string, greeting?: string): string; + /** + * Adds two numbers together. + * @param a The first addend. + * @param b The second addend. + * @returns The sum of the inputs. + */ + add(a: number, b: number): number; + /** + * Has blank doc lines around the summary; boundaries should be trimmed. + */ + trimmed(): void; + /** + * Says hello to the world. + * + * Demonstrates that block doc comments are supported too. + */ + hello(): void; + /** + * Parses an integer from text. + * @param text The text to parse. + * @returns The parsed integer. + * @throws A `JSException` when the text is not a valid integer. + */ + parseInt(text: string): number; + /** + * Returns the JSDoc terminator *\/ embedded mid-sentence. + */ + terminator(): string; + Color: ColorObject + MathUtils: { + /** + * Doubles a value, in a namespace. + * @param value The value to double. + * @returns Twice the input. + */ + double(value: number): number; + }, +} +export type Imports = { +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DocComments.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DocComments.js new file mode 100644 index 000000000..07e8673bf --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DocComments.js @@ -0,0 +1,421 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export const ColorValues = { + Red: 0, + Green: 1, + Blue: 2, +}; + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + let decodeString; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + let tmpRetOptionalBool; + let tmpRetOptionalInt; + let tmpRetOptionalFloat; + let tmpRetOptionalDouble; + let tmpRetOptionalHeapObject; + let strStack = []; + let i32Stack = []; + let i64Stack = []; + let f32Stack = []; + let f64Stack = []; + let ptrStack = []; + let taStack = []; + const enumHelpers = {}; + const structHelpers = {}; + + let _exports = null; + let bjs = null; + const __bjs_createPointHelpers = () => ({ + lower: (value) => { + f64Stack.push(value.x); + f64Stack.push(value.y); + }, + lift: () => { + const f64 = f64Stack.pop(); + const f641 = f64Stack.pop(); + return { x: f641, y: f64 }; + } + }); + + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + bjs = {}; + importObject["bjs"] = bjs; + bjs["swift_js_return_string"] = function(ptr, len) { + tmpRetString = decodeString(ptr, len); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + swift.memory.release(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr >>> 0); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(ptr, len) { + return swift.memory.retain(decodeString(ptr, len)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr >>> 0, len >>> 0); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + bjs["swift_js_push_i32"] = function(v) { + i32Stack.push(v | 0); + } + bjs["swift_js_push_f32"] = function(v) { + f32Stack.push(Math.fround(v)); + } + bjs["swift_js_push_f64"] = function(v) { + f64Stack.push(v); + } + bjs["swift_js_push_string"] = function(ptr, len) { + const value = decodeString(ptr, len); + strStack.push(value); + } + bjs["swift_js_pop_i32"] = function() { + return i32Stack.pop(); + } + bjs["swift_js_pop_f32"] = function() { + return f32Stack.pop(); + } + bjs["swift_js_pop_f64"] = function() { + return f64Stack.pop(); + } + bjs["swift_js_push_pointer"] = function(pointer) { + ptrStack.push(pointer); + } + bjs["swift_js_pop_pointer"] = function() { + return ptrStack.pop(); + } + bjs["swift_js_push_i64"] = function(v) { + i64Stack.push(v); + } + bjs["swift_js_pop_i64"] = function() { + return i64Stack.pop(); + } + const taCtors = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array]; + bjs["swift_js_push_typed_array"] = function(kind, ptr, count) { + const Ctor = taCtors[kind]; + const byteLen = count * Ctor.BYTES_PER_ELEMENT; + const copy = memory.buffer.slice(ptr, ptr + byteLen); + taStack.push(Array.from(new Ctor(copy))); + } + bjs["swift_js_struct_lower_Point"] = function(objectId) { + structHelpers.Point.lower(swift.memory.getObject(objectId)); + } + bjs["swift_js_struct_lift_Point"] = function() { + const value = structHelpers.Point.lift(); + return swift.memory.retain(value); + } + const __bjs_promiseSettlers = Symbol("JavaScriptKit.promiseSettlers"); + bjs["swift_js_make_promise"] = function() { + let resolve, reject; + const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); + promise[__bjs_promiseSettlers] = { resolve, reject }; + return swift.memory.retain(promise); + } + bjs["swift_js_return_optional_bool"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalBool = null; + } else { + tmpRetOptionalBool = value !== 0; + } + } + bjs["swift_js_return_optional_int"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalInt = null; + } else { + tmpRetOptionalInt = value | 0; + } + } + bjs["swift_js_return_optional_float"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalFloat = null; + } else { + tmpRetOptionalFloat = Math.fround(value); + } + } + bjs["swift_js_return_optional_double"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalDouble = null; + } else { + tmpRetOptionalDouble = value; + } + } + bjs["swift_js_return_optional_string"] = function(isSome, ptr, len) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = decodeString(ptr, len); + } + } + bjs["swift_js_return_optional_object"] = function(isSome, objectId) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = swift.memory.getObject(objectId); + } + } + bjs["swift_js_return_optional_heap_object"] = function(isSome, pointer) { + if (isSome === 0) { + tmpRetOptionalHeapObject = null; + } else { + tmpRetOptionalHeapObject = pointer; + } + } + bjs["swift_js_get_optional_int_presence"] = function() { + return tmpRetOptionalInt != null ? 1 : 0; + } + bjs["swift_js_get_optional_int_value"] = function() { + const value = tmpRetOptionalInt; + tmpRetOptionalInt = undefined; + return value; + } + bjs["swift_js_get_optional_string"] = function() { + const str = tmpRetString; + tmpRetString = undefined; + if (str == null) { + return -1; + } else { + const bytes = textEncoder.encode(str); + tmpRetBytes = bytes; + return bytes.length; + } + } + bjs["swift_js_get_optional_float_presence"] = function() { + return tmpRetOptionalFloat != null ? 1 : 0; + } + bjs["swift_js_get_optional_float_value"] = function() { + const value = tmpRetOptionalFloat; + tmpRetOptionalFloat = undefined; + return value; + } + bjs["swift_js_get_optional_double_presence"] = function() { + return tmpRetOptionalDouble != null ? 1 : 0; + } + bjs["swift_js_get_optional_double_value"] = function() { + const value = tmpRetOptionalDouble; + tmpRetOptionalDouble = undefined; + return value; + } + bjs["swift_js_get_optional_heap_object_pointer"] = function() { + const pointer = tmpRetOptionalHeapObject; + tmpRetOptionalHeapObject = undefined; + return pointer || 0; + } + bjs["swift_js_closure_unregister"] = function(funcRef) {} + // Wrapper functions for module: TestModule + if (!importObject["TestModule"]) { + importObject["TestModule"] = {}; + } + importObject["TestModule"]["bjs_Greeter_wrap"] = function(pointer) { + const obj = _exports['Greeter'].__construct(pointer); + return swift.memory.retain(obj); + }; + const TestModule = importObject["TestModule"] = importObject["TestModule"] || {}; + TestModule["bjs_Listener_name_get"] = function bjs_Listener_name_get(self) { + try { + let ret = swift.memory.getObject(self).name; + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } catch (error) { + setException(error); + } + } + TestModule["bjs_Listener_onEvent"] = function bjs_Listener_onEvent(self, id) { + try { + swift.memory.getObject(self).onEvent(id); + } catch (error) { + setException(error); + } + } + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + + decodeString = (ptr, len) => { const bytes = new Uint8Array(memory.buffer, ptr >>> 0, len >>> 0); return textDecoder.decode(bytes); } + + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + const swiftHeapObjectFinalizationRegistry = (typeof FinalizationRegistry === "undefined") ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry((state) => { + if (state.hasReleased) { + return; + } + state.hasReleased = true; + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + }); + + /// Represents a Swift heap object like a class instance or an actor instance. + class SwiftHeapObject { + static __wrap(pointer, deinit, prototype, identityCache) { + pointer = pointer >>> 0; + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); + } + + release() { + const state = this.__swiftHeapObjectState; + if (state.hasReleased) { + return; + } + state.hasReleased = true; + swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + } + } + class Greeter extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Greeter_deinit, Greeter.prototype, null); + } + + constructor(name) { + const nameBytes = textEncoder.encode(name); + const nameId = swift.memory.retain(nameBytes); + const ret = instance.exports.bjs_Greeter_init(nameId, nameBytes.length); + return Greeter.__construct(ret); + } + greet() { + instance.exports.bjs_Greeter_greet(this.pointer); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } + get name() { + instance.exports.bjs_Greeter_name_get(this.pointer); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } + set name(value) { + const valueBytes = textEncoder.encode(value); + const valueId = swift.memory.retain(valueBytes); + instance.exports.bjs_Greeter_name_set(this.pointer, valueId, valueBytes.length); + } + } + const PointHelpers = __bjs_createPointHelpers(); + structHelpers.Point = PointHelpers; + + const exports = { + Greeter, + greet: function bjs_greet(name, greeting = "Hello") { + const nameBytes = textEncoder.encode(name); + const nameId = swift.memory.retain(nameBytes); + const greetingBytes = textEncoder.encode(greeting); + const greetingId = swift.memory.retain(greetingBytes); + instance.exports.bjs_greet(nameId, nameBytes.length, greetingId, greetingBytes.length); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + }, + add: function bjs_add(a, b) { + const ret = instance.exports.bjs_add(a, b); + return ret; + }, + trimmed: function bjs_trimmed() { + instance.exports.bjs_trimmed(); + }, + hello: function bjs_hello() { + instance.exports.bjs_hello(); + }, + parseInt: function bjs_parseInt(text) { + const textBytes = textEncoder.encode(text); + const textId = swift.memory.retain(textBytes); + const ret = instance.exports.bjs_parseInt(textId, textBytes.length); + if (tmpRetException) { + const error = swift.memory.getObject(tmpRetException); + swift.memory.release(tmpRetException); + tmpRetException = undefined; + throw error; + } + return ret; + }, + terminator: function bjs_terminator() { + instance.exports.bjs_terminator(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + }, + Color: { + ...ColorValues, + canonical: function(label) { + const labelBytes = textEncoder.encode(label); + const labelId = swift.memory.retain(labelBytes); + instance.exports.bjs_Color_static_canonical(labelId, labelBytes.length); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + }, + get fallback() { + instance.exports.bjs_Color_static_fallback_get(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } + }, + MathUtils: { + double: function bjs_MathUtils_double(value) { + const ret = instance.exports.bjs_MathUtils_double(value); + return ret; + }, + }, + }; + _exports = exports; + return exports; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.d.ts index d9af0c8eb..ae792be4c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.d.ts @@ -17,6 +17,10 @@ declare global { } namespace MyModule { namespace Utils { + /** + * A namespaced free function. + * @returns A fixed namespaced string. + */ function namespacedFunction(): string; } } @@ -31,8 +35,15 @@ declare global { } namespace __Swift { namespace Foundation { + /** + * A greeter living in a namespace. + */ class Greeter { constructor(name: string); + /** + * Produces a greeting for the configured name. + * @returns The greeting message. + */ greet(): string; static makeDefault(): Greeter; static readonly defaultGreeting: string; @@ -53,7 +64,14 @@ export interface SwiftHeapObject { /// Note: Calling this method will release the heap object and it will no longer be accessible. release(): void; } +/** + * A greeter living in a namespace. + */ export interface Greeter extends SwiftHeapObject { + /** + * Produces a greeting for the configured name. + * @returns The greeting message. + */ greet(): string; } export interface Converter extends SwiftHeapObject { @@ -75,6 +93,10 @@ export type Exports = { }, MyModule: { Utils: { + /** + * A namespaced free function. + * @returns A fixed namespaced string. + */ namespacedFunction(): string; }, }, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.d.ts index 6b2d65cd8..4c02c18b3 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.d.ts @@ -11,7 +11,14 @@ export interface SwiftHeapObject { /// Note: Calling this method will release the heap object and it will no longer be accessible. release(): void; } +/** + * A greeter living in a namespace. + */ export interface Greeter extends SwiftHeapObject { + /** + * Produces a greeting for the configured name. + * @returns The greeting message. + */ greet(): string; } export interface Converter extends SwiftHeapObject { @@ -33,6 +40,10 @@ export type Exports = { }, MyModule: { Utils: { + /** + * A namespaced free function. + * @returns A fixed namespaced string. + */ namespacedFunction(): string; }, }, From 219a115e5a94f29fd3c88b8f8bad6a2744812a3d Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Tue, 23 Jun 2026 14:50:39 +0200 Subject: [PATCH 4/4] CI: Build examples in parallel on pull requests The `build-examples` job built all examples sequentially in a single job, taking ~57 minutes - each example is its own SwiftPM package that recompiles JavaScriptKit and swift-syntax from scratch, with no sharing between them. Split the job by event: - Pull requests fan out a matrix `build-examples` with one job per example, built in parallel. Wall-clock drops to that of the slowest single example (~10 min). Each example now reports as a separate `build-examples ()` check, so the required status checks need updating (see PR description). - `main` keeps the full release build of all examples plus the GitHub Pages deploy (`build-examples-deploy`), so published artifacts are unchanged. Examples are built in release to match the deploy path, keeping the only behavioral change the parallelism. Each matrix step runs `build.sh` from the example directory (via `working-directory`), mirroring Utilities/build-examples.sh. --- .github/workflows/test.yml | 39 +++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ce6ebe60..e19fed6f2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -155,7 +155,44 @@ jobs: exit 1 } + # Pull requests: compile every example in parallel just to catch breakage. + # One job per example avoids rebuilding JavaScriptKit + swift-syntax 6x in series, + # which collapses the wall-clock time from ~1h to that of the slowest single example. build-examples: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + example: + - ActorOnWebWorker + - Basic + - Embedded + - Multithreading + - OffscrenCanvas + - PlayBridgeJS + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/install-swift + with: + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2026-05-27-a/swift-DEVELOPMENT-SNAPSHOT-2026-05-27-a-ubuntu22.04.tar.gz + - uses: swiftwasm/setup-swiftwasm@v2 + id: setup-wasm32-unknown-wasip1 + with: { target: wasm32-unknown-wasip1 } + - uses: swiftwasm/setup-swiftwasm@v2 + id: setup-wasm32-unknown-wasip1-threads + with: { target: wasm32-unknown-wasip1-threads } + # build.sh resolves the package relative to the working directory, so run it + # from the example directory (mirroring Utilities/build-examples.sh's `cd`). + - run: ./build.sh release + working-directory: Examples/${{ matrix.example }} + env: + SWIFT_SDK_ID_wasm32_unknown_wasip1_threads: ${{ steps.setup-wasm32-unknown-wasip1-threads.outputs.swift-sdk-id }} + SWIFT_SDK_ID_wasm32_unknown_wasip1: ${{ steps.setup-wasm32-unknown-wasip1.outputs.swift-sdk-id }} + + # main: build all examples in release and publish them to GitHub Pages. + build-examples-deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -184,7 +221,7 @@ jobs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - needs: build-examples + needs: build-examples-deploy permissions: pages: write id-token: write