Skip to content

Commit 73389eb

Browse files
committed
only use JS-module paths in skeletons, defer file handling to link time
1 parent 6e3beb3 commit 73389eb

31 files changed

Lines changed: 415 additions & 390 deletions

File tree

Plugins/BridgeJS/Package.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ let package = Package(
5757
dependencies: [
5858
"BridgeJSCore",
5959
"BridgeJSLink",
60-
"BridgeJSBuildPlugin",
6160
"TS2Swift",
6261
.product(name: "SwiftParser", package: "swift-syntax"),
6362
.product(name: "SwiftSyntax", package: "swift-syntax"),

Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
2727
.map(\.url)
2828

2929
let configFile = pathToConfigFile(target: target)
30-
let inputJavaScriptFiles = discoverJavaScriptModuleFiles(in: target.directoryURL)
31-
var inputFiles: [URL] = inputSwiftFiles + inputJavaScriptFiles
30+
var inputFiles: [URL] = inputSwiftFiles
3231
if FileManager.default.fileExists(atPath: configFile.path) {
3332
inputFiles.append(configFile)
3433
}
@@ -78,7 +77,7 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
7877
}
7978

8079
let allSwiftFiles = inputSwiftFiles + pluginGeneratedSwiftFiles
81-
arguments.append(contentsOf: (allSwiftFiles + inputJavaScriptFiles).map(\.path))
80+
arguments.append(contentsOf: allSwiftFiles.map(\.path))
8281

8382
return .buildCommand(
8483
displayName: "Generate BridgeJS code",

Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ extension BridgeJSCommandPlugin.Context {
184184
!$0.url.path.hasPrefix(generatedDirectory.path + "/")
185185
}.map(\.url.path)
186186
)
187-
generateArguments.append(contentsOf: discoverJavaScriptModuleFiles(in: target.directoryURL).map(\.path))
188187
generateArguments.append(contentsOf: remainingArguments)
189188

190189
try runBridgeJSTool(arguments: generateArguments)

Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSPluginUtilities

Lines changed: 0 additions & 1 deletion
This file was deleted.

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ public final class SwiftToSkeleton {
2323

2424
private var sourceFiles: [(sourceFile: SourceFileSyntax, inputFilePath: String)] = []
2525
private var usedExternalModules = Set<String>()
26-
private let javaScriptModuleSource: (String) throws -> String?
27-
private var importedJSModules: [String: ImportedJSModule] = [:]
26+
private let javaScriptModuleExists: (String) throws -> Bool
27+
private var validatedJavaScriptModulePaths = Set<String>()
2828

2929
/// Non-fatal diagnostics collected during `finalize()`. These do not fail the build.
3030
public private(set) var warnings: [(file: String, diagnostic: DiagnosticError)] = []
@@ -35,13 +35,13 @@ public final class SwiftToSkeleton {
3535
exposeToGlobal: Bool,
3636
externalModuleIndex: ExternalModuleIndex,
3737
identityMode: String? = nil,
38-
javaScriptModuleSource: @escaping (String) throws -> String? = { _ in nil }
38+
javaScriptModuleExists: @escaping (String) throws -> Bool = { _ in false }
3939
) {
4040
self.progress = progress
4141
self.moduleName = moduleName
4242
self.exposeToGlobal = exposeToGlobal
4343
self.identityMode = identityMode
44-
self.javaScriptModuleSource = javaScriptModuleSource
44+
self.javaScriptModuleExists = javaScriptModuleExists
4545
self.typeDeclResolver = TypeDeclResolver()
4646
self.externalModuleIndex = externalModuleIndex
4747

@@ -100,7 +100,7 @@ public final class SwiftToSkeleton {
100100
+ importCollector.importedGlobalGetters.compactMap(\.from)
101101
let modulePaths = Set(importOrigins.compactMap(\.modulePath))
102102
for path in modulePaths.sorted() {
103-
if importedJSModules[path] != nil {
103+
if validatedJavaScriptModulePaths.contains(path) {
104104
continue
105105
}
106106
let pathNode = importCollector.importedModulePathNodes[path] ?? Syntax(sourceFile)
@@ -114,7 +114,26 @@ public final class SwiftToSkeleton {
114114
)
115115
continue
116116
}
117-
guard let source = try javaScriptModuleSource(path) else {
117+
guard !path.split(separator: "/").contains("..") else {
118+
importCollector.errors.append(
119+
DiagnosticError(
120+
node: pathNode,
121+
message: "JavaScript module paths must not contain '..': '\(path)'."
122+
)
123+
)
124+
continue
125+
}
126+
let lowercasedPath = path.lowercased()
127+
guard lowercasedPath.hasSuffix(".js") || lowercasedPath.hasSuffix(".mjs") else {
128+
importCollector.errors.append(
129+
DiagnosticError(
130+
node: pathNode,
131+
message: "JavaScript modules must use a '.js' or '.mjs' extension: '\(path)'."
132+
)
133+
)
134+
continue
135+
}
136+
guard try javaScriptModuleExists(path) else {
118137
importCollector.errors.append(
119138
DiagnosticError(
120139
node: pathNode,
@@ -123,7 +142,7 @@ public final class SwiftToSkeleton {
123142
)
124143
continue
125144
}
126-
importedJSModules[path] = ImportedJSModule(path: path, source: source)
145+
validatedJavaScriptModulePaths.insert(path)
127146
}
128147

129148
let exportErrors = exportCollector.errors.filter { $0.severity == .error }
@@ -164,11 +183,7 @@ public final class SwiftToSkeleton {
164183
throw BridgeJSCoreDiagnosticError(diagnostics: diagnostics)
165184
}
166185
let importedSkeleton: ImportedModuleSkeleton? = {
167-
let modules = importedJSModules.values.sorted { $0.path < $1.path }
168-
let module = ImportedModuleSkeleton(
169-
children: importedFiles,
170-
modules: modules.isEmpty ? nil : modules
171-
)
186+
let module = ImportedModuleSkeleton(children: importedFiles)
172187
if module.children.allSatisfy({ $0.functions.isEmpty && $0.types.isEmpty && $0.globalGetters.isEmpty }) {
173188
return nil
174189
}

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ public struct BridgeJSLink {
4141
return configIdentityMode == "pointer"
4242
}
4343

44-
mutating func addSkeletonFile(data: Data) throws {
44+
@discardableResult
45+
mutating func addSkeletonFile(data: Data) throws -> BridgeJSSkeleton {
4546
do {
4647
let unified = try JSONDecoder().decode(BridgeJSSkeleton.self, from: data)
4748
skeletons.append(unified)
49+
return unified
4850
} catch {
4951
struct SkeletonDecodingError: Error, CustomStringConvertible {
5052
let description: String
@@ -1226,9 +1228,9 @@ public struct BridgeJSLink {
12261228
return printer.lines.joined(separator: "\n")
12271229
}
12281230

1229-
public func link() throws -> BridgeJSLinkOutput {
1231+
public func link() throws -> (outputJs: String, outputDts: String) {
12301232
intrinsicRegistry.reset()
1231-
try importedModuleRegistry.configure(skeletons: skeletons)
1233+
importedModuleRegistry.configure(skeletons: skeletons)
12321234
intrinsicRegistry.classNamespaces = skeletons.reduce(into: [:]) { result, unified in
12331235
guard let skeleton = unified.exported else { return }
12341236
for klass in skeleton.classes {
@@ -1240,11 +1242,7 @@ public struct BridgeJSLink {
12401242
let data = try collectLinkData()
12411243
let outputJs = try generateJavaScript(data: data)
12421244
let outputDts = generateTypeScript(data: data)
1243-
return BridgeJSLinkOutput(
1244-
outputJs: outputJs,
1245-
outputDts: outputDts,
1246-
modules: importedModuleRegistry.artifacts
1247-
)
1245+
return (outputJs, outputDts)
12481246
}
12491247

12501248
private func enumHelperAssignments() -> CodeFragmentPrinter {

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLinkOutput.swift

Lines changed: 0 additions & 21 deletions
This file was deleted.

Plugins/BridgeJS/Sources/BridgeJSLink/ImportedJSModuleRegistry.swift

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,41 @@ import BridgeJSSkeleton
33
#endif
44

55
final class ImportedJSModuleRegistry {
6-
private struct Key: Hashable {
6+
struct Reference: Hashable {
77
let swiftModuleName: String
88
let path: String
9+
10+
var relativeOutputPath: String {
11+
"bridge-js-modules/\(swiftModuleName)\(path)"
12+
}
913
}
1014

11-
private var aliases: [Key: String] = [:]
12-
private(set) var artifacts: [BridgeJSLinkOutput.Module] = []
15+
private var aliases: [Reference: String] = [:]
16+
private(set) var references: [Reference] = []
1317

14-
func configure(skeletons: [BridgeJSSkeleton]) throws {
18+
func configure(skeletons: [BridgeJSSkeleton]) {
1519
aliases.removeAll(keepingCapacity: true)
16-
artifacts.removeAll(keepingCapacity: true)
20+
references = Self.collectReferences(skeletons: skeletons)
21+
for (index, reference) in references.enumerated() {
22+
aliases[reference] = "__bjs_imported_module_\(index)"
23+
}
24+
}
1725

18-
var sources: [Key: String] = [:]
26+
static func collectReferences(skeletons: [BridgeJSSkeleton]) -> [Reference] {
27+
var references = Set<Reference>()
1928
for skeleton in skeletons {
20-
for module in skeleton.imported?.modules ?? [] {
21-
guard module.path.hasPrefix("/") else {
22-
throw BridgeJSLinkError(
23-
message: "JavaScript module path must start with '/': \(module.path)"
24-
)
25-
}
26-
let key = Key(swiftModuleName: skeleton.moduleName, path: module.path)
27-
if let existing = sources[key], existing != module.source {
28-
throw BridgeJSLinkError(
29-
message: "Conflicting JavaScript module contents for \(skeleton.moduleName)/\(module.path)"
30-
)
29+
for file in skeleton.imported?.children ?? [] {
30+
let origins =
31+
file.functions.compactMap(\.from)
32+
+ file.globalGetters.compactMap(\.from)
33+
+ file.types.compactMap(\.from)
34+
for case .module(let path) in origins {
35+
references.insert(Reference(swiftModuleName: skeleton.moduleName, path: path))
3136
}
32-
sources[key] = module.source
3337
}
3438
}
35-
36-
for (index, entry) in sources.sorted(by: {
37-
($0.key.swiftModuleName, $0.key.path) < ($1.key.swiftModuleName, $1.key.path)
38-
}).enumerated() {
39-
let relativePath = "bridge-js-modules/\(entry.key.swiftModuleName)\(entry.key.path)"
40-
aliases[entry.key] = "__bjs_imported_module_\(index)"
41-
artifacts.append(.init(relativePath: relativePath, source: entry.value))
39+
return references.sorted {
40+
($0.swiftModuleName, $0.path) < ($1.swiftModuleName, $1.path)
4241
}
4342
}
4443

@@ -49,19 +48,19 @@ final class ImportedJSModuleRegistry {
4948
case .global:
5049
return "globalThis"
5150
case .module(let path):
52-
let key = Key(swiftModuleName: swiftModuleName, path: path)
53-
guard let alias = aliases[key] else {
51+
let reference = Reference(swiftModuleName: swiftModuleName, path: path)
52+
guard let alias = aliases[reference] else {
5453
throw BridgeJSLinkError(
55-
message: "Missing embedded JavaScript module for \(swiftModuleName)/\(path)"
54+
message: "Missing JavaScript module \(swiftModuleName)\(path)"
5655
)
5756
}
5857
return alias
5958
}
6059
}
6160

6261
var importLines: [String] {
63-
artifacts.enumerated().map { index, artifact in
64-
let path = BridgeJSLink.escapeForJavaScriptStringLiteral(artifact.relativePath)
62+
references.enumerated().map { index, reference in
63+
let path = BridgeJSLink.escapeForJavaScriptStringLiteral(reference.relativeOutputPath)
6564
return "import * as __bjs_imported_module_\(index) from \"./\(path)\";"
6665
}
6766
}

Plugins/BridgeJS/Sources/BridgeJSPluginUtilities/InputDiscovery.swift

Lines changed: 0 additions & 19 deletions
This file was deleted.

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,13 +1126,24 @@ private struct AsyncClosureReturnTypeCollector: BridgeSkeletonVisitor {
11261126
/// Controls where BridgeJS reads imported JS values from.
11271127
///
11281128
/// - `global`: Read from `globalThis`.
1129+
/// - `module`: Read from a target-local ECMAScript module.
11291130
public enum JSImportFrom: Codable, Equatable, Sendable {
11301131
case global
11311132
case module(String)
11321133

11331134
public init(from decoder: any Decoder) throws {
1134-
let value = try decoder.singleValueContainer().decode(String.self)
1135-
self = value == "global" ? .global : .module(value)
1135+
let container = try decoder.singleValueContainer()
1136+
let value = try container.decode(String.self)
1137+
if value == "global" {
1138+
self = .global
1139+
} else if value.hasPrefix("/") && !value.split(separator: "/").contains("..") {
1140+
self = .module(value)
1141+
} else {
1142+
throw DecodingError.dataCorruptedError(
1143+
in: container,
1144+
debugDescription: "Unknown import origin '\(value)'. Expected \"global\" or a rooted module path."
1145+
)
1146+
}
11361147
}
11371148

11381149
public func encode(to encoder: any Encoder) throws {
@@ -1146,17 +1157,6 @@ public enum JSImportFrom: Codable, Equatable, Sendable {
11461157
}
11471158
}
11481159

1149-
/// A target-local ECMAScript module embedded into a BridgeJS skeleton.
1150-
public struct ImportedJSModule: Codable, Equatable, Sendable {
1151-
public let path: String
1152-
public let source: String
1153-
1154-
public init(path: String, source: String) {
1155-
self.path = path
1156-
self.source = source
1157-
}
1158-
}
1159-
11601160
public struct ImportedFunctionSkeleton: Codable {
11611161
public let name: String
11621162
/// The JavaScript function/method name to call, if different from `name`.
@@ -1482,32 +1482,9 @@ public struct ImportedFileSkeleton: Codable {
14821482

14831483
public struct ImportedModuleSkeleton: Codable {
14841484
public var children: [ImportedFileSkeleton]
1485-
public var modules: [ImportedJSModule]?
14861485

1487-
public init(children: [ImportedFileSkeleton], modules: [ImportedJSModule]? = nil) {
1486+
public init(children: [ImportedFileSkeleton]) {
14881487
self.children = children
1489-
self.modules = modules
1490-
}
1491-
1492-
/// Deterministic non-cryptographic content fingerprint used to invalidate
1493-
/// build-plugin outputs when only an embedded JavaScript module changes.
1494-
public var moduleContentFingerprint: String? {
1495-
guard let modules, !modules.isEmpty else { return nil }
1496-
var hash: UInt64 = 0xcbf29ce484222325
1497-
func combine<S: Sequence>(_ bytes: S) where S.Element == UInt8 {
1498-
for byte in bytes {
1499-
hash ^= UInt64(byte)
1500-
hash &*= 0x100000001b3
1501-
}
1502-
}
1503-
for module in modules.sorted(by: { $0.path < $1.path }) {
1504-
combine(module.path.utf8)
1505-
combine(CollectionOfOne(UInt8(0)))
1506-
combine(module.source.utf8)
1507-
combine(CollectionOfOne(UInt8(0xff)))
1508-
}
1509-
let hexadecimal = String(hash, radix: 16)
1510-
return String(repeating: "0", count: 16 - hexadecimal.count) + hexadecimal
15111488
}
15121489
}
15131490

0 commit comments

Comments
 (0)