diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 5a7245aa..478ba15d 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -12,6 +12,7 @@ struct BridgeJSLink { var exportedSkeletons: [ExportedSkeleton] = [] var importedSkeletons: [ImportedModuleSkeleton] = [] let sharedMemory: Bool + private let namespaceBuilder = NamespaceBuilder() init( exportedSkeletons: [ExportedSkeleton] = [], @@ -65,14 +66,11 @@ struct BridgeJSLink { } """ - private struct LinkData { + fileprivate struct LinkData { var exportsLines: [String] = [] var classLines: [String] = [] var dtsExportLines: [String] = [] var dtsClassLines: [String] = [] - var namespacedFunctions: [ExportedFunction] = [] - var namespacedClasses: [ExportedClass] = [] - var namespacedEnums: [ExportedEnum] = [] var topLevelEnumLines: [String] = [] var topLevelDtsEnumLines: [String] = [] var importObjectBuilders: [ImportObjectBuilder] = [] @@ -102,13 +100,13 @@ struct BridgeJSLink { for klass in skeleton.classes { let (jsType, dtsType, dtsExportEntry) = try renderExportedClass(klass) data.classLines.append(contentsOf: jsType) - data.exportsLines.append("\(klass.name),") - data.dtsExportLines.append(contentsOf: dtsExportEntry) - data.dtsClassLines.append(contentsOf: dtsType) - if klass.namespace != nil { - data.namespacedClasses.append(klass) + if klass.namespace == nil { + data.exportsLines.append("\(klass.name),") + data.dtsExportLines.append(contentsOf: dtsExportEntry) } + + data.dtsClassLines.append(contentsOf: dtsType) } // Process enums @@ -126,10 +124,6 @@ struct BridgeJSLink { } data.topLevelEnumLines.append(contentsOf: exportedJsEnum) data.topLevelDtsEnumLines.append(contentsOf: dtsEnum) - - if enumDefinition.namespace != nil { - data.namespacedEnums.append(enumDefinition) - } case .associatedValue: var exportedJsEnum = jsEnum if !exportedJsEnum.isEmpty && exportedJsEnum[0].hasPrefix("const ") { @@ -137,61 +131,29 @@ struct BridgeJSLink { } data.topLevelEnumLines.append(contentsOf: exportedJsEnum) data.topLevelDtsEnumLines.append(contentsOf: dtsEnum) - if enumDefinition.namespace != nil { - data.namespacedEnums.append(enumDefinition) - } } } } // Process functions for function in skeleton.functions { - var (js, dts) = try renderExportedFunction(function: function) - - if function.effects.isStatic, - case .namespaceEnum = function.staticContext - { - data.namespacedFunctions.append(function) - } else if function.namespace != nil { - data.namespacedFunctions.append(function) - } - - js[0] = "\(function.name): " + js[0] - js[js.count - 1] += "," - data.exportsLines.append(contentsOf: js) - data.dtsExportLines.append(contentsOf: dts) - } - - for enumDefinition in skeleton.enums where enumDefinition.enumType == .namespace { - for function in enumDefinition.staticMethods { - // Create namespace function with full namespace path (parent namespace + enum name) - var functionWithNamespace = function - let fullNamespace = (enumDefinition.namespace ?? []) + [enumDefinition.name] - functionWithNamespace.namespace = fullNamespace - data.namespacedFunctions.append(functionWithNamespace) - - var (js, dts) = try renderExportedFunction(function: functionWithNamespace) - js[0] = "\(functionWithNamespace.name): " + js[0] + if function.namespace == nil { + var (js, dts) = try renderExportedFunction(function: function) + js[0] = "\(function.name): " + js[0] js[js.count - 1] += "," data.exportsLines.append(contentsOf: js) data.dtsExportLines.append(contentsOf: dts) } - - for property in enumDefinition.staticProperties { - let fullNamespace = (enumDefinition.namespace ?? []) + [enumDefinition.name] - let namespacePath = fullNamespace.joined(separator: ".") - let (propJs, _) = try renderNamespaceStaticProperty( - property: property, - namespacePath: namespacePath - ) - data.enumStaticAssignments.append(contentsOf: propJs) - } } for enumDefinition in skeleton.enums where enumDefinition.enumType != .namespace && enumDefinition.emitStyle != .tsEnum { + if enumDefinition.namespace != nil { + continue + } + let enumExportPrinter = CodeFragmentPrinter() - let enumValuesName = "\(enumDefinition.name)Values" + let enumValuesName = enumDefinition.valuesName for function in enumDefinition.staticMethods { let thunkBuilder = ExportedThunkBuilder(effects: function.effects) @@ -281,7 +243,7 @@ struct BridgeJSLink { exportsPrinter.write("\(enumDefinition.name): \(enumValuesName),") } - dtsExportsPrinter.write("\(enumDefinition.name): \(enumDefinition.name)Object") + dtsExportsPrinter.write("\(enumDefinition.name): \(enumDefinition.objectTypeName)") data.exportsLines.append(contentsOf: exportsPrinter.lines) data.dtsExportLines.append(contentsOf: dtsExportsPrinter.lines) @@ -927,8 +889,8 @@ struct BridgeJSLink { for skeleton in exportedSkeletons { for enumDefinition in skeleton.enums where enumDefinition.enumType != .namespace && enumDefinition.emitStyle != .tsEnum { - let enumValuesName = "\(enumDefinition.name)Values" - let enumObjectName = "\(enumDefinition.name)Object" + let enumValuesName = enumDefinition.valuesName + let enumObjectName = enumDefinition.objectTypeName if !enumDefinition.staticMethods.isEmpty || !enumDefinition.staticProperties.isEmpty { printer.write("export type \(enumObjectName) = typeof \(enumValuesName) & {") @@ -952,7 +914,6 @@ struct BridgeJSLink { } // Type definitions section (namespace declarations, class definitions, imported types) - let namespaceBuilder = NamespaceBuilder() let namespaceDeclarationsLines = namespaceBuilder.namespaceDeclarations( exportedSkeletons: exportedSkeletons, renderTSSignatureCallback: { parameters, returnType, effects in @@ -969,7 +930,30 @@ struct BridgeJSLink { // Exports type printer.write("export type Exports = {") printer.indent { + // Add non-namespaced items printer.write(lines: data.dtsExportLines) + + // Add hierarchical namespaced items + let hierarchicalLines = namespaceBuilder.buildHierarchicalExportsType( + exportedSkeletons: exportedSkeletons, + renderClassEntry: { klass in + let printer = CodeFragmentPrinter() + printer.write("\(klass.name): {") + printer.indent { + if let constructor = klass.constructor { + printer.write( + "new\(self.renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name), effects: constructor.effects));" + ) + } + } + printer.write("}") + return printer.lines + }, + renderFunctionSignature: { function in + "\(function.name)\(self.renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" + } + ) + printer.write(lines: hierarchicalLines) } printer.write("}") @@ -999,7 +983,7 @@ struct BridgeJSLink { } /// Generates JavaScript output using CodeFragmentPrinter for better maintainability - private func generateJavaScript(data: LinkData) -> String { + private func generateJavaScript(data: LinkData) throws -> String { let header = """ // NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, // DO NOT EDIT. @@ -1013,21 +997,11 @@ struct BridgeJSLink { // Top-level enums section printer.write(lines: data.topLevelEnumLines) - // Namespace assignments section - let topLevelNamespaceCode = generateNamespaceInitializationCode( - namespacePaths: Set(data.namespacedEnums.compactMap { $0.namespace }) + let topLevelNamespaceCode = namespaceBuilder.buildTopLevelNamespaceInitialization( + exportedSkeletons: exportedSkeletons ) printer.write(lines: topLevelNamespaceCode) - // Add enum assignments to global namespace - for enumDef in data.namespacedEnums { - if enumDef.enumType != .namespace { - let namespacePath = enumDef.namespace?.joined(separator: ".") ?? "" - let enumValuesName = enumDef.emitStyle == .tsEnum ? enumDef.name : "\(enumDef.name)Values" - printer.write("globalThis.\(namespacePath).\(enumValuesName) = \(enumValuesName);") - } - } - // Main function declaration printer.write("export async function createInstantiator(options, \(JSGlueVariableScope.reservedSwift)) {") @@ -1076,33 +1050,25 @@ struct BridgeJSLink { } // createExports method - printer.indent { + try printer.indent { printer.write(lines: [ "/** @param {WebAssembly.Instance} instance */", "createExports: (instance) => {", ]) - printer.indent { + try printer.indent { printer.write("const js = \(JSGlueVariableScope.reservedSwift).memory.heap;") - // Exports / return section - let hasNamespacedItems = !data.namespacedFunctions.isEmpty || !data.namespacedClasses.isEmpty - let hasNamespacedEnums = !data.namespacedEnums.isEmpty - let hasAnyNamespacedItems = hasNamespacedItems || hasNamespacedEnums - printer.write(lines: data.classLines) - // Initialize all namespaces before property assignments - if hasAnyNamespacedItems { - let allNamespacePaths = collectAllNamespacePaths(data: data) - let namespaceInitializationCode = generateNamespaceInitializationCode( - namespacePaths: allNamespacePaths - ) - printer.write(lines: namespaceInitializationCode) - } + let namespaceInitCode = namespaceBuilder.buildNamespaceInitialization( + exportedSkeletons: exportedSkeletons + ) + printer.write(lines: namespaceInitCode) - let propertyAssignments = generateNamespacePropertyAssignments( + let propertyAssignments = try generateNamespacePropertyAssignments( data: data, - hasAnyNamespacedItems: hasAnyNamespacedItems + exportedSkeletons: exportedSkeletons, + namespaceBuilder: namespaceBuilder ) printer.write(lines: propertyAssignments) } @@ -1117,7 +1083,7 @@ struct BridgeJSLink { func link() throws -> (outputJs: String, outputDts: String) { let data = try collectLinkData() - let outputJs = generateJavaScript(data: data) + let outputJs = try generateJavaScript(data: data) let outputDts = generateTypeScript(data: data) return (outputJs, outputDts) } @@ -1127,12 +1093,10 @@ struct BridgeJSLink { for skeleton in exportedSkeletons { for enumDef in skeleton.enums where enumDef.enumType == .associatedValue { - let base = enumDef.name - let baseValues = "\(base)Values" printer.write( - "const \(base)Helpers = __bjs_create\(baseValues)Helpers()(\(JSGlueVariableScope.reservedTmpParamInts), \(JSGlueVariableScope.reservedTmpParamF32s), \(JSGlueVariableScope.reservedTmpParamF64s), \(JSGlueVariableScope.reservedTextEncoder), \(JSGlueVariableScope.reservedSwift));" + "const \(enumDef.name)Helpers = __bjs_create\(enumDef.valuesName)Helpers()(\(JSGlueVariableScope.reservedTmpParamInts), \(JSGlueVariableScope.reservedTmpParamF32s), \(JSGlueVariableScope.reservedTmpParamF64s), \(JSGlueVariableScope.reservedTextEncoder), \(JSGlueVariableScope.reservedSwift));" ) - printer.write("enumHelpers.\(base) = \(base)Helpers;") + printer.write("enumHelpers.\(enumDef.name) = \(enumDef.name)Helpers;") printer.nextLine() } } @@ -1173,82 +1137,33 @@ struct BridgeJSLink { return wrapperLines } - /// Collects all unique namespace paths from functions, classes, enums, and static properties - private func collectAllNamespacePaths(data: LinkData) -> Set<[String]> { - let functionNamespacePaths: Set<[String]> = Set( - data.namespacedFunctions.compactMap { $0.namespace } - ) - let classNamespacePaths: Set<[String]> = Set( - data.namespacedClasses.compactMap { $0.namespace } - ) - let allRegularNamespacePaths = functionNamespacePaths.union(classNamespacePaths) - - let enumNamespacePaths: Set<[String]> = Set( - data.namespacedEnums.compactMap { $0.namespace } - ) - var staticPropertyNamespacePaths: Set<[String]> = Set() - for skeleton in exportedSkeletons { - for enumDefinition in skeleton.enums { - for property in enumDefinition.staticProperties { - if let namespace = property.namespace, !namespace.isEmpty { - staticPropertyNamespacePaths.insert(namespace) - } - } - } - } - - return allRegularNamespacePaths.union(enumNamespacePaths).union(staticPropertyNamespacePaths) - } - - /// Generates JavaScript code lines for initializing namespace objects on globalThis - private func generateNamespaceInitializationCode(namespacePaths: Set<[String]>) -> [String] { - let printer = CodeFragmentPrinter() - var allUniqueNamespaces: [String] = [] - var seen = Set() - - namespacePaths.forEach { namespacePath in - namespacePath.enumerated().forEach { (index, _) in - let path = namespacePath[0...index].joined(separator: ".") - if seen.insert(path).inserted { - allUniqueNamespaces.append(path) - } - } - } - - allUniqueNamespaces.sorted().forEach { namespace in - printer.write("if (typeof globalThis.\(namespace) === 'undefined') {") - printer.indent { - printer.write("globalThis.\(namespace) = {};") - } - printer.write("}") - } - - return printer.lines - } - - /// Generates JavaScript code for assigning namespaced items to globalThis private func generateNamespacePropertyAssignments( data: LinkData, - hasAnyNamespacedItems: Bool - ) -> [String] { + exportedSkeletons: [ExportedSkeleton], + namespaceBuilder: NamespaceBuilder + ) throws -> [String] { let printer = CodeFragmentPrinter() printer.write(lines: data.enumStaticAssignments) + printer.write("const exports = {") - printer.indent() - printer.write(lines: data.exportsLines.map { "\($0)" }) - printer.unindent() + try printer.indent { + printer.write(lines: data.exportsLines.map { "\($0)" }) + + let hierarchicalLines = try namespaceBuilder.buildHierarchicalExportsObject( + exportedSkeletons: exportedSkeletons, + renderFunctionImpl: { function in + let (js, _) = try self.renderExportedFunction(function: function) + return js + } + ) + printer.write(lines: hierarchicalLines) + } printer.write("};") printer.write("_exports = exports;") - if hasAnyNamespacedItems { - data.namespacedClasses.forEach { klass in - let namespacePath: String = klass.namespace?.joined(separator: ".") ?? "" - printer.write("globalThis.\(namespacePath).\(klass.name) = exports.\(klass.name);") - } - data.namespacedFunctions.forEach { function in - let namespacePath: String = function.namespace?.joined(separator: ".") ?? "" - printer.write("globalThis.\(namespacePath).\(function.name) = exports.\(function.name);") - } - } + + let globalThisLines = namespaceBuilder.buildGlobalThisAssignments(exportedSkeletons: exportedSkeletons) + printer.write(lines: globalThisLines) + printer.write("return exports;") return printer.lines @@ -1453,7 +1368,7 @@ struct BridgeJSLink { return "null" case .enumCase(let enumName, let caseName): let simpleName = enumName.components(separatedBy: ".").last ?? enumName - let jsEnumName = format == .javascript ? "\(simpleName)Values" : simpleName + let jsEnumName = format == .javascript ? "\(simpleName)\(ExportedEnum.valuesSuffix)" : simpleName return "\(jsEnumName).\(caseName.capitalizedFirstLetter)" case .object(let className): return "new \(className)()" @@ -1497,7 +1412,7 @@ struct BridgeJSLink { let scope = JSGlueVariableScope() let cleanup = CodeFragmentPrinter() let printer = CodeFragmentPrinter() - let enumValuesName = enumDefinition.emitStyle == .tsEnum ? enumDefinition.name : "\(enumDefinition.name)Values" + let enumValuesName = enumDefinition.valuesName switch enumDefinition.enumType { case .simple: @@ -1529,7 +1444,7 @@ struct BridgeJSLink { private func generateDeclarations(enumDefinition: ExportedEnum) -> [String] { let printer = CodeFragmentPrinter() - let enumValuesName = enumDefinition.emitStyle == .tsEnum ? enumDefinition.name : "\(enumDefinition.name)Values" + let enumValuesName = enumDefinition.valuesName switch enumDefinition.emitStyle { case .tsEnum: @@ -1848,80 +1763,6 @@ extension BridgeJSLink { return (jsLines, dtsLines) } - /// Renders a namespace static property as getter/setter assignments on the namespace object - private func renderNamespaceStaticProperty( - property: ExportedProperty, - namespacePath: String - ) throws -> (js: [String], dts: [String]) { - var jsLines: [String] = [] - - // Generate getter assignment - let getterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false)) - let getterReturnExpr = try getterThunkBuilder.call( - abiName: property.getterAbiName(), - returnType: property.type - ) - - let getterLines = getterThunkBuilder.renderFunction( - name: property.name, - parameters: [], - returnExpr: getterReturnExpr, - declarationPrefixKeyword: nil - ) - - // Build Object.defineProperty call for namespace - var definePropertyLines: [String] = [] - definePropertyLines.append( - "Object.defineProperty(globalThis.\(namespacePath), '\(property.name)', { get: function() {" - ) - - // Add getter body (skip function declaration and closing brace) - if getterLines.count > 2 { - let bodyLines = Array(getterLines[1.. 2 { - let bodyLines = Array(setterLines[1.. (js: [String], dtsType: [String], dtsExportEntry: [String]) { @@ -2320,11 +2161,129 @@ extension BridgeJSLink { } struct NamespaceBuilder { + func collectAllNamespacePaths(exportedSkeletons: [ExportedSkeleton]) -> Set<[String]> { + return Set( + exportedSkeletons.flatMap { skeleton in + let itemNamespaces = + (skeleton.functions.compactMap(\.namespace) + skeleton.classes.compactMap(\.namespace) + + skeleton.enums.filter { $0.namespace != nil && $0.enumType != .namespace } + .compactMap(\.namespace)) + + let namespaceEnumPaths = skeleton.enums + .filter { $0.enumType == .namespace } + .filter { !$0.staticProperties.isEmpty || !$0.staticMethods.isEmpty } + .map { ($0.namespace ?? []) + [$0.name] } + + return itemNamespaces + namespaceEnumPaths + } + ) + } + + func buildNamespaceInitialization(exportedSkeletons: [ExportedSkeleton]) -> [String] { + let allNamespacePaths = collectAllNamespacePaths(exportedSkeletons: exportedSkeletons) + return generateNamespaceInitializationCode(namespacePaths: allNamespacePaths) + } + + func buildTopLevelNamespaceInitialization(exportedSkeletons: [ExportedSkeleton]) -> [String] { + var namespacedEnumPaths: Set<[String]> = [] + for skeleton in exportedSkeletons { + for enumDef in skeleton.enums where enumDef.namespace != nil && enumDef.enumType != .namespace { + namespacedEnumPaths.insert(enumDef.namespace!) + } + } + + let initCode = generateNamespaceInitializationCode(namespacePaths: namespacedEnumPaths) + + let printer = CodeFragmentPrinter() + printer.write(lines: initCode) + + for skeleton in exportedSkeletons { + for enumDef in skeleton.enums where enumDef.namespace != nil && enumDef.enumType != .namespace { + let namespacePath = enumDef.namespace!.joined(separator: ".") + printer.write("globalThis.\(namespacePath).\(enumDef.valuesName) = \(enumDef.valuesName);") + } + } + + return printer.lines + } + + func buildGlobalThisAssignments(exportedSkeletons: [ExportedSkeleton]) -> [String] { + let printer = CodeFragmentPrinter() + + for skeleton in exportedSkeletons { + for klass in skeleton.classes where klass.namespace != nil { + let namespacePath = klass.namespace!.joined(separator: ".") + printer.write("globalThis.\(namespacePath).\(klass.name) = exports.\(namespacePath).\(klass.name);") + } + for function in skeleton.functions where function.namespace != nil { + let namespacePath = function.namespace!.joined(separator: ".") + printer.write( + "globalThis.\(namespacePath).\(function.name) = exports.\(namespacePath).\(function.name);" + ) + } + for enumDef in skeleton.enums where enumDef.enumType == .namespace { + for function in enumDef.staticMethods { + let fullNamespace = (enumDef.namespace ?? []) + [enumDef.name] + let namespacePath = fullNamespace.joined(separator: ".") + printer.write( + "globalThis.\(namespacePath).\(function.name) = exports.\(namespacePath).\(function.name);" + ) + } + for property in enumDef.staticProperties { + let fullNamespace = (enumDef.namespace ?? []) + [enumDef.name] + let namespacePath = fullNamespace.joined(separator: ".") + let exportsPath = "exports.\(namespacePath)" + + printer.write("Object.defineProperty(globalThis.\(namespacePath), '\(property.name)', {") + printer.indent { + printer.write("get: () => \(exportsPath).\(property.name),") + if !property.isReadonly { + printer.write("set: (value) => { \(exportsPath).\(property.name) = value; }") + } + } + printer.write("});") + } + } + } + + return printer.lines + } + + private func generateNamespaceInitializationCode(namespacePaths: Set<[String]>) -> [String] { + let printer = CodeFragmentPrinter() + var allUniqueNamespaces: [String] = [] + var seen = Set() + + namespacePaths.forEach { namespacePath in + namespacePath.enumerated().forEach { (index, _) in + let path = namespacePath[0...index].joined(separator: ".") + if seen.insert(path).inserted { + allUniqueNamespaces.append(path) + } + } + } + + allUniqueNamespaces.sorted().forEach { namespace in + printer.write("if (typeof globalThis.\(namespace) === 'undefined') {") + printer.indent { + printer.write("globalThis.\(namespace) = {};") + } + printer.write("}") + } + + return printer.lines + } + private struct NamespaceContent { var functions: [ExportedFunction] = [] var classes: [ExportedClass] = [] var enums: [ExportedEnum] = [] var staticProperties: [ExportedProperty] = [] + var functionJsLines: [(name: String, lines: [String])] = [] + var functionDtsLines: [(name: String, lines: [String])] = [] + var classDtsLines: [(name: String, lines: [String])] = [] + var enumDtsLines: [(name: String, line: String)] = [] + var propertyJsLines: [String] = [] } private final class NamespaceNode { @@ -2346,6 +2305,259 @@ extension BridgeJSLink { } } + private func buildExportsTree( + rootNode: NamespaceNode, + exportedSkeletons: [ExportedSkeleton] + ) { + for skeleton in exportedSkeletons { + for function in skeleton.functions where function.namespace != nil { + var currentNode = rootNode + for part in function.namespace! { + currentNode = currentNode.addChild(part) + } + currentNode.content.functions.append(function) + } + + for klass in skeleton.classes where klass.namespace != nil { + var currentNode = rootNode + for part in klass.namespace! { + currentNode = currentNode.addChild(part) + } + currentNode.content.classes.append(klass) + } + + for enumDef in skeleton.enums where enumDef.namespace != nil && enumDef.enumType != .namespace { + var currentNode = rootNode + for part in enumDef.namespace! { + currentNode = currentNode.addChild(part) + } + currentNode.content.enums.append(enumDef) + } + + for enumDef in skeleton.enums where enumDef.enumType == .namespace { + for property in enumDef.staticProperties { + let fullNamespace = (enumDef.namespace ?? []) + [enumDef.name] + var currentNode = rootNode + for part in fullNamespace { + currentNode = currentNode.addChild(part) + } + currentNode.content.staticProperties.append(property) + } + for function in enumDef.staticMethods { + let fullNamespace = (enumDef.namespace ?? []) + [enumDef.name] + var currentNode = rootNode + for part in fullNamespace { + currentNode = currentNode.addChild(part) + } + currentNode.content.functions.append(function) + } + } + } + } + + fileprivate func buildHierarchicalExportsType( + exportedSkeletons: [ExportedSkeleton], + renderClassEntry: (ExportedClass) -> [String], + renderFunctionSignature: (ExportedFunction) -> String + ) -> [String] { + let printer = CodeFragmentPrinter() + let rootNode = NamespaceNode(name: "") + + buildExportsTree(rootNode: rootNode, exportedSkeletons: exportedSkeletons) + + for (_, node) in rootNode.children { + populateTypeScriptExportLines( + node: node, + renderClassEntry: renderClassEntry, + renderFunctionSignature: renderFunctionSignature + ) + } + + printExportsTypeHierarchy(node: rootNode, printer: printer) + + return printer.lines + } + + private func populateTypeScriptExportLines( + node: NamespaceNode, + renderClassEntry: (ExportedClass) -> [String], + renderFunctionSignature: (ExportedFunction) -> String + ) { + for function in node.content.functions { + let signature = renderFunctionSignature(function) + node.content.functionDtsLines.append((function.name, [signature])) + } + + for klass in node.content.classes { + let entry = renderClassEntry(klass) + node.content.classDtsLines.append((klass.name, entry)) + } + + for enumDef in node.content.enums { + node.content.enumDtsLines.append((enumDef.name, "\(enumDef.name): \(enumDef.objectTypeName)")) + } + + for (_, childNode) in node.children { + populateTypeScriptExportLines( + node: childNode, + renderClassEntry: renderClassEntry, + renderFunctionSignature: renderFunctionSignature + ) + } + } + + fileprivate func buildHierarchicalExportsObject( + exportedSkeletons: [ExportedSkeleton], + renderFunctionImpl: (ExportedFunction) throws -> [String] + ) throws -> [String] { + let printer = CodeFragmentPrinter() + let rootNode = NamespaceNode(name: "") + + buildExportsTree(rootNode: rootNode, exportedSkeletons: exportedSkeletons) + + try populateJavaScriptExportLines(node: rootNode, renderFunctionImpl: renderFunctionImpl) + + try populatePropertyImplementations(node: rootNode) + + printExportsObjectHierarchy(node: rootNode, printer: printer, currentPath: []) + + return printer.lines + } + + private func populateJavaScriptExportLines( + node: NamespaceNode, + renderFunctionImpl: (ExportedFunction) throws -> [String] + ) throws { + for function in node.content.functions { + let impl = try renderFunctionImpl(function) + node.content.functionJsLines.append((function.name, impl)) + } + + for (_, childNode) in node.children { + try populateJavaScriptExportLines(node: childNode, renderFunctionImpl: renderFunctionImpl) + } + } + + private func populatePropertyImplementations(node: NamespaceNode) throws { + for property in node.content.staticProperties { + // Generate getter + let getterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false)) + let getterReturnExpr = try getterThunkBuilder.call( + abiName: property.getterAbiName(), + returnType: property.type + ) + + let getterPrinter = CodeFragmentPrinter() + getterPrinter.write("get \(property.name)() {") + getterPrinter.indent { + getterPrinter.write(contentsOf: getterThunkBuilder.body) + getterPrinter.write(contentsOf: getterThunkBuilder.cleanupCode) + getterPrinter.write(lines: getterThunkBuilder.checkExceptionLines()) + if let returnExpr = getterReturnExpr { + getterPrinter.write("return \(returnExpr);") + } + } + getterPrinter.write("},") + + var propertyLines = getterPrinter.lines + + // Generate setter if not readonly + if !property.isReadonly { + let setterThunkBuilder = ExportedThunkBuilder( + effects: Effects(isAsync: false, isThrows: false) + ) + try setterThunkBuilder.lowerParameter( + param: Parameter(label: "value", name: "value", type: property.type) + ) + _ = try setterThunkBuilder.call( + abiName: property.setterAbiName(), + returnType: .void + ) + + let setterPrinter = CodeFragmentPrinter() + setterPrinter.write("set \(property.name)(value) {") + setterPrinter.indent { + setterPrinter.write(contentsOf: setterThunkBuilder.body) + setterPrinter.write(contentsOf: setterThunkBuilder.cleanupCode) + setterPrinter.write(lines: setterThunkBuilder.checkExceptionLines()) + } + setterPrinter.write("},") + + propertyLines.append(contentsOf: setterPrinter.lines) + } + + node.content.propertyJsLines.append(contentsOf: propertyLines) + } + + // Recursively process child nodes + for (_, childNode) in node.children { + try populatePropertyImplementations(node: childNode) + } + } + + private func printExportsTypeHierarchy(node: NamespaceNode, printer: CodeFragmentPrinter) { + for (childName, childNode) in node.children.sorted(by: { $0.key < $1.key }) { + printer.write("\(childName): {") + printer.indent { + for (_, lines) in childNode.content.classDtsLines.sorted(by: { $0.name < $1.name }) { + printer.write(lines: lines) + } + + for (_, line) in childNode.content.enumDtsLines.sorted(by: { $0.name < $1.name }) { + 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.functionDtsLines.sorted(by: { $0.name < $1.name }) { + for line in lines { + printer.write(line) + } + } + + printExportsTypeHierarchy(node: childNode, printer: printer) + } + printer.write("},") + } + } + + private func printExportsObjectHierarchy( + node: NamespaceNode, + printer: CodeFragmentPrinter, + currentPath: [String] = [] + ) { + for (childName, childNode) in node.children.sorted(by: { $0.key < $1.key }) { + let newPath = currentPath + [childName] + printer.write("\(childName): {") + printer.indent { + for klass in childNode.content.classes.sorted(by: { $0.name < $1.name }) { + printer.write("\(klass.name),") + } + + for enumDef in childNode.content.enums.sorted(by: { $0.name < $1.name }) { + printer.write("\(enumDef.name): \(enumDef.valuesName),") + } + + // Print function and property implementations + printer.write(lines: childNode.content.propertyJsLines) + for (name, lines) in childNode.content.functionJsLines.sorted(by: { $0.name < $1.name }) { + var modifiedLines = lines + if !modifiedLines.isEmpty { + modifiedLines[0] = "\(name): " + modifiedLines[0] + modifiedLines[modifiedLines.count - 1] += "," + } + printer.write(lines: modifiedLines) + } + + printExportsObjectHierarchy(node: childNode, printer: printer, currentPath: newPath) + } + printer.write("},") + } + } + /// Generates TypeScript declarations for all namespaces /// /// This function enables properly grouping all Swift code within given namespaces @@ -2368,70 +2580,7 @@ extension BridgeJSLink { let printer = CodeFragmentPrinter() let rootNode = NamespaceNode(name: "") - for skeleton in exportedSkeletons { - for function in skeleton.functions { - if function.effects.isStatic, - case .namespaceEnum = function.staticContext - { - // Use the function's namespace property instead of enumName - if let namespace = function.namespace { - var currentNode = rootNode - for part in namespace { - currentNode = currentNode.addChild(part) - } - currentNode.content.functions.append(function) - } - } else if let namespace = function.namespace { - var currentNode = rootNode - for part in namespace { - currentNode = currentNode.addChild(part) - } - currentNode.content.functions.append(function) - } - } - for klass in skeleton.classes { - if let classNamespace = klass.namespace { - var currentNode = rootNode - for part in classNamespace { - currentNode = currentNode.addChild(part) - } - currentNode.content.classes.append(klass) - } - } - for enumDefinition in skeleton.enums { - if let enumNamespace = enumDefinition.namespace, enumDefinition.enumType != .namespace { - var currentNode = rootNode - for part in enumNamespace { - currentNode = currentNode.addChild(part) - } - currentNode.content.enums.append(enumDefinition) - } - - if enumDefinition.enumType == .namespace { - for function in enumDefinition.staticMethods { - var currentNode = rootNode - // Build full namespace path: parent namespace + enum name - let fullNamespace = (enumDefinition.namespace ?? []) + [enumDefinition.name] - for part in fullNamespace { - currentNode = currentNode.addChild(part) - } - currentNode.content.functions.append(function) - } - - // Add static properties to namespace content for TypeScript declarations - for property in enumDefinition.staticProperties { - var currentNode = rootNode - let fullNamespace = (enumDefinition.namespace ?? []) + [enumDefinition.name] - for part in fullNamespace { - currentNode = currentNode.addChild(part) - } - if !currentNode.content.staticProperties.contains(where: { $0.name == property.name }) { - currentNode.content.staticProperties.append(property) - } - } - } - } - } + buildExportsTree(rootNode: rootNode, exportedSkeletons: exportedSkeletons) guard !rootNode.children.isEmpty else { return printer.lines @@ -2471,8 +2620,7 @@ extension BridgeJSLink { let sortedEnums = childNode.content.enums.sorted { $0.name < $1.name } for enumDefinition in sortedEnums { let style: EnumEmitStyle = enumDefinition.emitStyle - let enumValuesName = - enumDefinition.emitStyle == .tsEnum ? enumDefinition.name : "\(enumDefinition.name)Values" + let enumValuesName = enumDefinition.valuesName switch enumDefinition.enumType { case .simple: switch style { diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index b019e4d9..1b59ddfc 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -260,6 +260,9 @@ public enum EnumEmitStyle: String, Codable, Sendable { } public struct ExportedEnum: Codable, Equatable, Sendable { + public static let valuesSuffix = "Values" + public static let objectSuffix = "Object" + public let name: String public let swiftCallName: String public let explicitAccessControl: String? @@ -279,6 +282,14 @@ public struct ExportedEnum: Codable, Equatable, Sendable { } } + public var valuesName: String { + emitStyle == .tsEnum ? name : "\(name)\(Self.valuesSuffix)" + } + + public var objectTypeName: String { + "\(name)\(Self.objectSuffix)" + } + public init( name: String, swiftCallName: String, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.d.ts index aac40514..e79f4119 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.d.ts @@ -93,9 +93,13 @@ export type Exports = { roundTripOptionalAPIOptionalResult(result: APIOptionalResultTag | null): APIOptionalResultTag | null; APIResult: APIResultObject ComplexResult: ComplexResultObject - Result: ResultObject - NetworkingResult: NetworkingResultObject APIOptionalResult: APIOptionalResultObject + API: { + NetworkingResult: NetworkingResultObject + }, + Utilities: { + Result: ResultObject + }, } export type Imports = { } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.js index 658361df..14c150e5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.js @@ -830,9 +830,13 @@ export async function createInstantiator(options, swift) { }, APIResult: APIResultValues, ComplexResult: ComplexResultValues, - Result: ResultValues, - NetworkingResult: NetworkingResultValues, APIOptionalResult: APIOptionalResultValues, + API: { + NetworkingResult: NetworkingResultValues, + }, + Utilities: { + Result: ResultValues, + }, }; _exports = exports; return exports; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts index 7c05f171..b7c1549a 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts @@ -83,19 +83,31 @@ export interface TestServer extends SwiftHeapObject { call(method: Internal.SupportedMethodTag): void; } export type Exports = { - Converter: { - new(): Converter; - } - HTTPServer: { - new(): HTTPServer; - } - TestServer: { - new(): TestServer; - } - Method: MethodObject - LogLevel: LogLevelObject - Port: PortObject - SupportedMethod: SupportedMethodObject + Configuration: { + LogLevel: LogLevelObject + Port: PortObject + }, + Networking: { + API: { + HTTPServer: { + new(): HTTPServer; + } + Method: MethodObject + }, + APIV2: { + Internal: { + TestServer: { + new(): TestServer; + } + SupportedMethod: SupportedMethodObject + }, + }, + }, + Utils: { + Converter: { + new(): Converter; + } + }, } export type Imports = { } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js index a788abf5..a38b5093 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js @@ -333,18 +333,30 @@ export async function createInstantiator(options, swift) { globalThis.Utils = {}; } const exports = { - Converter, - HTTPServer, - TestServer, - Method: MethodValues, - LogLevel: LogLevelValues, - Port: PortValues, - SupportedMethod: SupportedMethodValues, + Configuration: { + LogLevel: LogLevelValues, + Port: PortValues, + }, + Networking: { + API: { + HTTPServer, + Method: MethodValues, + }, + APIV2: { + Internal: { + TestServer, + SupportedMethod: SupportedMethodValues, + }, + }, + }, + Utils: { + Converter, + }, }; _exports = exports; - globalThis.Utils.Converter = exports.Converter; - globalThis.Networking.API.HTTPServer = exports.HTTPServer; - globalThis.Networking.APIV2.Internal.TestServer = exports.TestServer; + globalThis.Utils.Converter = exports.Utils.Converter; + globalThis.Networking.API.HTTPServer = exports.Networking.API.HTTPServer; + globalThis.Networking.APIV2.Internal.TestServer = exports.Networking.APIV2.Internal.TestServer; return exports; }, } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts index c6e40399..cdf83f36 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts @@ -50,16 +50,28 @@ export interface UUID extends SwiftHeapObject { uuidString(): string; } export type Exports = { - Greeter: { - new(name: string): Greeter; - } - Converter: { - new(): Converter; - } - UUID: { - } plainFunction(): string; - namespacedFunction(): string; + MyModule: { + Utils: { + namespacedFunction(): string; + }, + }, + Utils: { + Converters: { + Converter: { + new(): Converter; + } + }, + }, + __Swift: { + Foundation: { + Greeter: { + new(name: string): Greeter; + } + UUID: { + } + }, + }, } export type Imports = { } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js index 395b97f1..67670d20 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js @@ -294,27 +294,39 @@ export async function createInstantiator(options, swift) { globalThis.__Swift.Foundation = {}; } const exports = { - Greeter, - Converter, - UUID, plainFunction: function bjs_plainFunction() { instance.exports.bjs_plainFunction(); const ret = tmpRetString; tmpRetString = undefined; return ret; }, - namespacedFunction: function bjs_MyModule_Utils_namespacedFunction() { - instance.exports.bjs_MyModule_Utils_namespacedFunction(); - const ret = tmpRetString; - tmpRetString = undefined; - return ret; + MyModule: { + Utils: { + namespacedFunction: function bjs_MyModule_Utils_namespacedFunction() { + instance.exports.bjs_MyModule_Utils_namespacedFunction(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + }, + }, + }, + Utils: { + Converters: { + Converter, + }, + }, + __Swift: { + Foundation: { + Greeter, + UUID, + }, }, }; _exports = exports; - globalThis.__Swift.Foundation.Greeter = exports.Greeter; - globalThis.Utils.Converters.Converter = exports.Converter; - globalThis.__Swift.Foundation.UUID = exports.UUID; - globalThis.MyModule.Utils.namespacedFunction = exports.namespacedFunction; + globalThis.__Swift.Foundation.Greeter = exports.__Swift.Foundation.Greeter; + globalThis.Utils.Converters.Converter = exports.Utils.Converters.Converter; + globalThis.__Swift.Foundation.UUID = exports.__Swift.Foundation.UUID; + globalThis.MyModule.Utils.namespacedFunction = exports.MyModule.Utils.namespacedFunction; return exports; }, } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Export.d.ts index 9622891a..069c1628 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Export.d.ts @@ -56,6 +56,11 @@ export type Exports = { } Calculator: CalculatorObject APIResult: APIResultObject + Utils: { + String: { + uppercase(text: string): string; + }, + }, } export type Imports = { } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Export.js index 14f11469..3e445992 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Export.js @@ -305,15 +305,6 @@ export async function createInstantiator(options, swift) { } const exports = { MathUtils, - uppercase: function bjs_Utils_String_static_uppercase(text) { - const textBytes = textEncoder.encode(text); - const textId = swift.memory.retain(textBytes); - instance.exports.bjs_Utils_String_static_uppercase(textId, textBytes.length); - const ret = tmpRetString; - tmpRetString = undefined; - swift.memory.release(textId); - return ret; - }, Calculator: { ...CalculatorValues, square: function(value) { @@ -331,9 +322,22 @@ export async function createInstantiator(options, swift) { return ret; } }, + Utils: { + String: { + uppercase: function bjs_Utils_String_static_uppercase(text) { + const textBytes = textEncoder.encode(text); + const textId = swift.memory.retain(textBytes); + instance.exports.bjs_Utils_String_static_uppercase(textId, textBytes.length); + const ret = tmpRetString; + tmpRetString = undefined; + swift.memory.release(textId); + return ret; + }, + }, + }, }; _exports = exports; - globalThis.Utils.String.uppercase = exports.uppercase; + globalThis.Utils.String.uppercase = exports.Utils.String.uppercase; return exports; }, } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Export.d.ts index 40e28718..fea3c4b5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Export.d.ts @@ -51,6 +51,15 @@ export type Exports = { optionalProperty: string | null; } PropertyEnum: PropertyEnumObject + PropertyNamespace: { + readonly namespaceConstant: string; + namespaceProperty: string; + Nested: { + readonly nestedConstant: string; + nestedDouble: number; + nestedProperty: number; + }, + }, } export type Imports = { } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Export.js index 3577228e..09a2b0de 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Export.js @@ -303,41 +303,12 @@ export async function createInstantiator(options, swift) { } } } - Object.defineProperty(globalThis.PropertyNamespace, 'namespaceProperty', { get: function() { - instance.exports.bjs_PropertyNamespace_static_namespaceProperty_get(); - const ret = tmpRetString; - tmpRetString = undefined; - return ret; - }, set: function(value) { - const valueBytes = textEncoder.encode(value); - const valueId = swift.memory.retain(valueBytes); - instance.exports.bjs_PropertyNamespace_static_namespaceProperty_set(valueId, valueBytes.length); - swift.memory.release(valueId); - } }); - Object.defineProperty(globalThis.PropertyNamespace, 'namespaceConstant', { get: function() { - instance.exports.bjs_PropertyNamespace_static_namespaceConstant_get(); - const ret = tmpRetString; - tmpRetString = undefined; - return ret; - } }); - Object.defineProperty(globalThis.PropertyNamespace.Nested, 'nestedProperty', { get: function() { - const ret = instance.exports.bjs_PropertyNamespace_Nested_static_nestedProperty_get(); - return ret; - }, set: function(value) { - instance.exports.bjs_PropertyNamespace_Nested_static_nestedProperty_set(value); - } }); - Object.defineProperty(globalThis.PropertyNamespace.Nested, 'nestedConstant', { get: function() { - instance.exports.bjs_PropertyNamespace_Nested_static_nestedConstant_get(); - const ret = tmpRetString; - tmpRetString = undefined; - return ret; - } }); - Object.defineProperty(globalThis.PropertyNamespace.Nested, 'nestedDouble', { get: function() { - const ret = instance.exports.bjs_PropertyNamespace_Nested_static_nestedDouble_get(); - return ret; - }, set: function(value) { - instance.exports.bjs_PropertyNamespace_Nested_static_nestedDouble_set(value); - } }); + if (typeof globalThis.PropertyNamespace === 'undefined') { + globalThis.PropertyNamespace = {}; + } + if (typeof globalThis.PropertyNamespace.Nested === 'undefined') { + globalThis.PropertyNamespace.Nested = {}; + } const exports = { PropertyClass, PropertyEnum: { @@ -371,8 +342,68 @@ export async function createInstantiator(options, swift) { swift.memory.release(valueId); } }, + PropertyNamespace: { + get namespaceProperty() { + instance.exports.bjs_PropertyNamespace_static_namespaceProperty_get(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + }, + set namespaceProperty(value) { + const valueBytes = textEncoder.encode(value); + const valueId = swift.memory.retain(valueBytes); + instance.exports.bjs_PropertyNamespace_static_namespaceProperty_set(valueId, valueBytes.length); + swift.memory.release(valueId); + }, + get namespaceConstant() { + instance.exports.bjs_PropertyNamespace_static_namespaceConstant_get(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + }, + Nested: { + get nestedProperty() { + const ret = instance.exports.bjs_PropertyNamespace_Nested_static_nestedProperty_get(); + return ret; + }, + set nestedProperty(value) { + instance.exports.bjs_PropertyNamespace_Nested_static_nestedProperty_set(value); + }, + get nestedConstant() { + instance.exports.bjs_PropertyNamespace_Nested_static_nestedConstant_get(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + }, + get nestedDouble() { + const ret = instance.exports.bjs_PropertyNamespace_Nested_static_nestedDouble_get(); + return ret; + }, + set nestedDouble(value) { + instance.exports.bjs_PropertyNamespace_Nested_static_nestedDouble_set(value); + }, + }, + }, }; _exports = exports; + Object.defineProperty(globalThis.PropertyNamespace, 'namespaceProperty', { + get: () => exports.PropertyNamespace.namespaceProperty, + set: (value) => { exports.PropertyNamespace.namespaceProperty = value; } + }); + Object.defineProperty(globalThis.PropertyNamespace, 'namespaceConstant', { + get: () => exports.PropertyNamespace.namespaceConstant, + }); + Object.defineProperty(globalThis.PropertyNamespace.Nested, 'nestedProperty', { + get: () => exports.PropertyNamespace.Nested.nestedProperty, + set: (value) => { exports.PropertyNamespace.Nested.nestedProperty = value; } + }); + Object.defineProperty(globalThis.PropertyNamespace.Nested, 'nestedConstant', { + get: () => exports.PropertyNamespace.Nested.nestedConstant, + }); + Object.defineProperty(globalThis.PropertyNamespace.Nested, 'nestedDouble', { + get: () => exports.PropertyNamespace.Nested.nestedDouble, + set: (value) => { exports.PropertyNamespace.Nested.nestedDouble = value; } + }); return exports; }, } diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md index 2026c6a2..427dd597 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md @@ -21,10 +21,18 @@ import JavaScriptKit } ``` -This function will be accessible in JavaScript through its namespace hierarchy: +This function will be accessible in JavaScript through its namespace hierarchy in two ways: ```javascript -// Access the function through its namespace +// Recommended: Access via the exports object (supports multiple WASM instances) +const { createInstantiator } = await import('./path/to/bridge-js.js'); +const instantiator = await createInstantiator(options, swift); +const exports = instantiator.createExports(instance); + +const result = exports.MyModule.Utils.namespacedFunction(); +console.log(result); // "namespaced" + +// Alternative: Access via globalThis const result = globalThis.MyModule.Utils.namespacedFunction(); console.log(result); // "namespaced" ``` @@ -65,10 +73,14 @@ import JavaScriptKit } ``` -In JavaScript, this class is accessible through its namespace: +In JavaScript, this class is accessible through its namespace in two ways: ```javascript -// Create instances through namespaced constructors +// Recommended: Access via the exports object (supports multiple WASM instances) +const greeter = new exports.__Swift.Foundation.Greeter("World"); +console.log(greeter.greet()); // "Hello, World!" + +// Alternative: Access via globalThis and through its namespace const greeter = new globalThis.__Swift.Foundation.Greeter("World"); console.log(greeter.greet()); // "Hello, World!" ``` @@ -87,6 +99,16 @@ declare global { } } +export type Exports = { + __Swift: { + Foundation: { + Greeter: { + new(name: string): Greeter; + } + } + } +} + export interface Greeter extends SwiftHeapObject { greet(): string; } diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index f6e9cb85..77123fec 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -356,6 +356,22 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(globalThis.StaticPropertyNamespace.NestedProperties.nestedProperty, 1000); assert.equal(globalThis.StaticPropertyNamespace.NestedProperties.nestedDouble, 2.828); + assert.equal(exports.StaticPropertyNamespace.namespaceProperty, "modified namespace"); + assert.equal(exports.StaticPropertyNamespace.namespaceConstant, "constant"); + exports.StaticPropertyNamespace.namespaceProperty = "exports modified"; + assert.equal(exports.StaticPropertyNamespace.namespaceProperty, "exports modified"); + assert.equal(globalThis.StaticPropertyNamespace.namespaceProperty, "exports modified"); + + assert.equal(exports.StaticPropertyNamespace.NestedProperties.nestedProperty, 1000); + assert.equal(exports.StaticPropertyNamespace.NestedProperties.nestedConstant, "nested"); + assert.equal(exports.StaticPropertyNamespace.NestedProperties.nestedDouble, 2.828); + exports.StaticPropertyNamespace.NestedProperties.nestedProperty = 2000; + exports.StaticPropertyNamespace.NestedProperties.nestedDouble = 3.14; + assert.equal(exports.StaticPropertyNamespace.NestedProperties.nestedProperty, 2000); + assert.equal(exports.StaticPropertyNamespace.NestedProperties.nestedDouble, 3.14); + assert.equal(globalThis.StaticPropertyNamespace.NestedProperties.nestedProperty, 2000); + assert.equal(globalThis.StaticPropertyNamespace.NestedProperties.nestedDouble, 3.14); + // Test class without @JS init constructor const calc = exports.createCalculator(); assert.equal(calc.square(5), 25); @@ -428,6 +444,10 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(globalThis.Networking.API.MethodValues.Post, 1); assert.equal(globalThis.Networking.API.MethodValues.Put, 2); assert.equal(globalThis.Networking.API.MethodValues.Delete, 3); + assert.equal(exports.Networking.API.Method.Get, 0); + assert.equal(exports.Networking.API.Method.Post, 1); + assert.equal(exports.Networking.API.Method.Put, 2); + assert.equal(exports.Networking.API.Method.Delete, 3); assert.equal(globalThis.Configuration.LogLevelValues.Debug, "debug"); assert.equal(globalThis.Configuration.LogLevelValues.Info, "info"); assert.equal(globalThis.Configuration.LogLevelValues.Warning, "warning"); @@ -435,8 +455,17 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(globalThis.Configuration.PortValues.Http, 80); assert.equal(globalThis.Configuration.PortValues.Https, 443); assert.equal(globalThis.Configuration.PortValues.Development, 3000); + assert.equal(exports.Configuration.LogLevel.Debug, "debug"); + assert.equal(exports.Configuration.LogLevel.Info, "info"); + assert.equal(exports.Configuration.LogLevel.Warning, "warning"); + assert.equal(exports.Configuration.LogLevel.Error, "error"); + assert.equal(exports.Configuration.Port.Http, 80); + assert.equal(exports.Configuration.Port.Https, 443); + assert.equal(exports.Configuration.Port.Development, 3000); assert.equal(globalThis.Networking.APIV2.Internal.SupportedMethodValues.Get, 0); assert.equal(globalThis.Networking.APIV2.Internal.SupportedMethodValues.Post, 1); + assert.equal(exports.Networking.APIV2.Internal.SupportedMethod.Get, 0); + assert.equal(exports.Networking.APIV2.Internal.SupportedMethod.Post, 1); assert.equal(exports.roundtripNetworkingAPIMethod(globalThis.Networking.API.MethodValues.Get), globalThis.Networking.API.MethodValues.Get); assert.equal(exports.roundtripConfigurationLogLevel(globalThis.Configuration.LogLevelValues.Debug), globalThis.Configuration.LogLevelValues.Debug); @@ -447,17 +476,17 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(exports.processConfigurationLogLevel(globalThis.Configuration.LogLevelValues.Error), globalThis.Configuration.PortValues.Development); assert.equal(exports.roundtripInternalSupportedMethod(globalThis.Networking.APIV2.Internal.SupportedMethodValues.Get), globalThis.Networking.APIV2.Internal.SupportedMethodValues.Get); - const converter = new exports.Converter(); + const converter = new exports.Utils.Converter(); assert.equal(converter.toString(42), "42"); assert.equal(converter.toString(123), "123"); converter.release(); - const httpServer = new exports.HTTPServer(); + const httpServer = new exports.Networking.API.HTTPServer(); httpServer.call(globalThis.Networking.API.MethodValues.Get); httpServer.call(globalThis.Networking.API.MethodValues.Post); httpServer.release(); - const testServer = new exports.TestServer(); + const testServer = new exports.Networking.APIV2.Internal.TestServer(); testServer.call(globalThis.Networking.APIV2.Internal.SupportedMethodValues.Get); testServer.call(globalThis.Networking.APIV2.Internal.SupportedMethodValues.Post); testServer.release(); @@ -643,6 +672,8 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(StaticCalculatorValues.Scientific, exports.StaticCalculator.Scientific); assert.equal(StaticCalculatorValues.Basic, exports.StaticCalculator.Basic); assert.equal(globalThis.StaticUtils.Nested.roundtrip("hello world"), "hello world"); + assert.equal(exports.StaticUtils.Nested.roundtrip("test"), "test"); + assert.equal(exports.StaticUtils.Nested.roundtrip("exports api"), "exports api"); // Test default parameters assert.equal(exports.testStringDefault(), "Hello World"); @@ -1034,8 +1065,8 @@ function testProtocolSupport(exports) { getAPIResult() { return lastAPIResult; } }; - const successResult = { tag: exports.Result.Tag.Success, param0: "Operation completed" }; - const failureResult = { tag: exports.Result.Tag.Failure, param0: 500 }; + const successResult = { tag: exports.Utilities.Result.Tag.Success, param0: "Operation completed" }; + const failureResult = { tag: exports.Utilities.Result.Tag.Failure, param0: 500 }; const jsManager = new exports.DataProcessorManager(jsProcessor);