From df68ef786fe41ecab808594ede82b0e1cdf9f5d5 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 6 Feb 2026 12:08:17 +0900 Subject: [PATCH 1/3] Doc comments from .d.ts JSDoc now flow into generated Swift DocC output. - Added JSDoc parsing (description/@param/@returns) and DocC emission for callable/typed declarations, including classes/interfaces/enums, in `Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js`, with guards to only read param/return tags on callable nodes. - Added a documented fixture to cover the new behavior and updated the Vitest snapshot (`Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/Documentation.d.ts`, `Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap`). Tests: - `npm test -- -u` (Plugins/BridgeJS/Sources/TS2Swift/JavaScript) Next steps: optionally run `swift test --package-path ./Plugins/BridgeJS` to keep the Swift-side suite green. --- .../TS2Swift/JavaScript/src/processor.js | 230 +++++++++++++++--- .../test/__snapshots__/ts2swift.test.js.snap | 57 +++++ .../test/fixtures/Documentation.d.ts | 56 +++++ 3 files changed, 308 insertions(+), 35 deletions(-) create mode 100644 Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/Documentation.d.ts diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js index ef446af0..f326de22 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js @@ -157,10 +157,9 @@ export class TypeProcessor { this.visitEnumType(type, node); continue; } - const typeString = this.checker.typeToString(type); const members = type.getProperties(); if (members) { - this.visitStructuredType(typeString, members); + this.visitStructuredType(type, node, members); } } }); @@ -284,6 +283,7 @@ export class TypeProcessor { if (fromArg) args.push(fromArg); const annotation = this.renderMacroAnnotation("JSGetter", args); + this.emitDocComment(decl, { indent: "" }); this.swiftLines.push(`${annotation} var ${swiftVarName}: ${swiftType}`); this.swiftLines.push(""); } @@ -338,6 +338,7 @@ export class TypeProcessor { this.emittedEnumNames.add(enumName); const members = decl.members ?? []; + this.emitDocComment(decl, { indent: "" }); if (members.length === 0) { this.diagnosticEngine.print("warning", `Empty enum is not supported: ${enumName}`, diagnosticNode); this.swiftLines.push(`typealias ${this.renderIdentifier(enumName)} = String`); @@ -448,54 +449,194 @@ export class TypeProcessor { const signature = this.checker.getSignatureFromDeclaration(node); if (!signature) return; - const params = this.renderParameters(signature.getParameters(), node); + const parameters = signature.getParameters(); + const parameterNameMap = this.buildParameterNameMap(parameters); + const params = this.renderParameters(parameters, node); const returnType = this.visitType(signature.getReturnType(), node); const effects = this.renderEffects({ isAsync: false }); const swiftFuncName = this.renderIdentifier(swiftName); + this.emitDocComment(node, { parameterNameMap }); this.swiftLines.push(`${annotation} func ${swiftFuncName}(${params}) ${effects} -> ${returnType}`); this.swiftLines.push(""); } /** - * Get the full JSDoc text from a node - * @param {ts.Node} node - The node to get the JSDoc text from - * @returns {string | undefined} The full JSDoc text + * Convert a JSDoc comment node content to plain text. + * @param {string | ts.NodeArray | undefined} comment + * @returns {string} + * @private */ - getFullJSDocText(node) { - const docs = ts.getJSDocCommentsAndTags(node); - const parts = []; - for (const doc of docs) { - if (ts.isJSDoc(doc)) { - parts.push(doc.comment ?? ""); + renderJSDocText(comment) { + if (!comment) return ""; + if (typeof comment === "string") return comment; + let result = ""; + for (const part of comment) { + if (typeof part === "string") { + result += part; + continue; + } + // JSDocText/JSDocLink both have a `text` field + // https://github.com/microsoft/TypeScript/blob/main/src/compiler/types.ts + // @ts-ignore + if (typeof part.text === "string") { + // @ts-ignore + result += part.text; + continue; + } + if (typeof part.getText === "function") { + result += part.getText(); } } - if (parts.length === 0) return undefined; - return parts.join("\n"); + return result; } - /** @returns {string} */ - renderDefaultJSImportFromArgument() { - if (this.defaultImportFromGlobal) return "from: .global"; - return ""; + /** + * Split documentation text into lines suitable for DocC rendering. + * @param {string} text + * @returns {string[]} + * @private + */ + splitDocumentationText(text) { + if (!text) return []; + return text.split(/\r?\n/).map(line => line.trimEnd()); } /** - * Render constructor parameters - * @param {ts.ConstructorDeclaration} node - * @returns {string} Rendered parameters + * @param {string[]} lines + * @returns {boolean} * @private */ - renderConstructorParameters(node) { - const signature = this.checker.getSignatureFromDeclaration(node); - if (!signature) return ""; + hasMeaningfulLine(lines) { + return lines.some(line => line.trim().length > 0); + } + + /** + * Render Swift doc comments from a node's JSDoc, including parameter/return tags. + * @param {ts.Node} node + * @param {{ indent?: string, parameterNameMap?: Map }} options + * @private + */ + emitDocComment(node, options = {}) { + const indent = options.indent ?? ""; + const parameterNameMap = options.parameterNameMap ?? new Map(); + + /** @type {string[]} */ + const descriptionLines = []; + for (const doc of ts.getJSDocCommentsAndTags(node)) { + if (!ts.isJSDoc(doc)) continue; + const text = this.renderJSDocText(doc.comment); + if (text) { + descriptionLines.push(...this.splitDocumentationText(text)); + } + } + + /** @type {Array<{ name: string, lines: string[] }>} */ + const parameterDocs = []; + const supportsParameters = ( + ts.isFunctionLike(node) || + ts.isMethodSignature(node) || + ts.isCallSignatureDeclaration(node) || + ts.isConstructSignatureDeclaration(node) + ); + /** @type {ts.JSDocReturnTag | undefined} */ + let returnTag = undefined; + if (supportsParameters) { + for (const tag of ts.getJSDocTags(node)) { + if (ts.isJSDocParameterTag(tag)) { + const tsName = tag.name.getText(); + const name = parameterNameMap.get(tsName) ?? this.renderIdentifier(tsName); + const text = this.renderJSDocText(tag.comment); + const lines = this.splitDocumentationText(text); + parameterDocs.push({ name, lines }); + } else if (!returnTag && ts.isJSDocReturnTag(tag)) { + returnTag = tag; + } + } + } + + const returnLines = returnTag ? this.splitDocumentationText(this.renderJSDocText(returnTag.comment)) : []; + const hasDescription = this.hasMeaningfulLine(descriptionLines); + const hasParameters = parameterDocs.length > 0; + const hasReturns = returnTag !== undefined; + + if (!hasDescription && !hasParameters && !hasReturns) { + return; + } - return this.renderParameters(signature.getParameters(), node); + /** @type {string[]} */ + const docLines = []; + if (hasDescription) { + docLines.push(...descriptionLines); + } + + if (hasDescription && (hasParameters || hasReturns)) { + docLines.push(""); + } + + if (hasParameters) { + docLines.push("- Parameters:"); + for (const param of parameterDocs) { + const hasParamDescription = this.hasMeaningfulLine(param.lines); + const [firstParamLine, ...restParamLines] = param.lines; + if (hasParamDescription) { + docLines.push(` - ${param.name}: ${firstParamLine}`); + for (const line of restParamLines) { + docLines.push(` ${line}`); + } + } else { + docLines.push(` - ${param.name}:`); + } + } + } + + if (hasReturns) { + const hasReturnDescription = this.hasMeaningfulLine(returnLines); + const [firstReturnLine, ...restReturnLines] = returnLines; + if (hasReturnDescription) { + docLines.push(`- Returns: ${firstReturnLine}`); + for (const line of restReturnLines) { + docLines.push(` ${line}`); + } + } else { + docLines.push("- Returns:"); + } + } + + const prefix = `${indent}///`; + for (const line of docLines) { + if (line.length === 0) { + this.swiftLines.push(prefix); + } else { + this.swiftLines.push(`${prefix} ${line}`); + } + } } /** + * Build a map from TypeScript parameter names to rendered Swift identifiers. + * @param {ts.Symbol[]} parameters + * @returns {Map} + * @private + */ + buildParameterNameMap(parameters) { + const map = new Map(); + for (const parameter of parameters) { + map.set(parameter.name, this.renderIdentifier(parameter.name)); + } + return map; + } + + /** @returns {string} */ + renderDefaultJSImportFromArgument() { + if (this.defaultImportFromGlobal) return "from: .global"; + return ""; + } + + /** + * Visit a property declaration and extract metadata * @param {ts.PropertyDeclaration | ts.PropertySignature} node - * @returns {{ jsName: string, swiftName: string, type: string, isReadonly: boolean, documentation: string | undefined } | null} + * @returns {{ jsName: string, swiftName: string, type: string, isReadonly: boolean } | null} */ visitPropertyDecl(node) { if (!node.name) return null; @@ -515,8 +656,7 @@ export class TypeProcessor { const type = this.checker.getTypeAtLocation(node) const swiftType = this.visitType(type, node); const isReadonly = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) ?? false; - const documentation = this.getFullJSDocText(node); - return { jsName, swiftName, type: swiftType, isReadonly, documentation }; + return { jsName, swiftName, type: swiftType, isReadonly }; } /** @@ -551,6 +691,7 @@ export class TypeProcessor { if (fromArg) args.push(fromArg); const annotation = this.renderMacroAnnotation("JSClass", args); const className = this.renderIdentifier(swiftName); + this.emitDocComment(node, { indent: "" }); this.swiftLines.push(`${annotation} struct ${className} {`); // Process members in declaration order @@ -600,11 +741,15 @@ export class TypeProcessor { /** * Visit a structured type (interface) and render Swift code - * @param {string} name + * @param {ts.Type} type + * @param {ts.Node} diagnosticNode * @param {ts.Symbol[]} members * @private */ - visitStructuredType(name, members) { + visitStructuredType(type, diagnosticNode, members) { + const symbol = type.getSymbol() ?? type.aliasSymbol; + const name = symbol?.name ?? this.checker.typeToString(type); + if (!name) return; if (this.emittedStructuredTypeNames.has(name)) return; this.emittedStructuredTypeNames.add(name); @@ -615,17 +760,22 @@ export class TypeProcessor { if (jsNameArg) args.push(jsNameArg); const annotation = this.renderMacroAnnotation("JSClass", args); const typeName = this.renderIdentifier(swiftName); + const docNode = symbol?.getDeclarations()?.[0] ?? diagnosticNode; + if (docNode) { + this.emitDocComment(docNode, { indent: "" }); + } this.swiftLines.push(`${annotation} struct ${typeName} {`); // Collect all declarations with their positions to preserve order /** @type {Array<{ decl: ts.Node, symbol: ts.Symbol, position: number }>} */ const allDecls = []; - for (const symbol of members) { - for (const decl of symbol.getDeclarations() ?? []) { + const typeMembers = members ?? type.getProperties() ?? []; + for (const memberSymbol of typeMembers) { + for (const decl of memberSymbol.getDeclarations() ?? []) { const sourceFile = decl.getSourceFile(); const pos = sourceFile ? decl.getStart() : 0; - allDecls.push({ decl, symbol, position: pos }); + allDecls.push({ decl, symbol: memberSymbol, position: pos }); } } @@ -800,6 +950,7 @@ export class TypeProcessor { const getterAnnotation = this.renderMacroAnnotation("JSGetter", getterArgs); // Always render getter + this.emitDocComment(node, { indent: " " }); this.swiftLines.push(` ${getterAnnotation} var ${swiftName}: ${type}`); // Render setter if not readonly @@ -849,7 +1000,9 @@ export class TypeProcessor { const signature = this.checker.getSignatureFromDeclaration(node); if (!signature) return; - const params = this.renderParameters(signature.getParameters(), node); + const parameters = signature.getParameters(); + const parameterNameMap = this.buildParameterNameMap(parameters); + const params = this.renderParameters(parameters, node); const returnType = this.visitType(signature.getReturnType(), node); const effects = this.renderEffects({ isAsync: false }); const swiftMethodName = this.renderIdentifier(swiftName); @@ -858,6 +1011,7 @@ export class TypeProcessor { ) ?? false; const staticKeyword = isStatic ? "static " : ""; + this.emitDocComment(node, { indent: " ", parameterNameMap }); this.swiftLines.push(` ${annotation} ${staticKeyword}func ${swiftMethodName}(${params}) ${effects} -> ${returnType}`); } @@ -876,8 +1030,14 @@ export class TypeProcessor { * @private */ renderConstructor(node) { - const params = this.renderConstructorParameters(node); + const signature = this.checker.getSignatureFromDeclaration(node); + if (!signature) return; + + const parameters = signature.getParameters(); + const parameterNameMap = this.buildParameterNameMap(parameters); + const params = this.renderParameters(parameters, node); const effects = this.renderEffects({ isAsync: false }); + this.emitDocComment(node, { indent: " ", parameterNameMap }); this.swiftLines.push(` @JSFunction init(${params}) ${effects}`); } diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap index 6fdb79f7..2f82b71d 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap @@ -48,6 +48,63 @@ exports[`ts2swift > snapshots Swift output for Async.d.ts > Async 1`] = ` " `; +exports[`ts2swift > snapshots Swift output for Documentation.d.ts > Documentation 1`] = ` +"// 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\`. + +@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit + +/// Return a greeting for a user. +/// +/// - Parameters: +/// - name: The user's name. +/// - Returns: The greeting message. +@JSFunction func greet(_ name: String) throws(JSException) -> String + +@JSFunction func origin() throws(JSException) -> Point + +/// Represents a 2D point. +@JSClass struct Point { + /// The horizontal position. + @JSGetter var x: Double + @JSSetter func setX(_ value: Double) throws(JSException) + /// Translate the point. + /// + /// - Parameters: + /// - dx: Delta to apply on x. + /// - dy: Delta to apply on y. + /// - Returns: The moved point. + @JSFunction func translate(_ dx: Double, _ dy: Double) throws(JSException) -> Point +} + +/// Move a point by the given delta. +/// +/// - Parameters: +/// - point: The point to move. +/// - dx: Delta to apply on x. +/// - dy: Delta to apply on y. +@JSFunction func translatePoint(_ point: Point, _ dx: Double, _ dy: Double) throws(JSException) -> Point + +/// A greeter that keeps the target name. +@JSClass struct Greeter { + /// Create a greeter. + /// + /// - Parameters: + /// - name: Name to greet. + @JSFunction init(_ name: String) throws(JSException) + /// The configured name. + @JSGetter var targetName: String + /// Say hello. + /// + /// - Returns: Greeting message. + @JSFunction func greet() throws(JSException) -> String +} +" +`; + exports[`ts2swift > snapshots Swift output for Interface.d.ts > Interface 1`] = ` "// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, // DO NOT EDIT. diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/Documentation.d.ts b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/Documentation.d.ts new file mode 100644 index 00000000..90ba064b --- /dev/null +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/Documentation.d.ts @@ -0,0 +1,56 @@ +/** + * Return a greeting for a user. + * @param name The user's name. + * @returns The greeting message. + */ +export function greet(name: string): string; + +/** + * Represents a 2D point. + */ +export interface Point { + /** + * The horizontal position. + */ + x: number; + + /** + * Translate the point. + * @param dx Delta to apply on x. + * @param dy Delta to apply on y. + * @returns The moved point. + */ + translate(dx: number, dy: number): Point; +} + +export function origin(): Point; + +/** + * Move a point by the given delta. + * @param point The point to move. + * @param dx Delta to apply on x. + * @param dy Delta to apply on y. + */ +export function translatePoint(point: Point, dx: number, dy: number): Point; + +/** + * A greeter that keeps the target name. + */ +export class Greeter { + /** + * Create a greeter. + * @param name Name to greet. + */ + constructor(name: string); + + /** + * The configured name. + */ + readonly targetName: string; + + /** + * Say hello. + * @returns Greeting message. + */ + greet(): string; +} From b56d4653068da63da52ab452c019d2e20f84e6f4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 6 Feb 2026 12:13:12 +0900 Subject: [PATCH 2/3] Added richer WebIDL-derived doc coverage using the bridgejs-development skill: new fixtures model lib.dom-style comments (MDN reference blocks, @param text) and updated Vitest snapshots to show how those JSDoc comments render into DocC. Key files: `Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/WebIDLDOMDocs.d.ts`, `Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/DOMLike.d.ts`, and the refreshed snapshot in `Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap`. Tests run: `npm test -- -u` (TS2Swift JavaScript). --- .../test/__snapshots__/ts2swift.test.js.snap | 107 ++++++++++++++++++ .../JavaScript/test/fixtures/DOMLike.d.ts | 61 ++++++++++ .../test/fixtures/WebIDLDOMDocs.d.ts | 81 +++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/DOMLike.d.ts create mode 100644 Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/WebIDLDOMDocs.d.ts diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap index 2f82b71d..6ec5ec3d 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap @@ -48,6 +48,26 @@ exports[`ts2swift > snapshots Swift output for Async.d.ts > Async 1`] = ` " `; +exports[`ts2swift > snapshots Swift output for DOMLike.d.ts > DOMLike 1`] = ` +"// 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\`. + +@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit + +/// Schedules a function to be run after a delay. +/// +/// - Parameters: +/// - handler: A function to execute after the timer expires. +/// - timeout: The time, in milliseconds, the timer should wait before the handler is executed. +/// - args: Additional arguments to pass to the handler. +/// - Returns: A numeric identifier for the timer. +@JSFunction func setTimeout(_ handler: JSObject, _ timeout: JSUndefinedOr, _ args: [JSValue]) throws(JSException) -> Double +" +`; + exports[`ts2swift > snapshots Swift output for Documentation.d.ts > Documentation 1`] = ` "// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, // DO NOT EDIT. @@ -429,3 +449,90 @@ exports[`ts2swift > snapshots Swift output for VoidParameterVoidReturn.d.ts > Vo @JSFunction func check() throws(JSException) -> Void " `; + +exports[`ts2swift > snapshots Swift output for WebIDLDOMDocs.d.ts > WebIDLDOMDocs 1`] = ` +"// 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\`. + +@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit + +/// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document) +@JSGetter var document: Document + +/// A simple Document subset with WebIDL-derived comments. +@JSClass struct Document { + /// Creates an instance of the element for the specified tag. + /// + /// - Parameters: + /// - tagName: The name of an element. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/createElement) + @JSFunction func createElement(_ tagName: String, _ options: JSUndefinedOr) throws(JSException) -> Element + /// Returns an element with namespace namespace. Its namespace prefix will be everything before ":" (U+003E) in qualifiedName or null. Its local name will be everything after ":" (U+003E) in qualifiedName or qualifiedName. + /// + /// If localName does not match the Name production an "InvalidCharacterError" DOMException will be thrown. + /// + /// If one of the following conditions is true a "NamespaceError" DOMException will be thrown: + /// + /// localName does not match the QName production. + /// Namespace prefix is not null and namespace is the empty string. + /// Namespace prefix is "xml" and namespace is not the XML namespace. + /// qualifiedName or namespace prefix is "xmlns" and namespace is not the XMLNS namespace. + /// namespace is the XMLNS namespace and neither qualifiedName nor namespace prefix is "xmlns". + /// + /// When supplied, options's is can be used to create a customized built-in element. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/createElementNS) + @JSFunction func createElementNS(_ namespaceURI: Optional, _ qualifiedName: String, _ options: JSObject) throws(JSException) -> Element + /// Creates a TreeWalker object. + /// + /// - Parameters: + /// - root: The root element or node to start traversing on. + /// - whatToShow: The type of nodes or elements to appear in the node list + /// - filter: A custom NodeFilter function to use. For more information, see filter. Use null for no filter. + /// - Returns: The created TreeWalker. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/createTreeWalker) + @JSFunction func createTreeWalker(_ root: Node, _ whatToShow: JSUndefinedOr, _ filter: JSUndefinedOr) throws(JSException) -> TreeWalker +} + +@JSClass struct ElementCreationOptions { + @JSGetter var \`is\`: JSUndefinedOr + @JSSetter func setIs(_ value: JSUndefinedOr) throws(JSException) +} + +@JSClass struct Element { + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/textContent) + @JSGetter var textContent: Optional + @JSSetter func setTextContent(_ value: Optional) throws(JSException) + /// Returns a copy of node. If deep is true, the copy also includes the node's descendants. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/cloneNode) + @JSFunction func cloneNode(_ subtree: JSObject) throws(JSException) -> Node +} + +/// Minimal stubs to mirror lib.dom.d.ts documentation converted from WebIDL. +@JSClass struct Node { + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/textContent) + @JSGetter var textContent: Optional + @JSSetter func setTextContent(_ value: Optional) throws(JSException) + /// Returns a copy of node. If deep is true, the copy also includes the node's descendants. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/cloneNode) + @JSFunction func cloneNode(_ subtree: JSObject) throws(JSException) -> Node +} + +@JSClass struct TreeWalker { + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/TreeWalker/currentNode) + @JSGetter var currentNode: Node + @JSSetter func setCurrentNode(_ value: Node) throws(JSException) + /// Moves the currentNode to the next visible node in the document order. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/TreeWalker/nextNode) + @JSFunction func nextNode() throws(JSException) -> Optional +} +" +`; diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/DOMLike.d.ts b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/DOMLike.d.ts new file mode 100644 index 00000000..077bb2f0 --- /dev/null +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/DOMLike.d.ts @@ -0,0 +1,61 @@ +/** + * Represents a node in the DOM tree. + */ +export interface Node { + /** + * Returns the name of the node, depending on its type. + */ + readonly nodeName: string; +} + +/** + * A string tokenizer that supports adding and removing tokens. + * + * Mirrors DOMTokenList in lib.dom.d.ts. + */ +export interface DOMTokenList { + /** + * Adds the specified token(s) to the list. + * @param tokens A set of space-separated tokens to add to the list. + */ + add(...tokens: string[]): void; + + /** + * Returns true if the list contains a given token, otherwise false. + * @param token Token to locate in the list. + * @returns True if the token is present, otherwise false. + */ + contains(token: string): boolean; +} + +/** + * Represents an element in the document. + */ +export interface Element extends Node { + /** + * Returns a live DOMTokenList of class values. + */ + readonly classList: DOMTokenList; + + /** + * Returns the first element that is a descendant of node that matches selectors. + * @param selectors Selectors to match against. + * @returns The first matching element or null if there is no such element. + */ + querySelector(selectors: string): Element | null; + + /** + * Appends nodes or strings after the last child of this element. + * @param nodes Nodes or strings to append. + */ + append(...nodes: (Node | string)[]): void; +} + +/** + * Schedules a function to be run after a delay. + * @param handler A function to execute after the timer expires. + * @param timeout The time, in milliseconds, the timer should wait before the handler is executed. + * @param args Additional arguments to pass to the handler. + * @returns A numeric identifier for the timer. + */ +export function setTimeout(handler: (...args: any[]) => void, timeout?: number, ...args: any[]): number; diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/WebIDLDOMDocs.d.ts b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/WebIDLDOMDocs.d.ts new file mode 100644 index 00000000..ace4011a --- /dev/null +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/WebIDLDOMDocs.d.ts @@ -0,0 +1,81 @@ +/** + * Minimal stubs to mirror lib.dom.d.ts documentation converted from WebIDL. + */ + +interface Node { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/textContent) */ + textContent: string | null; + /** + * Returns a copy of node. If deep is true, the copy also includes the node's descendants. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/cloneNode) + */ + cloneNode(subtree?: boolean): Node; +} + +interface Element extends Node {} +type HTMLElement = Element; +type SVGElement = Element; +type MathMLElement = Element; +type DocumentFragment = Node; + +interface ElementCreationOptions { + is?: string; +} + +/** + * A simple Document subset with WebIDL-derived comments. + */ +interface Document { + /** + * Creates an instance of the element for the specified tag. + * @param tagName The name of an element. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/createElement) + */ + createElement(tagName: string, options?: ElementCreationOptions): HTMLElement; + + /** + * Returns an element with namespace namespace. Its namespace prefix will be everything before ":" (U+003E) in qualifiedName or null. Its local name will be everything after ":" (U+003E) in qualifiedName or qualifiedName. + * + * If localName does not match the Name production an "InvalidCharacterError" DOMException will be thrown. + * + * If one of the following conditions is true a "NamespaceError" DOMException will be thrown: + * + * localName does not match the QName production. + * Namespace prefix is not null and namespace is the empty string. + * Namespace prefix is "xml" and namespace is not the XML namespace. + * qualifiedName or namespace prefix is "xmlns" and namespace is not the XMLNS namespace. + * namespace is the XMLNS namespace and neither qualifiedName nor namespace prefix is "xmlns". + * + * When supplied, options's is can be used to create a customized built-in element. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/createElementNS) + */ + createElementNS(namespaceURI: string | null, qualifiedName: string, options?: string | ElementCreationOptions): Element; + + /** + * Creates a TreeWalker object. + * @param root The root element or node to start traversing on. + * @param whatToShow The type of nodes or elements to appear in the node list + * @param filter A custom NodeFilter function to use. For more information, see filter. Use null for no filter. + * @returns The created TreeWalker. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/createTreeWalker) + */ + createTreeWalker(root: Node, whatToShow?: number, filter?: (node: Node) => number | null): TreeWalker; +} + +interface TreeWalker { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/TreeWalker/currentNode) */ + currentNode: Node; + /** + * Moves the currentNode to the next visible node in the document order. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TreeWalker/nextNode) + */ + nextNode(): Node | null; +} + +/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document) */ +export const document: Document; From 3236fe9fe87d606dc8064a26ba8659eacbfde316 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 6 Feb 2026 12:29:26 +0900 Subject: [PATCH 3/3] Removed the DOM-like fixture and refreshed snapshots to drop its output. Changes: deleted `Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/DOMLike.d.ts` and updated `Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap`. Tests: `npm test -- -u` (TS2Swift JavaScript). --- .../test/__snapshots__/ts2swift.test.js.snap | 20 ------ .../JavaScript/test/fixtures/DOMLike.d.ts | 61 ------------------- 2 files changed, 81 deletions(-) delete mode 100644 Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/DOMLike.d.ts diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap index 6ec5ec3d..0ef4daf9 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap @@ -48,26 +48,6 @@ exports[`ts2swift > snapshots Swift output for Async.d.ts > Async 1`] = ` " `; -exports[`ts2swift > snapshots Swift output for DOMLike.d.ts > DOMLike 1`] = ` -"// 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\`. - -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit - -/// Schedules a function to be run after a delay. -/// -/// - Parameters: -/// - handler: A function to execute after the timer expires. -/// - timeout: The time, in milliseconds, the timer should wait before the handler is executed. -/// - args: Additional arguments to pass to the handler. -/// - Returns: A numeric identifier for the timer. -@JSFunction func setTimeout(_ handler: JSObject, _ timeout: JSUndefinedOr, _ args: [JSValue]) throws(JSException) -> Double -" -`; - exports[`ts2swift > snapshots Swift output for Documentation.d.ts > Documentation 1`] = ` "// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, // DO NOT EDIT. diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/DOMLike.d.ts b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/DOMLike.d.ts deleted file mode 100644 index 077bb2f0..00000000 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/DOMLike.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Represents a node in the DOM tree. - */ -export interface Node { - /** - * Returns the name of the node, depending on its type. - */ - readonly nodeName: string; -} - -/** - * A string tokenizer that supports adding and removing tokens. - * - * Mirrors DOMTokenList in lib.dom.d.ts. - */ -export interface DOMTokenList { - /** - * Adds the specified token(s) to the list. - * @param tokens A set of space-separated tokens to add to the list. - */ - add(...tokens: string[]): void; - - /** - * Returns true if the list contains a given token, otherwise false. - * @param token Token to locate in the list. - * @returns True if the token is present, otherwise false. - */ - contains(token: string): boolean; -} - -/** - * Represents an element in the document. - */ -export interface Element extends Node { - /** - * Returns a live DOMTokenList of class values. - */ - readonly classList: DOMTokenList; - - /** - * Returns the first element that is a descendant of node that matches selectors. - * @param selectors Selectors to match against. - * @returns The first matching element or null if there is no such element. - */ - querySelector(selectors: string): Element | null; - - /** - * Appends nodes or strings after the last child of this element. - * @param nodes Nodes or strings to append. - */ - append(...nodes: (Node | string)[]): void; -} - -/** - * Schedules a function to be run after a delay. - * @param handler A function to execute after the timer expires. - * @param timeout The time, in milliseconds, the timer should wait before the handler is executed. - * @param args Additional arguments to pass to the handler. - * @returns A numeric identifier for the timer. - */ -export function setTimeout(handler: (...args: any[]) => void, timeout?: number, ...args: any[]): number;