diff --git a/Package.resolved b/Package.resolved index 2f09fe3..955be34 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "d0598e83f2b2806716655b5032a402046dd1efdff757df1d642ead8b025cb10a", + "originHash" : "8395e2e32ae08588577a2030c55a4ebaf8f5db87cd61dad3bb35e9125823a155", "pins" : [ { "identity" : "coremodel", "kind" : "remoteSourceControl", "location" : "https://github.com/PureSwift/CoreModel.git", "state" : { - "revision" : "2fc6856cd47d376acd50e66e8c798894a4b5ffe3", - "version" : "2.8.1" + "revision" : "790cfe9680a8705907a73898620a431a2c087bb2", + "version" : "2.10.0" } }, { diff --git a/Package.swift b/Package.swift index 5e4d137..6f20eb3 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,17 @@ import PackageDescription import class Foundation.ProcessInfo let darwin: [Platform] = [.macOS, .iOS, .tvOS, .watchOS, .visionOS, .macCatalyst] -let nonAndroidPlatforms: [Platform] = darwin + [.linux, .windows, .wasi, .openbsd] +// Platforms whose Foundation ships a usable `URLSession` networking stack, so +// `HTTPTypesFoundation`'s `URLSession: HTTPClient` bridge can be linked. +// Excludes Android (JNI-callback transport, see `FuelingAndroid`) and wasm +// (JavaScriptKit `fetch` transport, see `Web/`) — both provide their own +// `HTTPClient` and must not drag in `FoundationNetworking`. +let urlSessionPlatforms: [Platform] = darwin + [.linux, .windows, .openbsd] +// Platforms the SQLite persistence backend supports. Excludes wasm: the SQLite +// package's SQLCipher/libtomcrypt C code fails to compile for `wasm32-unknown-wasip1` +// (it needs `_WASI_EMULATED_SIGNAL`), so the browser app uses the in-memory +// store (`Store(inMemory:)`) instead of `Store(sqliteDatabase:)`. +let sqlitePlatforms: [Platform] = darwin + [.linux, .windows, .android, .openbsd] // The Android jextract/JNI build (see Android/fueling-jni/build.gradle.kts) cross-compiles // this package with this flag set, so every library in the graph — including this package's @@ -47,7 +57,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/PureSwift/CoreModel.git", - from: "2.8.0" + from: "2.10.0" ), .package( url: "https://github.com/apple/swift-http-types", @@ -90,7 +100,7 @@ let package = Package( .product( name: "HTTPTypesFoundation", package: "swift-http-types", - condition: .when(platforms: nonAndroidPlatforms) + condition: .when(platforms: urlSessionPlatforms) ) ] ), @@ -114,11 +124,13 @@ let package = Package( ), .product( name: "CoreModelSQLite", - package: "CoreModel-SQLite" + package: "CoreModel-SQLite", + condition: .when(platforms: sqlitePlatforms) ), .product( name: "SQLite", - package: "SQLite" + package: "SQLite", + condition: .when(platforms: sqlitePlatforms) ) ] ), diff --git a/Sources/FuelingAPI/URLSession.swift b/Sources/FuelingAPI/URLSession.swift index b48cff4..e406cab 100644 --- a/Sources/FuelingAPI/URLSession.swift +++ b/Sources/FuelingAPI/URLSession.swift @@ -3,11 +3,13 @@ // FuelingAPI // -// Excluded on Android: the app there supplies its own JNI-callback transport -// (see `FuelingAndroid`), and merely linking `HTTPTypesFoundation`'s -// `URLSession` bridge would pull `FoundationNetworking` + its ~42 MB ICU -// dependency chain into every Android build. -#if canImport(Foundation) && !os(Android) +// Excluded on Android and wasm: each supplies its own transport (a JNI-callback +// client in `FuelingAndroid`, a JavaScriptKit `fetch` client in `Web/`), and +// `HTTPTypesFoundation` isn't linked there anyway — on Android it would drag in +// `FoundationNetworking` + its ~42 MB ICU chain, and on wasm `URLSession` has +// no networking backend. The manifest drops the product on both (see +// `urlSessionPlatforms`); the `!os(WASI)` guard keeps this file from importing it. +#if canImport(Foundation) && !os(Android) && !os(WASI) import Foundation #if canImport(FoundationNetworking) import FoundationNetworking diff --git a/Sources/FuelingModel/APILocationService.swift b/Sources/FuelingModel/APILocationService.swift index ff7e71c..7ca04a3 100644 --- a/Sources/FuelingModel/APILocationService.swift +++ b/Sources/FuelingModel/APILocationService.swift @@ -73,9 +73,10 @@ public struct APILocationService: LocationService { // non-Darwin platforms), not `FoundationEssentials` — the top-level import // above prefers the lean subset whenever it's importable, so this extension // imports what it actually needs directly rather than relying on that. -// Excluded on Android, where the transport is a JNI-callback client instead -// (see `FuelingAndroid`) and `URLSession: HTTPClient` does not exist. -#if canImport(Foundation) && !os(Android) +// Excluded on Android and wasm, where the transport is a JNI-callback client +// (see `FuelingAndroid`) / a JavaScriptKit `fetch` client (see `Web/`) and +// `URLSession: HTTPClient` does not exist. +#if canImport(Foundation) && !os(Android) && !os(WASI) #if canImport(FoundationNetworking) import FoundationNetworking #endif diff --git a/Sources/FuelingModel/InMemoryStore.swift b/Sources/FuelingModel/InMemoryStore.swift new file mode 100644 index 0000000..e8bb91d --- /dev/null +++ b/Sources/FuelingModel/InMemoryStore.swift @@ -0,0 +1,162 @@ +// +// InMemoryStore.swift +// FuelingModel +// + +import CoreModel + +/// An in-memory `ModelStorage` **and** `ViewContext` backed by a plain +/// dictionary, using CoreModel's pure-Swift `FetchRequest.evaluate` engine. +/// +/// The cross-platform, dependency-free storage backend — used where neither +/// `CoreDataModel` (Darwin) nor the SQLite backend are available. In +/// particular the browser/wasm target: the SQLite package pulls in SQLCipher's +/// libtomcrypt C code, which doesn't compile for `wasm32-unknown-wasip1` +/// (it needs `_WASI_EMULATED_SIGNAL`), so `Store+SQLite` is off the table there. +/// +/// Single-object design: the *same* instance is handed to `Store` as both its +/// asynchronous `storage` and its synchronous `viewContext`, so UI reads +/// always observe prior writes. This is sound because the whole app runs on +/// the main actor — the class is `@MainActor`, so its `ModelStorage` async +/// methods and its `ViewContext` sync methods share one isolation domain and +/// one copy of the data. +@MainActor +public final class InMemoryStore { + + /// The schema this store validates entities against. + public let model: Model + + /// `entity name -> (object id -> object)`. + private var objects = [EntityName: [ObjectID: ModelData]]() + + /// Custom functions registered for predicate/sort evaluation. + private var functions = [String: DatabaseFunction]() + + /// Initialize an empty store validating entities against the given schema. + public init(model: Model) { + self.model = model + } + + // MARK: - Core (synchronous) + + private func read(_ entity: EntityName, for id: ObjectID) throws -> ModelData? { + try validate(entity) + return objects[entity]?[id] + } + + private func read(_ fetchRequest: FetchRequest) throws -> [ModelData] { + try validate(fetchRequest.entity) + let all = objects[fetchRequest.entity].map { Array($0.values) } ?? [] + return fetchRequest.evaluate(all, functions: functions) + } + + private func write(_ value: ModelData) throws { + try validate(value.entity) + // Upsert as a *partial* update, merging the provided keys over any + // existing object rather than replacing it wholesale. This matches the + // SQL/CoreData backends and preserves relationships a caller left out + // on purpose (e.g. a location refresh that deliberately omits + // `fuelProducts` so it never severs cached price links). + var merged = objects[value.entity]?[value.id] ?? ModelData(entity: value.entity, id: value.id) + merged.attributes.merge(value.attributes) { _, new in new } + merged.relationships.merge(value.relationships) { _, new in new } + // Fill in any schema-declared property still missing with a null/empty + // default, so a freshly-inserted object decodes the same way it would + // after a round trip through a database (unset attribute → null, unset + // to-many → empty, unset to-one → null) instead of throwing + // `keyNotFound`. Callers that map DTOs to `ModelData` routinely omit + // optional attributes (`lastViewed`, `brand`, …) and relationships they + // don't want to overwrite. + if let description = model[value.entity] { + for attribute in description.attributes where merged.attributes[attribute.id] == nil { + merged.attributes[attribute.id] = .null + } + for relationship in description.relationships where merged.relationships[relationship.id] == nil { + switch relationship.type { + case .toMany: + merged.relationships[relationship.id] = .toMany([]) + case .toOne: + merged.relationships[relationship.id] = .null + } + } + } + objects[value.entity, default: [:]][value.id] = merged + } + + private func remove(_ entity: EntityName, for id: ObjectID) throws { + try validate(entity) + objects[entity]?[id] = nil + } + + private func validate(_ entity: EntityName) throws { + guard model[entity] != nil else { + throw CoreModelError.invalidEntity(entity) + } + } +} + +// MARK: - ViewContext (synchronous, main-actor UI reads) + +extension InMemoryStore: ViewContext { + + public func fetch(_ entity: EntityName, for id: ObjectID) throws -> ModelData? { + try read(entity, for: id) + } + + public func fetch(_ fetchRequest: FetchRequest) throws -> [ModelData] { + try read(fetchRequest) + } + + public func fetchID(_ fetchRequest: FetchRequest) throws -> [ObjectID] { + try read(fetchRequest).map { $0.id } + } + + public func count(_ fetchRequest: FetchRequest) throws -> UInt { + try UInt(read(fetchRequest).count) + } +} + +// MARK: - ModelStorage (asynchronous writes + reads) + +extension InMemoryStore: ModelStorage { + + public func fetch(_ entity: EntityName, for id: ObjectID) async throws -> ModelData? { + try read(entity, for: id) + } + + public func fetch(_ fetchRequest: FetchRequest) async throws -> [ModelData] { + try read(fetchRequest) + } + + public func fetchID(_ fetchRequest: FetchRequest) async throws -> [ObjectID] { + try read(fetchRequest).map { $0.id } + } + + public func count(_ fetchRequest: FetchRequest) async throws -> UInt { + try UInt(read(fetchRequest).count) + } + + public func insert(_ value: ModelData) async throws { + try write(value) + } + + public func insert(_ values: [ModelData]) async throws { + for value in values { + try write(value) + } + } + + public func delete(_ entity: EntityName, for id: ObjectID) async throws { + try remove(entity, for: id) + } + + public func delete(_ entity: EntityName, for ids: [ObjectID]) async throws { + for id in ids { + try remove(entity, for: id) + } + } + + public func register(function: DatabaseFunction) async throws { + functions[function.name] = function + } +} diff --git a/Sources/FuelingModel/Store+InMemory.swift b/Sources/FuelingModel/Store+InMemory.swift new file mode 100644 index 0000000..cd1f9e9 --- /dev/null +++ b/Sources/FuelingModel/Store+InMemory.swift @@ -0,0 +1,38 @@ +// +// Store+InMemory.swift +// FuelingModel +// + +import CoreModel +import CoreFueling + +public extension Store { + + /// Build a store backed entirely by an in-memory dictionary. + /// + /// No filesystem, no C dependencies — the storage backend for platforms + /// without CoreData or SQLite (notably the browser/wasm target), and a + /// convenient one for previews and tests everywhere. + /// + /// The same ``InMemoryStore`` instance serves as both the store's + /// asynchronous `storage` and its synchronous `viewContext`, so UI reads + /// see prior writes immediately. + /// + /// - Parameters: + /// - model: The schema to validate entities against. Defaults to `.fueling`. + /// - locationService: Network transport, or `nil` for offline use. + /// - userLocation: Current user location, if known. + convenience init( + inMemory model: Model = .fueling, + locationService: (any LocationService)? = nil, + userLocation: LocationCoordinate? = nil + ) { + let store = InMemoryStore(model: model) + self.init( + storage: store, + viewContext: store, + locationService: locationService, + userLocation: userLocation + ) + } +} diff --git a/Sources/FuelingModel/Store+SQLite.swift b/Sources/FuelingModel/Store+SQLite.swift index 8af8062..ada0fd1 100644 --- a/Sources/FuelingModel/Store+SQLite.swift +++ b/Sources/FuelingModel/Store+SQLite.swift @@ -3,6 +3,11 @@ // FuelingModel // +// Gated to platforms where the SQLite backend is available (see +// `sqlitePlatforms` in Package.swift). On wasm the SQLite package's C code +// doesn't compile, so the modules aren't in the graph there and this whole +// file compiles away — the browser app uses `Store(inMemory:)` instead. +#if canImport(CoreModelSQLite) import CoreModel import CoreModelSQLite import SQLite @@ -31,3 +36,4 @@ public extension Store { ) } } +#endif diff --git a/Tests/FuelingTests/InMemoryStoreTests.swift b/Tests/FuelingTests/InMemoryStoreTests.swift new file mode 100644 index 0000000..c4dda2b --- /dev/null +++ b/Tests/FuelingTests/InMemoryStoreTests.swift @@ -0,0 +1,81 @@ +// +// InMemoryStoreTests.swift +// FuelingTests +// + +import Foundation +import Testing +import CoreModel +import CoreFueling +@testable import FuelingModel + +@MainActor +@Suite +struct InMemoryStoreTests { + + @Test + func fetchLocations() async throws { + let store = Store(inMemory: .fueling, locationService: .mock) + // download and persist all locations + let ids = try await store.locations() + #expect(ids.count == MockHTTPClient.locations.count) + // fetch back through storage — the location DTO mapping omits the + // `fuelProducts` relationship and the optional `lastViewed` attribute, + // so this exercises the store's default-fill on read (without it, + // `Location.init(from:)` throws `keyNotFound`). + let locations = try await store.storage.fetch(Location.self, search: nil) + #expect(locations.count == ids.count) + // filtered fetch + let filtered = try await store.storage.fetch(Location.self, search: "Seville") + #expect(filtered.count == 1) + #expect(filtered.first?.id == 15) + // read through the synchronous view context too + let viewed = try store.viewContext.fetch(Location.self, for: 15) + #expect(viewed?.name == "Seville Travel Center") + } + + @Test + func fetchFuelPrices() async throws { + let store = Store(inMemory: .fueling, locationService: .mock) + let location: Location.ID = 15 + try await store.locations(ids: [location]) + let products = try await store.fuelPrices(for: [location]) + #expect(products.isEmpty == false) + // The in-memory store doesn't maintain inverse relationships, so the + // parent location's `fuelProducts` isn't back-populated; query the + // `FuelProduct` entities by their own `location` field instead. + let fetched = try await store.storage + .fetch(FuelProduct.self) + .filter { $0.location == location } + #expect(Set(fetched.map(\.id)) == Set(products)) + } + + @Test + func partialUpdatePreservesRelationships() async throws { + let store = Store(inMemory: .fueling) + let id: Location.ID = 42 + // Seed a location that already references a fuel product. + try await store.storage.insert( + ModelData( + entity: Location.entityName, + id: ObjectID(id), + attributes: [PropertyKey(Location.CodingKeys.name): .string("Original")], + relationships: [PropertyKey(Location.CodingKeys.fuelProducts): .toMany([ObjectID("1")])] + ) + ) + // A refresh that only updates the name (and omits `fuelProducts`, as the + // location DTO mapping does) must not sever the existing link. + try await store.storage.insert( + ModelData( + entity: Location.entityName, + id: ObjectID(id), + attributes: [PropertyKey(Location.CodingKeys.name): .string("Renamed")] + ) + ) + // Inspect the raw `ModelData` (decoding would require all of the + // location's non-optional attributes, which this minimal seed omits). + let raw = try #require(try store.viewContext.fetch(Location.entityName, for: ObjectID(id))) + #expect(raw.attributes[PropertyKey(Location.CodingKeys.name)] == .string("Renamed")) + #expect(raw.relationships[PropertyKey(Location.CodingKeys.fuelProducts)] == .toMany([ObjectID("1")])) + } +} diff --git a/Web/.gitignore b/Web/.gitignore new file mode 100644 index 0000000..ebdee3c --- /dev/null +++ b/Web/.gitignore @@ -0,0 +1,7 @@ +# Swift +.build/ + +# Node / Vite +node_modules/ +dist/ +*.log diff --git a/Web/BrowserRuntime/.gitignore b/Web/BrowserRuntime/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/Web/BrowserRuntime/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/Web/BrowserRuntime/LICENSE.md b/Web/BrowserRuntime/LICENSE.md new file mode 100644 index 0000000..73d1e1a --- /dev/null +++ b/Web/BrowserRuntime/LICENSE.md @@ -0,0 +1,14 @@ +This package contains code under multiple licenses: + +1. ElementaryUI Browser Runtime (Apache-2.0) + Licensed under the Apache License, Version 2.0 + See: ../LICENSE + +2. JavaScriptKit Runtime (MIT) - Vendored in src/vendored/javascriptkit/ + Licensed under the MIT License + See: src/vendored/javascriptkit/LICENSE + +--- + +The bundled output (dist/) contains code from both sources and is subject +to both the Apache-2.0 and MIT licenses. diff --git a/Web/BrowserRuntime/README.md b/Web/BrowserRuntime/README.md new file mode 100644 index 0000000..e740be7 --- /dev/null +++ b/Web/BrowserRuntime/README.md @@ -0,0 +1,30 @@ +# ElementaryUI Browser Runtime + +Bundled JavaScriptKit + WASI bootstrap for running ElementaryUI WebAssembly applications in the browser. + +## What is this? + +This package provides JavaScript glue code to run ElementaryUI WebAssembly applications in the browser. + +> [!IMPORTANT] +> If you are not targeting an ElementaryUI Vite setup, you should use the JavaScriptKit `swift package js` plugin instead. + +- **JavaScriptKit Runtime** - Swift-to-JavaScript interop layer (vendored from [JavaScriptKit](https://github.com/swiftwasm/JavaScriptKit)) +- **WASI Bootstrap** - Minimal WASI setup for browser environments ([@bjorn3/browser_wasi_shim](https://github.com/bjorn3/browser_wasi_shim)) + +## Usage +```ts +import { runApplication } from "elementary-ui-browser-runtime"; + +await runApplication(async (imports) => { + const { instance } = await WebAssembly.instantiateStreaming( + fetch("./App.wasm"), + imports + ); + return instance; +}); +``` + +## License + +This package contains code under multiple licenses. See [LICENSE](LICENSE.md) for details. \ No newline at end of file diff --git a/Web/BrowserRuntime/package.json b/Web/BrowserRuntime/package.json new file mode 100644 index 0000000..f27fe75 --- /dev/null +++ b/Web/BrowserRuntime/package.json @@ -0,0 +1,13 @@ +{ + "name": "elementary-ui-browser-runtime", + "version": "0.1.0", + "description": "Bundled browser runtime for ElementaryUI (vendored, prebuilt dist)", + "type": "module", + "license": "Apache-2.0 AND MIT", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + } +} diff --git a/Web/BrowserRuntime/pnpm-lock.yaml b/Web/BrowserRuntime/pnpm-lock.yaml new file mode 100644 index 0000000..d7c95f2 --- /dev/null +++ b/Web/BrowserRuntime/pnpm-lock.yaml @@ -0,0 +1,605 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@bjorn3/browser_wasi_shim': + specifier: ~0.4.2 + version: 0.4.2 + tsdown: + specifier: ^0.21.7 + version: 0.21.7(typescript@6.0.2) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + +packages: + + '@babel/generator@8.0.0-rc.3': + resolution: {integrity: sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-string-parser@8.0.0-rc.3': + resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-validator-identifier@8.0.0-rc.3': + resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/parser@8.0.0-rc.3': + resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + '@babel/types@8.0.0-rc.3': + resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@bjorn3/browser_wasi_shim@0.4.2': + resolution: {integrity: sha512-/iHkCVUG3VbcbmEHn5iIUpIrh7a7WPiwZ3sHy4HZKZzBdSadwdddYDZAII2zBvQYV0Lfi8naZngPCN7WPHI/hA==} + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + ast-kit@3.0.0-beta.1: + resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==} + engines: {node: '>=20.19.0'} + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + dts-resolver@2.1.3: + resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} + engines: {node: '>=20.19.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + + hookable@6.1.0: + resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==} + + import-without-cache@0.2.5: + resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==} + engines: {node: '>=20.19.0'} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rolldown-plugin-dts@0.23.2: + resolution: {integrity: sha512-PbSqLawLgZBGcOGT3yqWBGn4cX+wh2nt5FuBGdcMHyOhoukmjbhYAl8NT9sE4U38Cm9tqLOIQeOrvzeayM0DLQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20260325.1' + rolldown: ^1.0.0-rc.12 + typescript: ^5.0.0 || ^6.0.0 + vue-tsc: ~3.2.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tsdown@0.21.7: + resolution: {integrity: sha512-ukKIxKQzngkWvOYJAyptudclkm4VQqbjq+9HF5K5qDO8GJsYtMh8gIRwicbnZEnvFPr6mquFwYAVZ8JKt3rY2g==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.21.7 + '@tsdown/exe': 0.21.7 + '@vitejs/devtools': '*' + publint: ^0.3.0 + typescript: ^5.0.0 || ^6.0.0 + unplugin-unused: ^0.5.0 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} + engines: {node: '>=14.17'} + hasBin: true + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + unrun@0.2.34: + resolution: {integrity: sha512-LyaghRBR++r7svhDK6tnDz2XaYHWdneBOA0jbS8wnRsHerI9MFljX4fIiTgbbNbEVzZ0C9P1OjWLLe1OqoaaEw==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + synckit: ^0.11.11 + peerDependenciesMeta: + synckit: + optional: true + +snapshots: + + '@babel/generator@8.0.0-rc.3': + dependencies: + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-string-parser@8.0.0-rc.3': {} + + '@babel/helper-validator-identifier@8.0.0-rc.3': {} + + '@babel/parser@8.0.0-rc.3': + dependencies: + '@babel/types': 8.0.0-rc.3 + + '@babel/types@8.0.0-rc.3': + dependencies: + '@babel/helper-string-parser': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 + + '@bjorn3/browser_wasi_shim@0.4.2': {} + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.122.0': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/estree@1.0.8': {} + + '@types/jsesc@2.5.1': {} + + ansis@4.2.0: {} + + ast-kit@3.0.0-beta.1: + dependencies: + '@babel/parser': 8.0.0-rc.3 + estree-walker: 3.0.3 + pathe: 2.0.3 + + birpc@4.0.0: {} + + cac@7.0.0: {} + + defu@6.1.4: {} + + dts-resolver@2.1.3: {} + + empathic@2.0.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + + hookable@6.1.0: {} + + import-without-cache@0.2.5: {} + + jsesc@3.1.0: {} + + obug@2.1.1: {} + + pathe@2.0.3: {} + + picomatch@4.0.4: {} + + quansync@1.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + rolldown-plugin-dts@0.23.2(rolldown@1.0.0-rc.12)(typescript@6.0.2): + dependencies: + '@babel/generator': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + ast-kit: 3.0.0-beta.1 + birpc: 4.0.0 + dts-resolver: 2.1.3 + get-tsconfig: 4.13.7 + obug: 2.1.1 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.12 + optionalDependencies: + typescript: 6.0.2 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.0.0-rc.12: + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + + semver@7.7.4: {} + + tinyexec@1.0.4: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tree-kill@1.2.2: {} + + tsdown@0.21.7(typescript@6.0.2): + dependencies: + ansis: 4.2.0 + cac: 7.0.0 + defu: 6.1.4 + empathic: 2.0.0 + hookable: 6.1.0 + import-without-cache: 0.2.5 + obug: 2.1.1 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.12 + rolldown-plugin-dts: 0.23.2(rolldown@1.0.0-rc.12)(typescript@6.0.2) + semver: 7.7.4 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + unrun: 0.2.34 + optionalDependencies: + typescript: 6.0.2 + transitivePeerDependencies: + - '@ts-macro/tsc' + - '@typescript/native-preview' + - oxc-resolver + - synckit + - vue-tsc + + tslib@2.8.1: + optional: true + + typescript@6.0.2: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unrun@0.2.34: + dependencies: + rolldown: 1.0.0-rc.12 diff --git a/Web/BrowserRuntime/src/bridgejs-shims.ts b/Web/BrowserRuntime/src/bridgejs-shims.ts new file mode 100644 index 0000000..d69716d --- /dev/null +++ b/Web/BrowserRuntime/src/bridgejs-shims.ts @@ -0,0 +1,40 @@ +export function createBridgeJSStubs() { + const unexpectedBjsCall = () => { + throw new Error("Unexpected call to BridgeJS function"); + }; + return { + swift_js_return_string: unexpectedBjsCall, + swift_js_init_memory: unexpectedBjsCall, + swift_js_make_js_string: unexpectedBjsCall, + swift_js_init_memory_with_result: unexpectedBjsCall, + swift_js_throw: unexpectedBjsCall, + swift_js_retain: unexpectedBjsCall, + swift_js_release: unexpectedBjsCall, + swift_js_push_i32: unexpectedBjsCall, + swift_js_push_f32: unexpectedBjsCall, + swift_js_push_f64: unexpectedBjsCall, + swift_js_push_string: unexpectedBjsCall, + swift_js_pop_i32: unexpectedBjsCall, + swift_js_pop_f32: unexpectedBjsCall, + swift_js_pop_f64: unexpectedBjsCall, + swift_js_return_optional_bool: unexpectedBjsCall, + swift_js_return_optional_int: unexpectedBjsCall, + swift_js_return_optional_string: unexpectedBjsCall, + swift_js_return_optional_double: unexpectedBjsCall, + swift_js_return_optional_float: unexpectedBjsCall, + swift_js_return_optional_heap_object: unexpectedBjsCall, + swift_js_return_optional_object: unexpectedBjsCall, + swift_js_get_optional_int_presence: unexpectedBjsCall, + swift_js_get_optional_int_value: unexpectedBjsCall, + swift_js_get_optional_string: unexpectedBjsCall, + swift_js_get_optional_float_presence: unexpectedBjsCall, + swift_js_get_optional_float_value: unexpectedBjsCall, + swift_js_get_optional_double_presence: unexpectedBjsCall, + swift_js_get_optional_double_value: unexpectedBjsCall, + swift_js_get_optional_heap_object_pointer: unexpectedBjsCall, + swift_js_push_pointer: unexpectedBjsCall, + swift_js_pop_pointer: unexpectedBjsCall, + swift_js_struct_cleanup: unexpectedBjsCall, + swift_js_closure_unregister: unexpectedBjsCall, + }; +} diff --git a/Web/BrowserRuntime/src/generated/bridge-js.d.ts b/Web/BrowserRuntime/src/generated/bridge-js.d.ts new file mode 100644 index 0000000..62988bf --- /dev/null +++ b/Web/BrowserRuntime/src/generated/bridge-js.d.ts @@ -0,0 +1,136 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export const JSCompositeOperationValues: { + readonly Replace: "replace"; + readonly Add: "add"; + readonly Accumulate: "accumulate"; +}; +export type JSCompositeOperationTag = typeof JSCompositeOperationValues[keyof typeof JSCompositeOperationValues]; + +export const JSFillModeValues: { + readonly None: "none"; + readonly Forwards: "forwards"; + readonly Backwards: "backwards"; + readonly Both: "both"; + readonly Auto: "auto"; +}; +export type JSFillModeTag = typeof JSFillModeValues[keyof typeof JSFillModeValues]; + +export interface JSKeyframeEffectOptions { + duration: number; + fill: JSFillModeTag; + composite: JSCompositeOperationTag; +} +export interface JSAnimationTiming { + duration: number; +} +export type JSCompositeOperationObject = typeof JSCompositeOperationValues; + +export type JSFillModeObject = typeof JSFillModeValues; + +export interface JSDocument { + createElement(tagName: string): JSElement; + createElementNS(namespaceURI: string, qualifiedName: string): JSElement; + createTextNode(text: string): JSNode; + querySelector(selector: string): JSElement; + addEventListener(type: string, listener: (arg0: JSEvent) => void): void; + removeEventListener(type: string, listener: (arg0: JSEvent) => void): void; + readonly body: JSElement; +} +export interface JSWindow { + getComputedStyle(element: JSElement): JSCSSStyleDeclaration; + readonly scrollX: number; + readonly scrollY: number; +} +export interface JSPerformance { + now(): number; +} +export interface JSNode { + textContent: string; +} +export interface JSElement { + setAttribute(name: string, value: string): void; + removeAttribute(name: string): void; + appendChild(child: JSNode): void; + removeChild(child: JSNode): void; + insertBefore(newChild: JSNode, refChild: JSNode): void; + replaceChildren(): void; + getBoundingClientRect(): JSDOMRect; + addEventListener(type: string, listener: (arg0: JSEvent) => void): void; + removeEventListener(type: string, listener: (arg0: JSEvent) => void): void; + focus(): void; + blur(): void; + animate(keyframes: any, options: JSKeyframeEffectOptions): JSAnimation; + readonly style: JSCSSStyleDeclaration; + readonly offsetParent: JSElement; +} +export interface JSCSSStyleDeclaration { + getPropertyValue(name: string): string; + setProperty(name: string, value: string): void; + removeProperty(name: string): void; +} +export interface JSDOMRect { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +} +export interface JSAnimation { + persist(): void; + pause(): void; + play(): void; + cancel(): void; + readonly effect: JSAnimationEffect; + currentTime: number; + onfinish: () => void; +} +export interface JSAnimationEffect { + setKeyframes(keyframes: any): void; + updateTiming(timing: JSAnimationTiming): void; +} +export interface JSEvent { + readonly type: string; + readonly target: any; +} +export interface JSKeyboardEvent { + readonly key: string; +} +export interface JSMouseEvent { + readonly altKey: boolean; + readonly button: number; + readonly buttons: number; + readonly clientX: number; + readonly clientY: number; + readonly ctrlKey: boolean; + readonly metaKey: boolean; + readonly movementX: number; + readonly movementY: number; + readonly offsetX: number; + readonly offsetY: number; + readonly pageX: number; + readonly pageY: number; + readonly screenX: number; + readonly screenY: number; + readonly shiftKey: boolean; +} +export interface JSInputEvent { + readonly data: string | null; + readonly target: any; +} +export type Exports = { + JSCompositeOperation: JSCompositeOperationObject + JSFillMode: JSFillModeObject +} +export type Imports = { +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; diff --git a/Web/BrowserRuntime/src/generated/bridge-js.js b/Web/BrowserRuntime/src/generated/bridge-js.js new file mode 100644 index 0000000..3122ecb --- /dev/null +++ b/Web/BrowserRuntime/src/generated/bridge-js.js @@ -0,0 +1,979 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export const JSCompositeOperationValues = { + Replace: "replace", + Add: "add", + Accumulate: "accumulate", +}; + +export const JSFillModeValues = { + None: "none", + Forwards: "forwards", + Backwards: "backwards", + Both: "both", + Auto: "auto", +}; + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + let decodeString; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + let tmpRetOptionalBool; + let tmpRetOptionalInt; + let tmpRetOptionalFloat; + let tmpRetOptionalDouble; + let tmpRetOptionalHeapObject; + let strStack = []; + let i32Stack = []; + let i64Stack = []; + let f32Stack = []; + let f64Stack = []; + let ptrStack = []; + let taStack = []; + const enumHelpers = {}; + const structHelpers = {}; + + let _exports = null; + let bjs = null; + const swiftClosureRegistry = (typeof FinalizationRegistry === "undefined") ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry((state) => { + if (state.unregistered) { return; } + instance?.exports?.bjs_release_swift_closure(state.pointer); + }); + const makeClosure = (pointer, file, line, func) => { + const state = { pointer, file, line, unregistered: false }; + const real = (...args) => { + if (state.unregistered) { + const bytes = new Uint8Array(memory.buffer, state.file >>> 0); + let length = 0; + while (bytes[length] !== 0) { length += 1; } + const fileID = decodeString(state.file, length); + throw new Error(`Attempted to call a released JSTypedClosure created at ${fileID}:${state.line}`); + } + return func(...args); + }; + real.__unregister = () => { + if (state.unregistered) { return; } + state.unregistered = true; + swiftClosureRegistry.unregister(state); + }; + swiftClosureRegistry.register(real, state, state); + return swift.memory.retain(real); + }; + + const __bjs_createJSKeyframeEffectOptionsHelpers = () => ({ + lower: (value) => { + i32Stack.push((value.duration | 0)); + const bytes = textEncoder.encode(value.fill); + const id = swift.memory.retain(bytes); + i32Stack.push(bytes.length); + i32Stack.push(id); + const bytes1 = textEncoder.encode(value.composite); + const id1 = swift.memory.retain(bytes1); + i32Stack.push(bytes1.length); + i32Stack.push(id1); + }, + lift: () => { + const rawValue = strStack.pop(); + const rawValue1 = strStack.pop(); + const int = i32Stack.pop(); + return { duration: int, fill: rawValue1, composite: rawValue }; + } + }); + const __bjs_createJSAnimationTimingHelpers = () => ({ + lower: (value) => { + i32Stack.push((value.duration | 0)); + }, + lift: () => { + const int = i32Stack.pop(); + return { duration: int }; + } + }); + + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + bjs = {}; + importObject["bjs"] = bjs; + bjs["swift_js_return_string"] = function(ptr, len) { + tmpRetString = decodeString(ptr, len); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + swift.memory.release(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr >>> 0); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(ptr, len) { + return swift.memory.retain(decodeString(ptr, len)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr >>> 0, len >>> 0); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + bjs["swift_js_push_i32"] = function(v) { + i32Stack.push(v | 0); + } + bjs["swift_js_push_f32"] = function(v) { + f32Stack.push(Math.fround(v)); + } + bjs["swift_js_push_f64"] = function(v) { + f64Stack.push(v); + } + bjs["swift_js_push_string"] = function(ptr, len) { + const value = decodeString(ptr, len); + strStack.push(value); + } + bjs["swift_js_pop_i32"] = function() { + return i32Stack.pop(); + } + bjs["swift_js_pop_f32"] = function() { + return f32Stack.pop(); + } + bjs["swift_js_pop_f64"] = function() { + return f64Stack.pop(); + } + bjs["swift_js_push_pointer"] = function(pointer) { + ptrStack.push(pointer); + } + bjs["swift_js_pop_pointer"] = function() { + return ptrStack.pop(); + } + bjs["swift_js_push_i64"] = function(v) { + i64Stack.push(v); + } + bjs["swift_js_pop_i64"] = function() { + return i64Stack.pop(); + } + const taCtors = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array]; + bjs["swift_js_push_typed_array"] = function(kind, ptr, count) { + const Ctor = taCtors[kind]; + const byteLen = count * Ctor.BYTES_PER_ELEMENT; + const copy = memory.buffer.slice(ptr, ptr + byteLen); + taStack.push(Array.from(new Ctor(copy))); + } + bjs["swift_js_struct_lower_JSKeyframeEffectOptions"] = function(objectId) { + structHelpers.JSKeyframeEffectOptions.lower(swift.memory.getObject(objectId)); + } + bjs["swift_js_struct_lift_JSKeyframeEffectOptions"] = function() { + const value = structHelpers.JSKeyframeEffectOptions.lift(); + return swift.memory.retain(value); + } + bjs["swift_js_struct_lower_JSAnimationTiming"] = function(objectId) { + structHelpers.JSAnimationTiming.lower(swift.memory.getObject(objectId)); + } + bjs["swift_js_struct_lift_JSAnimationTiming"] = function() { + const value = structHelpers.JSAnimationTiming.lift(); + return swift.memory.retain(value); + } + const __bjs_promiseSettlers = Symbol("JavaScriptKit.promiseSettlers"); + bjs["swift_js_make_promise"] = function() { + let resolve, reject; + const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); + promise[__bjs_promiseSettlers] = { resolve, reject }; + return swift.memory.retain(promise); + } + bjs["swift_js_return_optional_bool"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalBool = null; + } else { + tmpRetOptionalBool = value !== 0; + } + } + bjs["swift_js_return_optional_int"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalInt = null; + } else { + tmpRetOptionalInt = value | 0; + } + } + bjs["swift_js_return_optional_float"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalFloat = null; + } else { + tmpRetOptionalFloat = Math.fround(value); + } + } + bjs["swift_js_return_optional_double"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalDouble = null; + } else { + tmpRetOptionalDouble = value; + } + } + bjs["swift_js_return_optional_string"] = function(isSome, ptr, len) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = decodeString(ptr, len); + } + } + bjs["swift_js_return_optional_object"] = function(isSome, objectId) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = swift.memory.getObject(objectId); + } + } + bjs["swift_js_return_optional_heap_object"] = function(isSome, pointer) { + if (isSome === 0) { + tmpRetOptionalHeapObject = null; + } else { + tmpRetOptionalHeapObject = pointer; + } + } + bjs["swift_js_get_optional_int_presence"] = function() { + return tmpRetOptionalInt != null ? 1 : 0; + } + bjs["swift_js_get_optional_int_value"] = function() { + const value = tmpRetOptionalInt; + tmpRetOptionalInt = undefined; + return value; + } + bjs["swift_js_get_optional_string"] = function() { + const str = tmpRetString; + tmpRetString = undefined; + if (str == null) { + return -1; + } else { + const bytes = textEncoder.encode(str); + tmpRetBytes = bytes; + return bytes.length; + } + } + bjs["swift_js_get_optional_float_presence"] = function() { + return tmpRetOptionalFloat != null ? 1 : 0; + } + bjs["swift_js_get_optional_float_value"] = function() { + const value = tmpRetOptionalFloat; + tmpRetOptionalFloat = undefined; + return value; + } + bjs["swift_js_get_optional_double_presence"] = function() { + return tmpRetOptionalDouble != null ? 1 : 0; + } + bjs["swift_js_get_optional_double_value"] = function() { + const value = tmpRetOptionalDouble; + tmpRetOptionalDouble = undefined; + return value; + } + bjs["swift_js_get_optional_heap_object_pointer"] = function() { + const pointer = tmpRetOptionalHeapObject; + tmpRetOptionalHeapObject = undefined; + return pointer || 0; + } + bjs["swift_js_closure_unregister"] = function(funcRef) {} + bjs["swift_js_closure_unregister"] = function(funcRef) { + const func = swift.memory.getObject(funcRef); + func.__unregister(); + } + bjs["invoke_js_callback_BrowserInterop_14BrowserInterop7JSEventC_y"] = function(callbackId, param0) { + try { + const callback = swift.memory.getObject(callbackId); + callback(swift.memory.getObject(param0)); + } catch (error) { + setException(error); + } + } + bjs["make_swift_closure_BrowserInterop_14BrowserInterop7JSEventC_y"] = function(boxPtr, file, line) { + const lower_closure_BrowserInterop_14BrowserInterop7JSEventC_y = function(param0) { + instance.exports.invoke_swift_closure_BrowserInterop_14BrowserInterop7JSEventC_y(boxPtr, swift.memory.retain(param0)); + if (tmpRetException) { + const error = swift.memory.getObject(tmpRetException); + swift.memory.release(tmpRetException); + tmpRetException = undefined; + throw error; + } + }; + return makeClosure(boxPtr, file, line, lower_closure_BrowserInterop_14BrowserInterop7JSEventC_y); + } + bjs["invoke_js_callback_BrowserInterop_14BrowserInteropSd_y"] = function(callbackId, param0) { + try { + const callback = swift.memory.getObject(callbackId); + callback(param0); + } catch (error) { + setException(error); + } + } + bjs["make_swift_closure_BrowserInterop_14BrowserInteropSd_y"] = function(boxPtr, file, line) { + const lower_closure_BrowserInterop_14BrowserInteropSd_y = function(param0) { + instance.exports.invoke_swift_closure_BrowserInterop_14BrowserInteropSd_y(boxPtr, param0); + if (tmpRetException) { + const error = swift.memory.getObject(tmpRetException); + swift.memory.release(tmpRetException); + tmpRetException = undefined; + throw error; + } + }; + return makeClosure(boxPtr, file, line, lower_closure_BrowserInterop_14BrowserInteropSd_y); + } + bjs["invoke_js_callback_BrowserInterop_14BrowserInteropy_y"] = function(callbackId) { + try { + const callback = swift.memory.getObject(callbackId); + callback(); + } catch (error) { + setException(error); + } + } + bjs["make_swift_closure_BrowserInterop_14BrowserInteropy_y"] = function(boxPtr, file, line) { + const lower_closure_BrowserInterop_14BrowserInteropy_y = function() { + instance.exports.invoke_swift_closure_BrowserInterop_14BrowserInteropy_y(boxPtr); + if (tmpRetException) { + const error = swift.memory.getObject(tmpRetException); + swift.memory.release(tmpRetException); + tmpRetException = undefined; + throw error; + } + }; + return makeClosure(boxPtr, file, line, lower_closure_BrowserInterop_14BrowserInteropy_y); + } + const BrowserInterop = importObject["BrowserInterop"] = importObject["BrowserInterop"] || {}; + BrowserInterop["bjs_JSDocument_body_get"] = function bjs_JSDocument_body_get(self) { + try { + let ret = swift.memory.getObject(self).body; + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSDocument_createElement"] = function bjs_JSDocument_createElement(self, tagNameBytes, tagNameCount) { + try { + const string = decodeString(tagNameBytes, tagNameCount); + let ret = swift.memory.getObject(self).createElement(string); + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSDocument_createElementNS"] = function bjs_JSDocument_createElementNS(self, namespaceURIBytes, namespaceURICount, qualifiedNameBytes, qualifiedNameCount) { + try { + const string = decodeString(namespaceURIBytes, namespaceURICount); + const string1 = decodeString(qualifiedNameBytes, qualifiedNameCount); + let ret = swift.memory.getObject(self).createElementNS(string, string1); + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSDocument_createTextNode"] = function bjs_JSDocument_createTextNode(self, textBytes, textCount) { + try { + const string = decodeString(textBytes, textCount); + let ret = swift.memory.getObject(self).createTextNode(string); + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSDocument_querySelector"] = function bjs_JSDocument_querySelector(self, selectorBytes, selectorCount) { + try { + const string = decodeString(selectorBytes, selectorCount); + let ret = swift.memory.getObject(self).querySelector(string); + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSDocument_addEventListener"] = function bjs_JSDocument_addEventListener(self, typeBytes, typeCount, listener) { + try { + const string = decodeString(typeBytes, typeCount); + swift.memory.getObject(self).addEventListener(string, swift.memory.getObject(listener)); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSDocument_removeEventListener"] = function bjs_JSDocument_removeEventListener(self, typeBytes, typeCount, listener) { + try { + const string = decodeString(typeBytes, typeCount); + swift.memory.getObject(self).removeEventListener(string, swift.memory.getObject(listener)); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSWindow_scrollX_get"] = function bjs_JSWindow_scrollX_get(self) { + try { + let ret = swift.memory.getObject(self).scrollX; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSWindow_scrollY_get"] = function bjs_JSWindow_scrollY_get(self) { + try { + let ret = swift.memory.getObject(self).scrollY; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSWindow_getComputedStyle"] = function bjs_JSWindow_getComputedStyle(self, element) { + try { + let ret = swift.memory.getObject(self).getComputedStyle(swift.memory.getObject(element)); + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSPerformance_now"] = function bjs_JSPerformance_now(self) { + try { + let ret = swift.memory.getObject(self).now(); + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSNode_textContent_set"] = function bjs_JSNode_textContent_set(self, newValueBytes, newValueCount) { + try { + const string = decodeString(newValueBytes, newValueCount); + swift.memory.getObject(self).textContent = string; + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSElement_style_get"] = function bjs_JSElement_style_get(self) { + try { + let ret = swift.memory.getObject(self).style; + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSElement_offsetParent_get"] = function bjs_JSElement_offsetParent_get(self) { + try { + let ret = swift.memory.getObject(self).offsetParent; + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSElement_setAttribute"] = function bjs_JSElement_setAttribute(self, nameBytes, nameCount, valueBytes, valueCount) { + try { + const string = decodeString(nameBytes, nameCount); + const string1 = decodeString(valueBytes, valueCount); + swift.memory.getObject(self).setAttribute(string, string1); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSElement_removeAttribute"] = function bjs_JSElement_removeAttribute(self, nameBytes, nameCount) { + try { + const string = decodeString(nameBytes, nameCount); + swift.memory.getObject(self).removeAttribute(string); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSElement_appendChild"] = function bjs_JSElement_appendChild(self, child) { + try { + swift.memory.getObject(self).appendChild(swift.memory.getObject(child)); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSElement_removeChild"] = function bjs_JSElement_removeChild(self, child) { + try { + swift.memory.getObject(self).removeChild(swift.memory.getObject(child)); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSElement_insertBefore"] = function bjs_JSElement_insertBefore(self, newChild, refChild) { + try { + swift.memory.getObject(self).insertBefore(swift.memory.getObject(newChild), swift.memory.getObject(refChild)); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSElement_replaceChildren"] = function bjs_JSElement_replaceChildren(self) { + try { + swift.memory.getObject(self).replaceChildren(); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSElement_getBoundingClientRect"] = function bjs_JSElement_getBoundingClientRect(self) { + try { + let ret = swift.memory.getObject(self).getBoundingClientRect(); + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSElement_addEventListener"] = function bjs_JSElement_addEventListener(self, typeBytes, typeCount, listener) { + try { + const string = decodeString(typeBytes, typeCount); + swift.memory.getObject(self).addEventListener(string, swift.memory.getObject(listener)); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSElement_removeEventListener"] = function bjs_JSElement_removeEventListener(self, typeBytes, typeCount, listener) { + try { + const string = decodeString(typeBytes, typeCount); + swift.memory.getObject(self).removeEventListener(string, swift.memory.getObject(listener)); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSElement_focus"] = function bjs_JSElement_focus(self) { + try { + swift.memory.getObject(self).focus(); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSElement_blur"] = function bjs_JSElement_blur(self) { + try { + swift.memory.getObject(self).blur(); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSElement_animate"] = function bjs_JSElement_animate(self, keyframes, options) { + try { + const value = swift.memory.getObject(options); + swift.memory.release(options); + let ret = swift.memory.getObject(self).animate(swift.memory.getObject(keyframes), value); + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSCSSStyleDeclaration_getPropertyValue"] = function bjs_JSCSSStyleDeclaration_getPropertyValue(self, nameBytes, nameCount) { + try { + const string = decodeString(nameBytes, nameCount); + let ret = swift.memory.getObject(self).getPropertyValue(string); + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSCSSStyleDeclaration_setProperty"] = function bjs_JSCSSStyleDeclaration_setProperty(self, nameBytes, nameCount, valueBytes, valueCount) { + try { + const string = decodeString(nameBytes, nameCount); + const string1 = decodeString(valueBytes, valueCount); + swift.memory.getObject(self).setProperty(string, string1); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSCSSStyleDeclaration_removeProperty"] = function bjs_JSCSSStyleDeclaration_removeProperty(self, nameBytes, nameCount) { + try { + const string = decodeString(nameBytes, nameCount); + swift.memory.getObject(self).removeProperty(string); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSDOMRect_x_get"] = function bjs_JSDOMRect_x_get(self) { + try { + let ret = swift.memory.getObject(self).x; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSDOMRect_y_get"] = function bjs_JSDOMRect_y_get(self) { + try { + let ret = swift.memory.getObject(self).y; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSDOMRect_width_get"] = function bjs_JSDOMRect_width_get(self) { + try { + let ret = swift.memory.getObject(self).width; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSDOMRect_height_get"] = function bjs_JSDOMRect_height_get(self) { + try { + let ret = swift.memory.getObject(self).height; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSAnimation_effect_get"] = function bjs_JSAnimation_effect_get(self) { + try { + let ret = swift.memory.getObject(self).effect; + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSAnimation_currentTime_set"] = function bjs_JSAnimation_currentTime_set(self, newValue) { + try { + swift.memory.getObject(self).currentTime = newValue; + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSAnimation_onfinish_set"] = function bjs_JSAnimation_onfinish_set(self, newValue) { + try { + swift.memory.getObject(self).onfinish = swift.memory.getObject(newValue); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSAnimation_persist"] = function bjs_JSAnimation_persist(self) { + try { + swift.memory.getObject(self).persist(); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSAnimation_pause"] = function bjs_JSAnimation_pause(self) { + try { + swift.memory.getObject(self).pause(); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSAnimation_play"] = function bjs_JSAnimation_play(self) { + try { + swift.memory.getObject(self).play(); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSAnimation_cancel"] = function bjs_JSAnimation_cancel(self) { + try { + swift.memory.getObject(self).cancel(); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSAnimationEffect_setKeyframes"] = function bjs_JSAnimationEffect_setKeyframes(self, keyframes) { + try { + swift.memory.getObject(self).setKeyframes(swift.memory.getObject(keyframes)); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSAnimationEffect_updateTiming"] = function bjs_JSAnimationEffect_updateTiming(self, timing) { + try { + const value = swift.memory.getObject(timing); + swift.memory.release(timing); + swift.memory.getObject(self).updateTiming(value); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSEvent_type_get"] = function bjs_JSEvent_type_get(self) { + try { + let ret = swift.memory.getObject(self).type; + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSEvent_target_get"] = function bjs_JSEvent_target_get(self) { + try { + let ret = swift.memory.getObject(self).target; + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSKeyboardEvent_key_get"] = function bjs_JSKeyboardEvent_key_get(self) { + try { + let ret = swift.memory.getObject(self).key; + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSMouseEvent_altKey_get"] = function bjs_JSMouseEvent_altKey_get(self) { + try { + let ret = swift.memory.getObject(self).altKey; + return ret ? 1 : 0; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_button_get"] = function bjs_JSMouseEvent_button_get(self) { + try { + let ret = swift.memory.getObject(self).button; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_buttons_get"] = function bjs_JSMouseEvent_buttons_get(self) { + try { + let ret = swift.memory.getObject(self).buttons; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_clientX_get"] = function bjs_JSMouseEvent_clientX_get(self) { + try { + let ret = swift.memory.getObject(self).clientX; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_clientY_get"] = function bjs_JSMouseEvent_clientY_get(self) { + try { + let ret = swift.memory.getObject(self).clientY; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_ctrlKey_get"] = function bjs_JSMouseEvent_ctrlKey_get(self) { + try { + let ret = swift.memory.getObject(self).ctrlKey; + return ret ? 1 : 0; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_metaKey_get"] = function bjs_JSMouseEvent_metaKey_get(self) { + try { + let ret = swift.memory.getObject(self).metaKey; + return ret ? 1 : 0; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_movementX_get"] = function bjs_JSMouseEvent_movementX_get(self) { + try { + let ret = swift.memory.getObject(self).movementX; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_movementY_get"] = function bjs_JSMouseEvent_movementY_get(self) { + try { + let ret = swift.memory.getObject(self).movementY; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_offsetX_get"] = function bjs_JSMouseEvent_offsetX_get(self) { + try { + let ret = swift.memory.getObject(self).offsetX; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_offsetY_get"] = function bjs_JSMouseEvent_offsetY_get(self) { + try { + let ret = swift.memory.getObject(self).offsetY; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_pageX_get"] = function bjs_JSMouseEvent_pageX_get(self) { + try { + let ret = swift.memory.getObject(self).pageX; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_pageY_get"] = function bjs_JSMouseEvent_pageY_get(self) { + try { + let ret = swift.memory.getObject(self).pageY; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_screenX_get"] = function bjs_JSMouseEvent_screenX_get(self) { + try { + let ret = swift.memory.getObject(self).screenX; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_screenY_get"] = function bjs_JSMouseEvent_screenY_get(self) { + try { + let ret = swift.memory.getObject(self).screenY; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSMouseEvent_shiftKey_get"] = function bjs_JSMouseEvent_shiftKey_get(self) { + try { + let ret = swift.memory.getObject(self).shiftKey; + return ret ? 1 : 0; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_JSInputEvent_data_get"] = function bjs_JSInputEvent_data_get(self) { + try { + let ret = swift.memory.getObject(self).data; + const isSome = ret != null; + tmpRetString = isSome ? ret : null; + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_JSInputEvent_target_get"] = function bjs_JSInputEvent_target_get(self) { + try { + let ret = swift.memory.getObject(self).target; + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_window_get"] = function bjs_window_get() { + try { + let ret = globalThis.window; + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_document_get"] = function bjs_document_get() { + try { + let ret = globalThis.document; + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_performance_get"] = function bjs_performance_get() { + try { + let ret = globalThis.performance; + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_requestAnimationFrame"] = function bjs_requestAnimationFrame(callback) { + try { + let ret = globalThis.requestAnimationFrame(swift.memory.getObject(callback)); + return ret; + } catch (error) { + setException(error); + return 0 + } + } + BrowserInterop["bjs_cancelAnimationFrame"] = function bjs_cancelAnimationFrame(handle) { + try { + globalThis.cancelAnimationFrame(handle); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_queueMicrotask"] = function bjs_queueMicrotask(callback) { + try { + globalThis.queueMicrotask(swift.memory.getObject(callback)); + } catch (error) { + setException(error); + } + } + BrowserInterop["bjs_setTimeout"] = function bjs_setTimeout(callback, timeout) { + try { + globalThis.setTimeout(swift.memory.getObject(callback), timeout); + } catch (error) { + setException(error); + } + } + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + + decodeString = (ptr, len) => { const bytes = new Uint8Array(memory.buffer, ptr >>> 0, len >>> 0); return textDecoder.decode(bytes); } + + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + const JSKeyframeEffectOptionsHelpers = __bjs_createJSKeyframeEffectOptionsHelpers(); + structHelpers.JSKeyframeEffectOptions = JSKeyframeEffectOptionsHelpers; + + const JSAnimationTimingHelpers = __bjs_createJSAnimationTimingHelpers(); + structHelpers.JSAnimationTiming = JSAnimationTimingHelpers; + + const exports = { + JSCompositeOperation: JSCompositeOperationValues, + JSFillMode: JSFillModeValues, + }; + _exports = exports; + return exports; + }, + } +} diff --git a/Web/BrowserRuntime/src/index.ts b/Web/BrowserRuntime/src/index.ts new file mode 100644 index 0000000..59a334c --- /dev/null +++ b/Web/BrowserRuntime/src/index.ts @@ -0,0 +1,42 @@ +import { createDefaultWASI } from "./wasi-shim"; +import { SwiftRuntime } from "./vendored/javascriptkit/index.mjs"; +import { createInstantiator } from "./generated/bridge-js"; + +type WasmInstanceInitializer = ( + importsObject?: WebAssembly.Imports +) => Promise; + +// TODO: offer more customization entry-points (ie: BYO WASI, BYO JavaScriptKit SwiftRuntime, figure out BridgeJS inclusion, ...) + +/** + * Runs an ElementaryUI application. + * + * This function bootstraps a JavaScriptKit SwiftRuntime and WASI shim, + * then runs the application by calling Swift's main entry point. + * + * @param initializer - A function that receives WebAssembly imports and returns a WebAssembly instance. + * @returns A promise that resolves when initialization is complete and the Swift application has started. + */ +export async function runApplication(initializer: WasmInstanceInitializer) { + const wasi = createDefaultWASI(); + const swiftRuntime = new SwiftRuntime(); + let instance: WebAssembly.Instance | null = null; + const instantiator = await createInstantiator({ + imports: {}, + }, swiftRuntime as any); + const importsObject: WebAssembly.Imports = { + javascript_kit: swiftRuntime.wasmImports, + wasi_snapshot_preview1: wasi.wasiImport, + }; + instantiator.addImports(importsObject); + + instance = await initializer(importsObject); + + swiftRuntime.setInstance(instance); + instantiator.setInstance(instance); + instantiator.createExports(instance); + // TODO: deal with this typing issue later + wasi.initialize(instance as any); + + swiftRuntime.main(); +} diff --git a/Web/BrowserRuntime/src/vendored/javascriptkit/LICENSE b/Web/BrowserRuntime/src/vendored/javascriptkit/LICENSE new file mode 100644 index 0000000..1f170b0 --- /dev/null +++ b/Web/BrowserRuntime/src/vendored/javascriptkit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Yuta Saito + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Web/BrowserRuntime/src/vendored/javascriptkit/index.d.ts b/Web/BrowserRuntime/src/vendored/javascriptkit/index.d.ts new file mode 100644 index 0000000..e4795be --- /dev/null +++ b/Web/BrowserRuntime/src/vendored/javascriptkit/index.d.ts @@ -0,0 +1,218 @@ +type ref = number; +type pointer = number; + +declare class JSObjectSpace { + private _slotByValue; + private _values; + private _stateBySlot; + private _freeSlotStack; + constructor(); + retain(value: any): number; + retainByRef(reference: ref): number; + release(reference: ref): void; + getObject(reference: ref): any; + private _getValidatedSlotState; +} + +/** + * A thread channel is a set of functions that are used to communicate between + * the main thread and the worker thread. The main thread and the worker thread + * can send messages to each other using these functions. + * + * @example + * ```javascript + * // worker.js + * const runtime = new SwiftRuntime({ + * threadChannel: { + * postMessageToMainThread: postMessage, + * listenMessageFromMainThread: (listener) => { + * self.onmessage = (event) => { + * listener(event.data); + * }; + * } + * } + * }); + * + * // main.js + * const worker = new Worker("worker.js"); + * const runtime = new SwiftRuntime({ + * threadChannel: { + * postMessageToWorkerThread: (tid, data) => { + * worker.postMessage(data); + * }, + * listenMessageFromWorkerThread: (tid, listener) => { + * worker.onmessage = (event) => { + listener(event.data); + * }; + * } + * } + * }); + * ``` + */ +type SwiftRuntimeThreadChannel = { + /** + * This function is used to send messages from the worker thread to the main thread. + * The message submitted by this function is expected to be listened by `listenMessageFromWorkerThread`. + * @param message The message to be sent to the main thread. + * @param transfer The array of objects to be transferred to the main thread. + */ + postMessageToMainThread: (message: WorkerToMainMessage, transfer: any[]) => void; + /** + * This function is expected to be set in the worker thread and should listen + * to messages from the main thread sent by `postMessageToWorkerThread`. + * @param listener The listener function to be called when a message is received from the main thread. + */ + listenMessageFromMainThread: (listener: (message: MainToWorkerMessage) => void) => void; +} | { + /** + * This function is expected to be set in the main thread. + * The message submitted by this function is expected to be listened by `listenMessageFromMainThread`. + * @param tid The thread ID of the worker thread. + * @param message The message to be sent to the worker thread. + * @param transfer The array of objects to be transferred to the worker thread. + */ + postMessageToWorkerThread: (tid: number, message: MainToWorkerMessage, transfer: any[]) => void; + /** + * This function is expected to be set in the main thread and should listen + * to messages sent by `postMessageToMainThread` from the worker thread. + * @param tid The thread ID of the worker thread. + * @param listener The listener function to be called when a message is received from the worker thread. + */ + listenMessageFromWorkerThread: (tid: number, listener: (message: WorkerToMainMessage) => void) => void; + /** + * This function is expected to be set in the main thread and called + * when the worker thread is terminated. + * @param tid The thread ID of the worker thread. + */ + terminateWorkerThread?: (tid: number) => void; +}; +declare class ITCInterface { + private memory; + constructor(memory: JSObjectSpace); + send(sendingObject: ref, transferringObjects: ref[], sendingContext: pointer): { + object: any; + sendingContext: pointer; + transfer: Transferable[]; + }; + sendObjects(sendingObjects: ref[], transferringObjects: ref[], sendingContext: pointer): { + object: any[]; + sendingContext: pointer; + transfer: Transferable[]; + }; + invokeRemoteJSObjectBody(invocationContext: pointer): { + object: undefined; + transfer: Transferable[]; + }; + release(objectRef: ref): { + object: undefined; + transfer: Transferable[]; + }; +} +type AllRequests> = { + [K in keyof Interface]: { + method: K; + parameters: Parameters; + }; +}; +type ITCRequest> = AllRequests[keyof AllRequests]; +type AllResponses> = { + [K in keyof Interface]: ReturnType; +}; +type ITCResponse> = AllResponses[keyof AllResponses]; +type RequestMessage = { + type: "request"; + data: { + /** The TID of the thread that sent the request */ + sourceTid: number; + /** The TID of the thread that should respond to the request */ + targetTid: number; + /** The context pointer of the request */ + context: pointer; + /** The request content */ + request: ITCRequest; + }; +}; +type SerializedError = { + isError: true; + value: Error; +} | { + isError: false; + value: unknown; +}; +type ResponseMessage = { + type: "response"; + data: { + /** The TID of the thread that sent the response */ + sourceTid: number; + /** The context pointer of the request */ + context: pointer; + /** The request method this response corresponds to */ + requestMethod: keyof ITCInterface; + /** The response content */ + response: { + ok: true; + value: ITCResponse; + } | { + ok: false; + error: SerializedError; + }; + }; +}; +type MainToWorkerMessage = { + type: "wake"; +} | RequestMessage | ResponseMessage; +type WorkerToMainMessage = { + type: "job"; + data: number; +} | RequestMessage | ResponseMessage; + +type SwiftRuntimeOptions = { + /** + * If `true`, the memory space of the WebAssembly instance can be shared + * between the main thread and the worker thread. + */ + sharedMemory?: boolean; + /** + * The thread channel is a set of functions that are used to communicate + * between the main thread and the worker thread. + */ + threadChannel?: SwiftRuntimeThreadChannel; +}; +declare class SwiftRuntime { + private _instance; + private readonly memory; + private _closureDeallocator; + private options; + private version; + private textDecoder; + private textEncoder; + /** The thread ID of the current thread. */ + private tid; + private getDataView; + private getUint8Array; + private wasmMemory; + UnsafeEventLoopYield: typeof UnsafeEventLoopYield; + constructor(options?: SwiftRuntimeOptions); + setInstance(instance: WebAssembly.Instance): void; + main(): void; + /** + * Start a new thread with the given `tid` and `startArg`, which + * is forwarded to the `wasi_thread_start` function. + * This function is expected to be called from the spawned Web Worker thread. + */ + startThread(tid: number, startArg: number): void; + private get instance(); + private get exports(); + private get closureDeallocator(); + private callHostFunction; + /** @deprecated Use `wasmImports` instead */ + importObjects: () => WebAssembly.ModuleImports; + get wasmImports(): WebAssembly.ModuleImports; + private postMessageToMainThread; + private postMessageToWorkerThread; +} +declare class UnsafeEventLoopYield extends Error { +} + +export { SwiftRuntime }; +export type { SwiftRuntimeOptions, SwiftRuntimeThreadChannel }; diff --git a/Web/BrowserRuntime/src/vendored/javascriptkit/index.mjs b/Web/BrowserRuntime/src/vendored/javascriptkit/index.mjs new file mode 100644 index 0000000..b0be54b --- /dev/null +++ b/Web/BrowserRuntime/src/vendored/javascriptkit/index.mjs @@ -0,0 +1,991 @@ +/// Memory lifetime of closures in Swift are managed by Swift side +class SwiftClosureDeallocator { + constructor(exports$1) { + if (typeof FinalizationRegistry === "undefined") { + throw new Error("The Swift part of JavaScriptKit was configured to require " + + "the availability of JavaScript WeakRefs. Please build " + + "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " + + "disable features that use WeakRefs."); + } + this.functionRegistry = new FinalizationRegistry((id) => { + exports$1.swjs_free_host_function(id); + }); + } + track(func, func_ref) { + this.functionRegistry.register(func, func_ref); + } +} + +function assertNever(x, message) { + throw new Error(message); +} +const MAIN_THREAD_TID = -1; + +const decode = (kind, payload1, payload2, objectSpace) => { + switch (kind) { + case 0 /* Kind.Boolean */: + switch (payload1) { + case 0: + return false; + case 1: + return true; + } + // falls through + case 2 /* Kind.Number */: + return payload2; + case 1 /* Kind.String */: + case 3 /* Kind.Object */: + case 7 /* Kind.Symbol */: + case 8 /* Kind.BigInt */: + return objectSpace.getObject(payload1); + case 4 /* Kind.Null */: + return null; + case 5 /* Kind.Undefined */: + return undefined; + default: + assertNever(kind, `JSValue Type kind "${kind}" is not supported`); + } +}; +// Note: +// `decodeValues` assumes that the size of RawJSValue is 16. +const decodeArray = (ptr, length, memory, objectSpace) => { + const basePtr = ptr >>> 0; + const count = length >>> 0; + // fast path for empty array + if (count === 0) { + return []; + } + let result = []; + for (let index = 0; index < count; index++) { + const base = basePtr + 16 * index; + const kind = memory.getUint32(base, true); + const payload1 = memory.getUint32(base + 4, true); + const payload2 = memory.getFloat64(base + 8, true); + result.push(decode(kind, payload1, payload2, objectSpace)); + } + return result; +}; +// A helper function to encode a RawJSValue into a pointers. +// Please prefer to use `writeAndReturnKindBits` to avoid unnecessary +// memory stores. +// This function should be used only when kind flag is stored in memory. +const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory, objectSpace) => { + const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory, objectSpace); + memory.setUint32(kind_ptr >>> 0, kind, true); +}; +const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory, objectSpace) => { + const exceptionBit = (is_exception ? 1 : 0) << 31; + const payload1Offset = payload1_ptr >>> 0; + const payload2Offset = payload2_ptr >>> 0; + if (value === null) { + return exceptionBit | 4 /* Kind.Null */; + } + const writeRef = (kind) => { + memory.setUint32(payload1Offset, objectSpace.retain(value), true); + return exceptionBit | kind; + }; + const type = typeof value; + switch (type) { + case "boolean": { + memory.setUint32(payload1Offset, value ? 1 : 0, true); + return exceptionBit | 0 /* Kind.Boolean */; + } + case "number": { + memory.setFloat64(payload2Offset, value, true); + return exceptionBit | 2 /* Kind.Number */; + } + case "string": { + return writeRef(1 /* Kind.String */); + } + case "undefined": { + return exceptionBit | 5 /* Kind.Undefined */; + } + case "object": { + return writeRef(3 /* Kind.Object */); + } + case "function": { + return writeRef(3 /* Kind.Object */); + } + case "symbol": { + return writeRef(7 /* Kind.Symbol */); + } + case "bigint": { + return writeRef(8 /* Kind.BigInt */); + } + default: + assertNever(type, `Type "${type}" is not supported yet`); + } + throw new Error("Unreachable"); +}; +function decodeObjectRefs(ptr, length, memory) { + const basePtr = ptr >>> 0; + const count = length >>> 0; + const result = new Array(count); + for (let i = 0; i < count; i++) { + result[i] = memory.getUint32(basePtr + 4 * i, true); + } + return result; +} + +class ITCInterface { + constructor(memory) { + this.memory = memory; + } + send(sendingObject, transferringObjects, sendingContext) { + const object = this.memory.getObject(sendingObject); + const transfer = transferringObjects.map((ref) => this.memory.getObject(ref)); + return { object, sendingContext, transfer }; + } + sendObjects(sendingObjects, transferringObjects, sendingContext) { + const objects = sendingObjects.map((ref) => this.memory.getObject(ref)); + const transfer = transferringObjects.map((ref) => this.memory.getObject(ref)); + return { object: objects, sendingContext, transfer }; + } + invokeRemoteJSObjectBody(invocationContext) { + return { object: undefined, transfer: [] }; + } + release(objectRef) { + this.memory.release(objectRef); + return { object: undefined, transfer: [] }; + } +} +class MessageBroker { + constructor(selfTid, threadChannel, handlers) { + this.selfTid = selfTid; + this.threadChannel = threadChannel; + this.handlers = handlers; + } + request(message) { + if (message.data.targetTid == this.selfTid) { + // The request is for the current thread + this.handlers.onRequest(message); + } + else if ("postMessageToWorkerThread" in this.threadChannel) { + // The request is for another worker thread sent from the main thread + this.threadChannel.postMessageToWorkerThread(message.data.targetTid, message, []); + } + else if ("postMessageToMainThread" in this.threadChannel) { + // The request is for other worker threads or the main thread sent from a worker thread + this.threadChannel.postMessageToMainThread(message, []); + } + else { + throw new Error("unreachable"); + } + } + reply(message) { + if (message.data.sourceTid == this.selfTid) { + // The response is for the current thread + this.handlers.onResponse(message); + return; + } + const transfer = message.data.response.ok + ? message.data.response.value.transfer + : []; + if ("postMessageToWorkerThread" in this.threadChannel) { + // The response is for another worker thread sent from the main thread + this.threadChannel.postMessageToWorkerThread(message.data.sourceTid, message, transfer); + } + else if ("postMessageToMainThread" in this.threadChannel) { + // The response is for other worker threads or the main thread sent from a worker thread + this.threadChannel.postMessageToMainThread(message, transfer); + } + else { + throw new Error("unreachable"); + } + } + onReceivingRequest(message) { + if (message.data.targetTid == this.selfTid) { + this.handlers.onRequest(message); + } + else if ("postMessageToWorkerThread" in this.threadChannel) { + // Receive a request from a worker thread to other worker on main thread. + // Proxy the request to the target worker thread. + this.threadChannel.postMessageToWorkerThread(message.data.targetTid, message, []); + } + else if ("postMessageToMainThread" in this.threadChannel) { + // A worker thread won't receive a request for other worker threads + throw new Error("unreachable"); + } + } + onReceivingResponse(message) { + if (message.data.sourceTid == this.selfTid) { + this.handlers.onResponse(message); + } + else if ("postMessageToWorkerThread" in this.threadChannel) { + // Receive a response from a worker thread to other worker on main thread. + // Proxy the response to the target worker thread. + const transfer = message.data.response.ok + ? message.data.response.value.transfer + : []; + this.threadChannel.postMessageToWorkerThread(message.data.sourceTid, message, transfer); + } + else if ("postMessageToMainThread" in this.threadChannel) { + // A worker thread won't receive a response for other worker threads + throw new Error("unreachable"); + } + } +} +function serializeError(error) { + if (error instanceof Error) { + return { + isError: true, + value: { + message: error.message, + name: error.name, + stack: error.stack, + }, + }; + } + return { isError: false, value: error }; +} +function deserializeError(error) { + if (error.isError) { + return Object.assign(new Error(error.value.message), error.value); + } + return error.value; +} + +const globalVariable = globalThis; + +const SLOT_BITS = 24; +const SLOT_MASK = (1 << SLOT_BITS) - 1; +const GEN_MASK = (1 << (32 - SLOT_BITS)) - 1; +class JSObjectSpace { + constructor() { + this._slotByValue = new Map(); + this._values = []; + this._stateBySlot = []; + this._freeSlotStack = []; + // Note: 0 is preserved for invalid references, 1 is preserved for globalThis + this._values[0] = undefined; + this._values[1] = globalVariable; + this._slotByValue.set(globalVariable, 1); + this._stateBySlot[1] = 1; // gen=0, rc=1 + } + retain(value) { + const slot = this._slotByValue.get(value); + if (slot !== undefined) { + const state = this._stateBySlot[slot]; + const nextState = (state + 1) >>> 0; + if ((nextState & SLOT_MASK) === 0) { + throw new RangeError(`Reference count overflow at slot ${slot}`); + } + this._stateBySlot[slot] = nextState; + return ((nextState & ~SLOT_MASK) | slot) >>> 0; + } + let newSlot; + let state; + if (this._freeSlotStack.length > 0) { + newSlot = this._freeSlotStack.pop(); + const gen = this._stateBySlot[newSlot] >>> SLOT_BITS; + state = ((gen << SLOT_BITS) | 1) >>> 0; + } + else { + newSlot = this._values.length; + if (newSlot > SLOT_MASK) { + throw new RangeError(`Reference slot overflow: ${newSlot} exceeds ${SLOT_MASK}`); + } + state = 1; + } + this._stateBySlot[newSlot] = state; + this._values[newSlot] = value; + this._slotByValue.set(value, newSlot); + return ((state & ~SLOT_MASK) | newSlot) >>> 0; + } + retainByRef(reference) { + const state = this._getValidatedSlotState(reference); + const slot = reference & SLOT_MASK; + const nextState = (state + 1) >>> 0; + if ((nextState & SLOT_MASK) === 0) { + throw new RangeError(`Reference count overflow at slot ${slot}`); + } + this._stateBySlot[slot] = nextState; + return reference; + } + release(reference) { + const state = this._getValidatedSlotState(reference); + const slot = reference & SLOT_MASK; + if ((state & SLOT_MASK) > 1) { + this._stateBySlot[slot] = (state - 1) >>> 0; + return; + } + this._slotByValue.delete(this._values[slot]); + this._values[slot] = undefined; + const nextGen = ((state >>> SLOT_BITS) + 1) & GEN_MASK; + this._stateBySlot[slot] = (nextGen << SLOT_BITS) >>> 0; + this._freeSlotStack.push(slot); + } + getObject(reference) { + this._getValidatedSlotState(reference); + return this._values[reference & SLOT_MASK]; + } + // Returns the packed state for the slot, after validating the reference. + _getValidatedSlotState(reference) { + const slot = reference & SLOT_MASK; + if (slot === 0) + throw new ReferenceError(`Attempted to use invalid reference ${reference}`); + const state = this._stateBySlot[slot]; + if (state === undefined || (state & SLOT_MASK) === 0) { + throw new ReferenceError(`Attempted to use invalid reference ${reference}`); + } + if (state >>> SLOT_BITS !== reference >>> SLOT_BITS) { + throw new ReferenceError(`Attempted to use stale reference ${reference}`); + } + return state; + } +} + +class SwiftRuntime { + constructor(options) { + this.version = 708; + this.textDecoder = new TextDecoder("utf-8"); + this.textEncoder = new TextEncoder(); // Only support utf-8 + this.UnsafeEventLoopYield = UnsafeEventLoopYield; + /** @deprecated Use `wasmImports` instead */ + this.importObjects = () => this.wasmImports; + this._instance = null; + this.memory = new JSObjectSpace(); + this._closureDeallocator = null; + this.tid = null; + this.options = options || {}; + this.getDataView = () => { + throw new Error("Please call setInstance() before using any JavaScriptKit APIs from Swift."); + }; + this.getUint8Array = () => { + throw new Error("Please call setInstance() before using any JavaScriptKit APIs from Swift."); + }; + this.wasmMemory = null; + } + setInstance(instance) { + this._instance = instance; + const wasmMemory = instance.exports.memory; + if (wasmMemory instanceof WebAssembly.Memory) { + // Cache the DataView as it's not a cheap operation + let cachedDataView = new DataView(wasmMemory.buffer); + let cachedUint8Array = new Uint8Array(wasmMemory.buffer); + // Check the constructor name of the buffer to determine if it's backed by a SharedArrayBuffer. + // We can't reference SharedArrayBuffer directly here because: + // 1. It may not be available in the global scope if the context is not cross-origin isolated. + // 2. The underlying buffer may be still backed by SAB even if the context is not cross-origin + // isolated (e.g. localhost on Chrome on Android). + if (Object.getPrototypeOf(wasmMemory.buffer).constructor.name === + "SharedArrayBuffer") { + // When the wasm memory is backed by a SharedArrayBuffer, growing the memory + // doesn't invalidate the data view by setting the byte length to 0. Instead, + // the data view points to an old buffer after growing the memory. So we have + // to check the buffer identity to determine if the data view is valid. + this.getDataView = () => { + if (cachedDataView.buffer !== wasmMemory.buffer) { + cachedDataView = new DataView(wasmMemory.buffer); + } + return cachedDataView; + }; + this.getUint8Array = () => { + if (cachedUint8Array.buffer !== wasmMemory.buffer) { + cachedUint8Array = new Uint8Array(wasmMemory.buffer); + } + return cachedUint8Array; + }; + } + else { + this.getDataView = () => { + if (cachedDataView.buffer.byteLength === 0) { + // If the wasm memory is grown, the data view is invalidated, + // so we need to create a new data view. + cachedDataView = new DataView(wasmMemory.buffer); + } + return cachedDataView; + }; + this.getUint8Array = () => { + if (cachedUint8Array.byteLength === 0) { + cachedUint8Array = new Uint8Array(wasmMemory.buffer); + } + return cachedUint8Array; + }; + } + this.wasmMemory = wasmMemory; + } + else { + throw new Error("instance.exports.memory is not a WebAssembly.Memory!?"); + } + if (typeof this.exports._start === "function") { + throw new Error(`JavaScriptKit supports only WASI reactor ABI. + Please make sure you are building with: + -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor + `); + } + if (this.exports.swjs_library_version() != this.version) { + throw new Error(`The versions of JavaScriptKit are incompatible. + WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${this.version}`); + } + } + main() { + const instance = this.instance; + try { + if (typeof instance.exports.main === "function") { + instance.exports.main(); + } + else if (typeof instance.exports.__main_argc_argv === "function") { + // Swift 6.0 and later use `__main_argc_argv` instead of `main`. + instance.exports.__main_argc_argv(0, 0); + } + } + catch (error) { + if (error instanceof UnsafeEventLoopYield) { + // Ignore the error + return; + } + // Rethrow other errors + throw error; + } + } + /** + * Start a new thread with the given `tid` and `startArg`, which + * is forwarded to the `wasi_thread_start` function. + * This function is expected to be called from the spawned Web Worker thread. + */ + startThread(tid, startArg) { + this.tid = tid; + const instance = this.instance; + try { + if (typeof instance.exports.wasi_thread_start === "function") { + instance.exports.wasi_thread_start(tid, startArg); + } + else { + throw new Error(`The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`); + } + } + catch (error) { + if (error instanceof UnsafeEventLoopYield) { + // Ignore the error + return; + } + // Rethrow other errors + throw error; + } + } + get instance() { + if (!this._instance) + throw new Error("WebAssembly instance is not set yet"); + return this._instance; + } + get exports() { + return this.instance.exports; + } + get closureDeallocator() { + if (this._closureDeallocator) + return this._closureDeallocator; + const features = this.exports.swjs_library_features(); + const librarySupportsWeakRef = (features & 1 /* LibraryFeatures.WeakRefs */) != 0; + if (librarySupportsWeakRef) { + this._closureDeallocator = new SwiftClosureDeallocator(this.exports); + } + return this._closureDeallocator; + } + callHostFunction(host_func_id, line, file, args) { + const argc = args.length; + const argv = this.exports.swjs_prepare_host_function_call(argc); + const memory = this.memory; + const dataView = this.getDataView(); + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + const base = argv + 16 * index; + write(argument, base, base + 4, base + 8, false, dataView, memory); + } + let output; + // This ref is released by the swjs_call_host_function implementation + const callback_func_ref = memory.retain((result) => { + output = result; + }); + const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); + if (alreadyReleased) { + throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line} @${host_func_id}`); + } + this.exports.swjs_cleanup_host_function_call(argv); + return output; + } + get wasmImports() { + let broker = null; + const getMessageBroker = (threadChannel) => { + var _a; + if (broker) + return broker; + const itcInterface = new ITCInterface(this.memory); + const defaultRequestHandler = (message) => { + const request = message.data.request; + // @ts-ignore dynamic dispatch by method name + const result = itcInterface[request.method].apply(itcInterface, request.parameters); + return { ok: true, value: result }; + }; + const requestHandlers = { + invokeRemoteJSObjectBody: (message) => { + const invocationContext = message.data.request + .parameters[0]; + const hasError = this.exports.swjs_invoke_remote_jsobject_body(invocationContext); + return { + ok: true, + value: { + object: hasError, + sendingContext: message.data.context, + transfer: [], + }, + }; + }, + }; + const defaultResponseHandler = (message) => { + if (message.data.response.ok) { + const object = this.memory.retain(message.data.response.value.object); + this.exports.swjs_receive_response(object, message.data.context); + } + else { + const error = deserializeError(message.data.response.error); + const errorObject = this.memory.retain(error); + this.exports.swjs_receive_error(errorObject, message.data.context); + } + }; + const responseHandlers = { + invokeRemoteJSObjectBody: (_message) => { + // Swift continuation is resumed on the owner thread. + }, + }; + const newBroker = new MessageBroker((_a = this.tid) !== null && _a !== void 0 ? _a : -1, threadChannel, { + onRequest: (message) => { + var _a; + let returnValue; + try { + const method = message.data.request.method; + const handler = (_a = requestHandlers[method]) !== null && _a !== void 0 ? _a : defaultRequestHandler; + returnValue = handler(message); + } + catch (error) { + returnValue = { + ok: false, + error: serializeError(error), + }; + } + const responseMessage = { + type: "response", + data: { + sourceTid: message.data.sourceTid, + context: message.data.context, + requestMethod: message.data.request.method, + response: returnValue, + }, + }; + try { + newBroker.reply(responseMessage); + } + catch (error) { + responseMessage.data.response = { + ok: false, + error: serializeError(new TypeError(`Failed to serialize message: ${error}`)), + }; + newBroker.reply(responseMessage); + } + }, + onResponse: (message) => { + var _a; + const method = message.data.requestMethod; + const handler = (_a = responseHandlers[method]) !== null && _a !== void 0 ? _a : defaultResponseHandler; + handler(message); + }, + }); + broker = newBroker; + return newBroker; + }; + return { + swjs_set_prop: (ref, name, kind, payload1, payload2) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const value = decode(kind, payload1, payload2, memory); + obj[key] = value; + }, + swjs_get_prop: (ref, name, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const result = obj[key]; + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); + }, + swjs_set_subscript: (ref, index, kind, payload1, payload2) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const value = decode(kind, payload1, payload2, memory); + obj[index] = value; + }, + swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(ref); + const result = obj[index]; + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); + }, + swjs_encode_string: (ref, bytes_ptr_result) => { + const memory = this.memory; + const bytes = this.textEncoder.encode(memory.getObject(ref)); + const bytes_ptr = memory.retain(bytes); + this.getDataView().setUint32(bytes_ptr_result >>> 0, bytes_ptr, true); + return bytes.length; + }, + swjs_decode_string: + // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer + this.options.sharedMemory == true + ? (bytes_ptr, length) => { + const bytesOffset = bytes_ptr >>> 0; + const byteLength = length >>> 0; + const bytes = this.getUint8Array().slice(bytesOffset, bytesOffset + byteLength); + const string = this.textDecoder.decode(bytes); + return this.memory.retain(string); + } + : (bytes_ptr, length) => { + const bytesOffset = bytes_ptr >>> 0; + const byteLength = length >>> 0; + const bytes = this.getUint8Array().subarray(bytesOffset, bytesOffset + byteLength); + const string = this.textDecoder.decode(bytes); + return this.memory.retain(string); + }, + swjs_load_string: (ref, buffer) => { + const bytes = this.memory.getObject(ref); + this.getUint8Array().set(bytes, buffer >>> 0); + }, + swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const func = memory.getObject(ref); + let result; + try { + const args = decodeArray(argv, argc, this.getDataView(), memory); + result = func(...args); + } + catch (error) { + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.getDataView(), this.memory); + } + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); + }, + swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const func = memory.getObject(ref); + const args = decodeArray(argv, argc, this.getDataView(), memory); + const result = func(...args); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); + }, + swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result; + try { + const args = decodeArray(argv, argc, this.getDataView(), memory); + result = func.apply(obj, args); + } + catch (error) { + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.getDataView(), this.memory); + } + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); + }, + swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + const args = decodeArray(argv, argc, this.getDataView(), memory); + const result = func.apply(obj, args); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); + }, + swjs_call_new: (ref, argv, argc) => { + const memory = this.memory; + const constructor = memory.getObject(ref); + const args = decodeArray(argv, argc, this.getDataView(), memory); + const instance = new constructor(...args); + return this.memory.retain(instance); + }, + swjs_call_throwing_new: (ref, argv, argc, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr) => { + let memory = this.memory; + const constructor = memory.getObject(ref); + let result; + try { + const args = decodeArray(argv, argc, this.getDataView(), memory); + result = new constructor(...args); + } + catch (error) { + write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.getDataView(), this.memory); + return -1; + } + memory = this.memory; + write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, this.getDataView(), memory); + return memory.retain(result); + }, + swjs_instanceof: (obj_ref, constructor_ref) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const constructor = memory.getObject(constructor_ref); + return obj instanceof constructor; + }, + swjs_value_equals: (lhs_ref, rhs_ref) => { + const memory = this.memory; + const lhs = memory.getObject(lhs_ref); + const rhs = memory.getObject(rhs_ref); + return lhs == rhs; + }, + swjs_create_function: (host_func_id, line, file) => { + var _a; + const fileString = this.memory.getObject(file); + const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args); + const func_ref = this.memory.retain(func); + (_a = this.closureDeallocator) === null || _a === void 0 ? void 0 : _a.track(func, host_func_id); + return func_ref; + }, + swjs_create_oneshot_function: (host_func_id, line, file) => { + const fileString = this.memory.getObject(file); + const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args); + const func_ref = this.memory.retain(func); + return func_ref; + }, + swjs_create_typed_array: (constructor_ref, elementsPtr, length) => { + const ArrayType = this.memory.getObject(constructor_ref); + if (length == 0) { + // The elementsPtr can be unaligned in Swift's Array + // implementation when the array is empty. However, + // TypedArray requires the pointer to be aligned. + // So, we need to create a new empty array without + // using the elementsPtr. + // See https://github.com/swiftwasm/swift/issues/5599 + return this.memory.retain(new ArrayType()); + } + const array = new ArrayType(this.wasmMemory.buffer, elementsPtr >>> 0, length >>> 0); + // Call `.slice()` to copy the memory + return this.memory.retain(array.slice()); + }, + swjs_create_object: () => { + return this.memory.retain({}); + }, + swjs_load_typed_array: (ref, buffer) => { + const memory = this.memory; + const typedArray = memory.getObject(ref); + const bytes = new Uint8Array(typedArray.buffer); + this.getUint8Array().set(bytes, buffer >>> 0); + }, + swjs_release: (ref) => { + this.memory.release(ref); + }, + swjs_release_remote: (tid, ref) => { + var _a; + if (!this.options.threadChannel) { + throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to release objects on remote threads."); + } + const broker = getMessageBroker(this.options.threadChannel); + broker.request({ + type: "request", + data: { + sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, + targetTid: tid, + context: 0, + request: { + method: "release", + parameters: [ref], + }, + }, + }); + }, + swjs_i64_to_bigint: (value, signed) => { + return this.memory.retain(signed ? value : BigInt.asUintN(64, value)); + }, + swjs_bigint_to_i64: (ref, signed) => { + const object = this.memory.getObject(ref); + if (typeof object !== "bigint") { + throw new Error(`Expected a BigInt, but got ${typeof object}`); + } + if (signed) { + return object; + } + else { + if (object < BigInt(0)) { + return BigInt(0); + } + return BigInt.asIntN(64, object); + } + }, + swjs_i64_to_bigint_slow: (lower, upper, signed) => { + const value = BigInt.asUintN(32, BigInt(lower)) + + (BigInt.asUintN(32, BigInt(upper)) << BigInt(32)); + return this.memory.retain(signed + ? BigInt.asIntN(64, value) + : BigInt.asUintN(64, value)); + }, + swjs_unsafe_event_loop_yield: () => { + throw new UnsafeEventLoopYield(); + }, + swjs_send_job_to_main_thread: (unowned_job) => { + this.postMessageToMainThread({ + type: "job", + data: unowned_job, + }); + }, + swjs_listen_message_from_main_thread: () => { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && + "listenMessageFromMainThread" in threadChannel)) { + throw new Error("listenMessageFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread."); + } + const broker = getMessageBroker(threadChannel); + threadChannel.listenMessageFromMainThread((message) => { + switch (message.type) { + case "wake": + this.exports.swjs_wake_worker_thread(); + break; + case "request": { + broker.onReceivingRequest(message); + break; + } + case "response": { + broker.onReceivingResponse(message); + break; + } + default: { + const unknownMessage = message; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + } + }); + }, + swjs_wake_up_worker_thread: (tid) => { + this.postMessageToWorkerThread(tid, { type: "wake" }); + }, + swjs_listen_message_from_worker_thread: (tid) => { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && + "listenMessageFromWorkerThread" in threadChannel)) { + throw new Error("listenMessageFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads."); + } + const broker = getMessageBroker(threadChannel); + threadChannel.listenMessageFromWorkerThread(tid, (message) => { + switch (message.type) { + case "job": + this.exports.swjs_enqueue_main_job_from_worker(message.data); + break; + case "request": { + broker.onReceivingRequest(message); + break; + } + case "response": { + broker.onReceivingResponse(message); + break; + } + default: { + const unknownMessage = message; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + } + }); + }, + swjs_terminate_worker_thread: (tid) => { + var _a; + const threadChannel = this.options.threadChannel; + if (threadChannel && "terminateWorkerThread" in threadChannel) { + (_a = threadChannel.terminateWorkerThread) === null || _a === void 0 ? void 0 : _a.call(threadChannel, tid); + } // Otherwise, just ignore the termination request + }, + swjs_get_worker_thread_id: () => { + // Main thread's tid is always -1 + return this.tid || -1; + }, + swjs_request_sending_object: (sending_object, transferring_objects, transferring_objects_count, object_source_tid, sending_context) => { + var _a; + if (!this.options.threadChannel) { + throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); + } + const broker = getMessageBroker(this.options.threadChannel); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, this.getDataView()); + broker.request({ + type: "request", + data: { + sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, + targetTid: object_source_tid, + context: sending_context, + request: { + method: "send", + parameters: [ + sending_object, + transferringObjects, + sending_context, + ], + }, + }, + }); + }, + swjs_request_sending_objects: (sending_objects, sending_objects_count, transferring_objects, transferring_objects_count, object_source_tid, sending_context) => { + var _a; + if (!this.options.threadChannel) { + throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); + } + const broker = getMessageBroker(this.options.threadChannel); + const dataView = this.getDataView(); + const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, dataView); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, dataView); + broker.request({ + type: "request", + data: { + sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, + targetTid: object_source_tid, + context: sending_context, + request: { + method: "sendObjects", + parameters: [ + sendingObjects, + transferringObjects, + sending_context, + ], + }, + }, + }); + }, + swjs_request_remote_jsobject_body: (object_source_tid, invocation_context) => { + var _a; + if (!this.options.threadChannel) { + throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request remote JSObject access."); + } + const broker = getMessageBroker(this.options.threadChannel); + broker.request({ + type: "request", + data: { + sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, + targetTid: object_source_tid, + context: invocation_context, + request: { + method: "invokeRemoteJSObjectBody", + parameters: [invocation_context], + }, + }, + }); + }, + }; + } + postMessageToMainThread(message, transfer = []) { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && "postMessageToMainThread" in threadChannel)) { + throw new Error("postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread."); + } + threadChannel.postMessageToMainThread(message, transfer); + } + postMessageToWorkerThread(tid, message, transfer = []) { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) { + throw new Error("postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads."); + } + threadChannel.postMessageToWorkerThread(tid, message, transfer); + } +} +/// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` +/// to JavaScript. This is usually thrown when: +/// - The entry point of the Swift program is `func main() async` +/// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()` +/// - Calling exported `main` or `__main_argc_argv` function from JavaScript +/// +/// This exception must be caught by the caller of the exported function and the caller should +/// catch this exception and just ignore it. +/// +/// FAQ: Why this error is thrown? +/// This error is thrown to unwind the call stack of the Swift program and return the control to +/// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()` +/// because the event loop expects `exit()` call before the end of the event loop. +class UnsafeEventLoopYield extends Error { +} + +export { SwiftRuntime }; diff --git a/Web/BrowserRuntime/src/wasi-shim.ts b/Web/BrowserRuntime/src/wasi-shim.ts new file mode 100644 index 0000000..cd58665 --- /dev/null +++ b/Web/BrowserRuntime/src/wasi-shim.ts @@ -0,0 +1,14 @@ +import { WASI, OpenFile, File, ConsoleStdout } from "@bjorn3/browser_wasi_shim"; + +export function createDefaultWASI() { + return new WASI( + [], + [], + [ + new OpenFile(new File([])), + ConsoleStdout.lineBuffered(console.log), + ConsoleStdout.lineBuffered(console.error), + ], + { debug: false } + ); +} diff --git a/Web/Package.resolved b/Web/Package.resolved new file mode 100644 index 0000000..cd4855f --- /dev/null +++ b/Web/Package.resolved @@ -0,0 +1,96 @@ +{ + "originHash" : "17c184a29087ac4e1dfcec5fa376f5f5cbeed804643a3a787cc28b515af86991", + "pins" : [ + { + "identity" : "coremodel", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/CoreModel", + "state" : { + "revision" : "790cfe9680a8705907a73898620a431a2c087bb2", + "version" : "2.10.0" + } + }, + { + "identity" : "coremodel-sqlite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/CoreModel-SQLite", + "state" : { + "branch" : "master", + "revision" : "07b8c3ab7e7e7f687b1fb4b56c66689289fa960f" + } + }, + { + "identity" : "elementary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/elementary-swift/elementary", + "state" : { + "revision" : "b0ff5d1353308a1aead140dfed4ad4fcb4455008", + "version" : "0.8.0" + } + }, + { + "identity" : "elementary-ui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/elementary-swift/elementary-ui", + "state" : { + "revision" : "91fe191af532ea29c0f3330f0a2ad07eba1b1b76", + "version" : "0.5.0" + } + }, + { + "identity" : "javascriptkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftwasm/JavaScriptKit", + "state" : { + "revision" : "22905075f8b61834810babe5fb9a2f613f22f398", + "version" : "0.56.1" + } + }, + { + "identity" : "sqlite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/SQLite", + "state" : { + "branch" : "master", + "revision" : "19b9b5ac00c1d09d06f2a02596690709b8f47979" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "db774a277f60063a32d854f2980299caf06da041", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-sqlcipher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/swift-sqlcipher", + "state" : { + "revision" : "37a25816c4cea90b3bc6f428fc3896b82dc58915", + "version" : "0.1.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", + "version" : "603.0.2" + } + } + ], + "version" : 3 +} diff --git a/Web/Package.swift b/Web/Package.swift new file mode 100644 index 0000000..c6c434e --- /dev/null +++ b/Web/Package.swift @@ -0,0 +1,46 @@ +// swift-tools-version: 6.3 +import PackageDescription + +// FuelingWeb — the browser/WebAssembly front end. +// +// A standalone package (like `Android/`) so the heavy, wasm-only ElementaryUI / +// JavaScriptKit / swift-syntax graph never touches the root package's Darwin, +// Android, or test builds. It depends on the root package via path for the +// reusable model layer (`FuelingModel` + `CoreFueling`) — the same `Store`, +// view models, and domain entities the Apple and Android apps use — and adds a +// JavaScriptKit `fetch` transport plus ElementaryUI views on top. +// +// Build & run through the Vite toolchain (`npm run dev` / `npm run build`), +// which drives `swift build --swift-sdk swift-6.3.3-RELEASE_wasm` under the hood +// via `@elementary-swift/vite-plugin-swift-wasm` (`useEmbeddedSDK: false`, since +// the model layer needs full Foundation). +let package = Package( + name: "FuelingWeb", + platforms: [ + .macOS(.v15) + ], + dependencies: [ + .package(path: ".."), + .package( + url: "https://github.com/elementary-swift/elementary-ui", + from: "0.5.0" + ), + .package( + url: "https://github.com/swiftwasm/JavaScriptKit", + .upToNextMinor(from: "0.56.1") + ) + ], + targets: [ + .executableTarget( + name: "FuelingWeb", + dependencies: [ + .product(name: "FuelingModel", package: "FuelingApp"), + .product(name: "CoreFueling", package: "FuelingApp"), + .product(name: "ElementaryUI", package: "elementary-ui"), + .product(name: "JavaScriptKit", package: "JavaScriptKit"), + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit") + ] + ) + ], + swiftLanguageModes: [.v5] +) diff --git a/Web/README.md b/Web/README.md new file mode 100644 index 0000000..bd03188 --- /dev/null +++ b/Web/README.md @@ -0,0 +1,52 @@ +# FuelingWeb + +The browser / WebAssembly front end for FuelingApp, built with +[ElementaryUI](https://github.com/elementary-swift/elementary-ui) and +[JavaScriptKit](https://github.com/swiftwasm/JavaScriptKit). + +This is a **standalone Swift package** (like `../Android/`) that depends on the +root package via a path dependency for the reusable model layer — the same +`FuelingModel` `Store`, view-model logic, and `CoreFueling` entities the Apple +and Android apps use. On top of that it adds: + +- **`FetchHTTPClient`** — an `HTTPClient` transport backed by the browser + `fetch` API (the wasm counterpart to `URLSession` / the Android JNI client). +- **`Store(inMemory:)`** — an in-memory persistence backend (the SQLite backend + can't compile for wasm), defined in `FuelingModel`. +- **ElementaryUI views** — a searchable locations list and a location detail + screen with live fuel prices. + +## Prerequisites + +- Swift 6.3+ with the WebAssembly SDK from swift.org: + `swift sdk install ` (installs + `swift-6.3.3-RELEASE_wasm`). +- Node.js 20+ and npm. + +## Run + +```sh +# 1. Start the local test server (from the repo root) +python3 ../Scripts/test-server.py --port 8080 + +# 2. Install JS deps and start the dev server (from this directory) +npm install +npm run dev +``` + +Open http://localhost:5173. The Vite plugin +(`@elementary-swift/vite-plugin-swift-wasm`) cross-compiles `FuelingWeb` to wasm +on demand and hot-reloads on source changes. `/v1/*` requests are proxied to the +test server on port 8080, so the app talks to its own origin (no CORS). + +`npm run build` produces a static production bundle in `dist/` (release wasm, +optionally `wasm-opt`-minified). + +## Notes + +- The build uses the **non-embedded** WASI SDK (`useEmbeddedSDK: false` in + `vite.config.ts`) because the reused model layer needs full Foundation. +- `BrowserRuntime/` vendors ElementaryUI's browser runtime (it isn't published + to npm); it's referenced as a `file:` dependency. +- Don't run a CLI `swift build` here while `npm run dev` is running — they race + on `.build`; let the Vite watcher own the rebuilds. diff --git a/Web/Sources/FuelingWeb/FetchHTTPClient.swift b/Web/Sources/FuelingWeb/FetchHTTPClient.swift new file mode 100644 index 0000000..63adaec --- /dev/null +++ b/Web/Sources/FuelingWeb/FetchHTTPClient.swift @@ -0,0 +1,86 @@ +// +// FetchHTTPClient.swift +// FuelingWeb +// + +import JavaScriptKit +import HTTPTypes +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif +import FuelingAPI + +/// An ``HTTPClient`` transport backed by the browser `fetch` API through +/// JavaScriptKit. +/// +/// The wasm counterpart to `URLSession` (Apple/Linux) and the JNI-callback +/// client (Android): the API layer only speaks ``HTTPClient``, so wiring a +/// `fetch`-based transport is all it takes to run the same networking code in +/// the browser. +/// +/// Requires `JavaScriptEventLoop.installGlobalExecutor()` to have run so the +/// `await`s on JS promises resume on the single browser thread. +public struct FetchHTTPClient: HTTPClient { + + public enum Failure: Swift.Error { + /// The request lacked a scheme/authority/path, so no absolute URL could be built. + case invalidRequestURL + /// `fetch` resolved to something that wasn't a `Response` object. + case notAResponse + /// The underlying `fetch`/`arrayBuffer` promise rejected. + case javaScript(JSException) + } + + public init() {} + + public func data( + for request: HTTPRequest + ) async throws(Failure) -> (Data, HTTPResponse) { + guard + let scheme = request.scheme, + let authority = request.authority, + let path = request.path + else { + throw .invalidRequestURL + } + let url = "\(scheme)://\(authority)\(path)" + + // Build the `fetch(url, { method, headers })` init dictionary. + let headers = JSObject() + for field in request.headerFields { + headers[field.name.canonicalName] = .string(field.value) + } + let options = JSObject() + options["method"] = .string(request.method.rawValue) + options["headers"] = .object(headers) + + let fetch = JSObject.global.fetch.function! + do { + let responseValue = try await JSPromise( + unsafelyWrapping: fetch(url, options).object! + ).value + guard let response = responseValue.object else { + throw Failure.notAResponse + } + let statusCode = Int(response.status.number ?? 0) + + // response.arrayBuffer() -> Promise -> Uint8Array -> Data + let bufferValue = try await JSPromise( + unsafelyWrapping: response.arrayBuffer!().object! + ).value + let byteArray = JSObject.global.Uint8Array.function!.new(bufferValue) + let bytes = JSTypedArray(unsafelyWrapping: byteArray) + let data = bytes.withUnsafeBytes { Data(buffer: $0) } + + return (data, HTTPResponse(status: .init(code: statusCode))) + } catch let error as JSException { + throw .javaScript(error) + } catch let error as Failure { + throw error + } catch { + throw .notAResponse + } + } +} diff --git a/Web/Sources/FuelingWeb/FuelingStore.swift b/Web/Sources/FuelingWeb/FuelingStore.swift new file mode 100644 index 0000000..4626258 --- /dev/null +++ b/Web/Sources/FuelingWeb/FuelingStore.swift @@ -0,0 +1,134 @@ +// +// FuelingStore.swift +// FuelingWeb +// + +import Reactivity +import FuelingModel +import CoreFueling + +/// Bridges the `@Observable` ``Store`` (stdlib Observation) into ElementaryUI's +/// own reactivity system. +/// +/// ElementaryUI tracks changes to `@Reactive` properties, not stdlib +/// `@Observable` objects, so this coordinator owns the plain snapshots the +/// views render (`locations`, `detailPrices`, `isLoading`, …) and re-populates +/// them from the `Store` after each operation. Mutating a `@Reactive` property +/// schedules a re-render. +/// +/// The type is deliberately **not** `@MainActor` — ElementaryUI renders view +/// bodies and dispatches DOM events from a `nonisolated` context, so the +/// snapshot properties must be reachable there. The `@MainActor` `Store` is +/// only ever touched inside `MainActor.assumeIsolated` (synchronous cache +/// reads) or `Task { @MainActor in … }` (async network), both valid on wasm's +/// single thread. +@Reactive +final class FuelingStore { + + let store: Store + + // MARK: List state + + var locations: [Location] = [] + var isLoading = false + var errorMessage: String? + var searchText = "" + + // MARK: Detail state + + var detailID: Location.ID? + var detailLocation: Location? + var detailPrices: [FuelProduct] = [] + var detailLoading = false + + init(store: Store) { + self.store = store + } + + // MARK: List + + func onAppear() { + guard locations.isEmpty else { return } + refresh() + } + + func refresh() { + guard !isLoading else { return } + isLoading = true + errorMessage = nil + Task { @MainActor in + do { + _ = try await store.locations() + } catch { + errorMessage = String(describing: error) + } + reloadFromCache() + isLoading = false + } + } + + func setSearch(_ text: String) { + guard text != searchText else { return } + searchText = text + // Synchronous, cache-only — no network round trip while typing. + MainActor.assumeIsolated { + reloadFromCache() + } + } + + @MainActor + private func reloadFromCache() { + let results = (try? store.viewContext.fetch(Location.self, search: searchText)) ?? [] + locations = results.sorted { $0.name < $1.name } + } + + // MARK: Detail + + func open(_ id: Location.ID) { + detailID = id + detailLoading = true + MainActor.assumeIsolated { + detailLocation = try? store.viewContext.fetch(Location.self, for: id) + detailPrices = prices(for: id) + } + Task { @MainActor in + // Refresh the location and its fuel prices from the network. + _ = try? await store.location(for: id) + _ = try? await store.fuelPrices(for: [id]) + detailLocation = try? store.viewContext.fetch(Location.self, for: id) + detailPrices = prices(for: id) + detailLoading = false + } + } + + func closeDetail() { + detailID = nil + detailLocation = nil + detailPrices = [] + detailLoading = false + } + + /// Fuel products for a location, queried by each product's `location` field. + /// + /// The DB-backed stores populate `Location.fuelProducts` via inverse- + /// relationship maintenance, which the in-memory store doesn't do — so this + /// filters `FuelProduct` by its own to-one `location` instead, which works + /// on any backend. + @MainActor + private func prices(for id: Location.ID) -> [FuelProduct] { + let all = (try? store.viewContext.fetch(FuelProduct.self)) ?? [] + return all + .filter { $0.location == id } + .sorted { $0.descriptionText < $1.descriptionText } + } + + /// Format a fuel price as US dollars without relying on Foundation's + /// `NumberFormatter`/`String(format:)` (unreliable on wasm/FoundationEssentials). + func formattedPrice(_ product: FuelProduct) -> String { + let cents = Int((product.price * 100).rounded()) + let dollars = cents / 100 + let remainder = abs(cents % 100) + let padded = remainder < 10 ? "0\(remainder)" : "\(remainder)" + return "$\(dollars).\(padded)" + } +} diff --git a/Web/Sources/FuelingWeb/LocationDetailView.swift b/Web/Sources/FuelingWeb/LocationDetailView.swift new file mode 100644 index 0000000..83646b7 --- /dev/null +++ b/Web/Sources/FuelingWeb/LocationDetailView.swift @@ -0,0 +1,70 @@ +// +// LocationDetailView.swift +// FuelingWeb +// + +import ElementaryUI +import CoreFueling + +/// A location's detail screen: address plus the current fuel prices table. +/// Mirrors `FuelingUI.LocationDetailView`. +@View +struct LocationDetailView { + + @Environment(FuelingStore.self) var model + + var body: some View { + div { + button { "← Back" } + .attributes( + .style([ + "background": "none", + "border": "none", + "color": "var(--accent)", + "font-size": "1rem", + "cursor": "pointer", + "padding": "4px 0", + "margin-bottom": "8px", + ]) + ) + .onClick { + model.closeDetail() + } + + if let location = model.detailLocation { + h2 { location.name } + .attributes(.style(["margin": "4px 0"])) + p { location.postalAddress } + .attributes(.style(["color": "var(--muted)", "white-space": "pre-line"])) + + h3 { "Fuel Prices" } + .attributes(.style(["margin": "20px 0 8px"])) + + if model.detailPrices.isEmpty { + p { + model.detailLoading ? "Loading prices…" : "No prices available." + } + .attributes(.style(["color": "var(--muted)"])) + } + + ForEach(model.detailPrices, key: { $0.id.description }) { product in + div { + span { product.descriptionText } + span { model.formattedPrice(product) } + .attributes(.style(["font-weight": "600"])) + } + .attributes( + .style([ + "display": "flex", + "justify-content": "space-between", + "padding": "10px 0", + "border-bottom": "1px solid var(--border)", + ]) + ) + } + } else { + p { "Loading…" }.attributes(.style(["color": "var(--muted)"])) + } + } + } +} diff --git a/Web/Sources/FuelingWeb/LocationsListView.swift b/Web/Sources/FuelingWeb/LocationsListView.swift new file mode 100644 index 0000000..ab5b2ba --- /dev/null +++ b/Web/Sources/FuelingWeb/LocationsListView.swift @@ -0,0 +1,83 @@ +// +// LocationsListView.swift +// FuelingWeb +// + +import ElementaryUI +import CoreFueling + +/// Searchable list of fueling locations. Mirrors `FuelingUI.LocationListView`. +@View +struct LocationsListView { + + @Environment(FuelingStore.self) var model + + var body: some View { + div { + input(.type(.text), .placeholder("Search locations")) + .attributes( + .style([ + "width": "100%", + "padding": "10px 12px", + "margin-bottom": "12px", + "border": "1px solid var(--border)", + "border-radius": "10px", + "background": "var(--card)", + "color": "var(--text)", + "font-size": "1rem", + ]) + ) + .onInput { event in + model.setSearch(event.targetValue ?? "") + } + + if let error = model.errorMessage { + p { "Couldn't load locations: \(error)" } + .attributes(.style(["color": "#ff6b6b"])) + } + + if model.isLoading && model.locations.isEmpty { + p { "Loading…" }.attributes(.style(["color": "var(--muted)"])) + } else if model.locations.isEmpty { + p { "No locations found." }.attributes(.style(["color": "var(--muted)"])) + } + + ForEach(model.locations, key: { $0.id.description }) { location in + LocationCardView(location: location) + } + } + .onAppear { + model.onAppear() + } + } +} + +/// A single tappable location row. Mirrors `FuelingUI.LocationCardView`. +@View +struct LocationCardView { + + @Environment(FuelingStore.self) var model + let location: Location + + var body: some View { + div { + div { location.name } + .attributes(.style(["font-weight": "600", "font-size": "1.05rem"])) + div { "\(location.city), \(location.state)" } + .attributes(.style(["color": "var(--muted)", "font-size": "0.9rem", "margin-top": "2px"])) + } + .attributes( + .style([ + "background": "var(--card)", + "border": "1px solid var(--border)", + "border-radius": "12px", + "padding": "12px 14px", + "margin-bottom": "8px", + "cursor": "pointer", + ]) + ) + .onClick { + model.open(location.id) + } + } +} diff --git a/Web/Sources/FuelingWeb/RootView.swift b/Web/Sources/FuelingWeb/RootView.swift new file mode 100644 index 0000000..c1cb0b6 --- /dev/null +++ b/Web/Sources/FuelingWeb/RootView.swift @@ -0,0 +1,39 @@ +// +// RootView.swift +// FuelingWeb +// + +import ElementaryUI + +/// Owns the reactive coordinator for the app's lifetime (via `@State`) and +/// publishes it into the environment so every child view observes its changes. +@View +struct RootView { + + @State var model: FuelingStore + + var body: some View { + ContentView() + .environment(model) + } +} + +/// Switches between the locations list and a location's detail screen. +@View +struct ContentView { + + @Environment(FuelingStore.self) var model + + var body: some View { + div { + h1 { "Fueling" } + .attributes(.style(["margin": "8px 0 16px", "font-size": "1.6rem"])) + + if model.detailID != nil { + LocationDetailView() + } else { + LocationsListView() + } + } + } +} diff --git a/Web/Sources/FuelingWeb/main.swift b/Web/Sources/FuelingWeb/main.swift new file mode 100644 index 0000000..e32a18b --- /dev/null +++ b/Web/Sources/FuelingWeb/main.swift @@ -0,0 +1,36 @@ +// +// main.swift +// FuelingWeb +// + +import ElementaryUI +import JavaScriptKit +import JavaScriptEventLoop +import FuelingAPI +import FuelingModel + +// Install the JS-promise-aware global executor so `await`s (network fetches, +// Store tasks) resume on the browser's single thread. +JavaScriptEventLoop.installGlobalExecutor() + +// `main`'s top-level code is nonisolated, but on wasm it runs on the single +// browser thread — so it is safe to assume the main actor to build the +// `@MainActor` store and mount the app. +MainActor.assumeIsolated { + // Talk to the page's own origin so requests stay same-origin (the Vite dev + // server proxies `/v1` to the local test server); fall back to localhost. + let origin = JSObject.global.location.object?.origin.string + let server = origin.flatMap { ServerURL(rawValue: $0) } ?? .localhost(port: 8080) + + // Composition root: an in-memory store (no SQLite/CoreData on wasm) fed by + // the browser `fetch` transport. Same `Store` the Apple and Android apps use. + let store = Store( + inMemory: .fueling, + locationService: APILocationService( + client: FetchHTTPClient(), + server: server + ) + ) + + Application(RootView(model: FuelingStore(store: store))).mount(in: "#app") +} diff --git a/Web/index.html b/Web/index.html new file mode 100644 index 0000000..91ad93c --- /dev/null +++ b/Web/index.html @@ -0,0 +1,35 @@ + + + + + + Fueling + + + + + +
+ + + + diff --git a/Web/index.ts b/Web/index.ts new file mode 100644 index 0000000..0e30a07 --- /dev/null +++ b/Web/index.ts @@ -0,0 +1,4 @@ +import { runApplication } from "elementary-ui-browser-runtime"; +import appInit from "virtual:swift-wasm?init"; + +await runApplication(appInit); diff --git a/Web/package-lock.json b/Web/package-lock.json new file mode 100644 index 0000000..e73fe38 --- /dev/null +++ b/Web/package-lock.json @@ -0,0 +1,1135 @@ +{ + "name": "fueling-web", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fueling-web", + "devDependencies": { + "@bjorn3/browser_wasi_shim": "~0.4.2", + "@elementary-swift/vite-plugin-swift-wasm": "^0.1.3", + "elementary-ui-browser-runtime": "file:./BrowserRuntime", + "vite": "^7.3.1" + } + }, + "BrowserRuntime": { + "name": "elementary-ui-browser-runtime", + "version": "0.1.0", + "dev": true, + "license": "Apache-2.0 AND MIT" + }, + "node_modules/@bjorn3/browser_wasi_shim": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.4.2.tgz", + "integrity": "sha512-/iHkCVUG3VbcbmEHn5iIUpIrh7a7WPiwZ3sHy4HZKZzBdSadwdddYDZAII2zBvQYV0Lfi8naZngPCN7WPHI/hA==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@elementary-swift/vite-plugin-swift-wasm": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@elementary-swift/vite-plugin-swift-wasm/-/vite-plugin-swift-wasm-0.1.3.tgz", + "integrity": "sha512-ATrybO1vdaMp4WCM3bHeGSrWDZNP6vvxDTYBxMH78UP70TFjoxtJWNOEeW1n6gpqzGV6STAwmq4wGQbpouDvlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "picocolors": "^1.1.0" + }, + "peerDependencies": { + "vite": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/elementary-ui-browser-runtime": { + "resolved": "BrowserRuntime", + "link": true + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/Web/package.json b/Web/package.json new file mode 100644 index 0000000..c724b88 --- /dev/null +++ b/Web/package.json @@ -0,0 +1,16 @@ +{ + "name": "fueling-web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@bjorn3/browser_wasi_shim": "~0.4.2", + "@elementary-swift/vite-plugin-swift-wasm": "^0.1.3", + "elementary-ui-browser-runtime": "file:./BrowserRuntime", + "vite": "^7.3.1" + } +} diff --git a/Web/tsconfig.json b/Web/tsconfig.json new file mode 100644 index 0000000..480dc34 --- /dev/null +++ b/Web/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "types": ["vite/client", "@elementary-swift/vite-plugin-swift-wasm/client"], + "isolatedModules": true, + "strict": true, + "noEmit": true + } +} diff --git a/Web/vite.config.ts b/Web/vite.config.ts new file mode 100644 index 0000000..0c5f043 --- /dev/null +++ b/Web/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "vite"; +import swiftWasm from "@elementary-swift/vite-plugin-swift-wasm"; + +export default defineConfig({ + plugins: [ + swiftWasm({ + // The FuelingWeb executable in this package's Package.swift. + product: "FuelingWeb", + // Non-embedded: the reused FuelingModel/CoreModel/FuelingAPI graph needs + // full Foundation, so build with the standard `swift-6.3.3-RELEASE_wasm` + // SDK rather than the embedded one. + useEmbeddedSDK: false, + }), + ], + server: { + // Proxy API calls to the local test server (Scripts/test-server.py) so the + // browser hits same-origin `/v1/...` instead of tripping CORS. + proxy: { + "/v1": "http://localhost:8080", + }, + }, +});