Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/wasm-prelude.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Prelude for running BSWFoundation's test suite on WebAssembly under Node
// (`swift package --swift-sdk … js test --prelude .github/wasm-prelude.js`).
//
// Node provides `fetch` but not `localStorage` (a browser API), so we shim it in memory
// for the storage tests (KeychainBacked / UserDefaultsBacked → WASMKeyValueStore).
const store = new Map()
globalThis.localStorage = {
getItem: (key) => (store.has(key) ? store.get(key) : null),
setItem: (key, value) => { store.set(key, String(value)) },
removeItem: (key) => { store.delete(key) },
}
24 changes: 24 additions & 0 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,27 @@ jobs:
fi
- name: Run tests on Android
run: SKIP_ENABLED=1 skip android test

wasm-build:
# GitHub-hosted: the self-hosted `mobile` runners don't have the WebAssembly Swift SDK.
# The official Swift image provides the toolchain; we then install the matching wasm SDK,
# compile the package for WebAssembly, and run the (wasm-eligible) unit tests in Node.
runs-on: ubuntu-latest
container: swift:6.3.3
steps:
- uses: actions/checkout@v4
- name: Install Swift SDK for WebAssembly
run: |
swift sdk install \
https://download.swift.org/swift-6.3.3-release/wasm-sdk/swift-6.3.3-RELEASE/swift-6.3.3-RELEASE_wasm.artifactbundle.tar.gz \
--checksum cabfa08b73bb8ac783927ecd15fa386e99d0c139c5f232445067bcf58379cae7
- name: Build for WebAssembly
run: swift build --swift-sdk swift-6.3.3-RELEASE_wasm
- name: Install Node.js (for the wasm test runner)
run: apt-get update && apt-get install -y nodejs npm
- name: Install the WASI shim
run: npm install @bjorn3/browser_wasi_shim@0.3.0
- name: Run unit tests on WebAssembly (Node)
run: |
swift package --swift-sdk swift-6.3.3-RELEASE_wasm --disable-sandbox \
js test --prelude .github/wasm-prelude.js
32 changes: 29 additions & 3 deletions Docs/context/platform-support.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Platform Support

`BSWFoundation` is Apple-first and has an explicit Skip/Android path for the subset that can run without Apple-only frameworks.
`BSWFoundation` is Apple-first and has explicit Android and browser WebAssembly paths for the subset that can run without Apple-only frameworks.

## Apple Platforms

Expand All @@ -26,8 +26,34 @@ When `SKIP_ENABLED` is present, the package adds Skip dependencies and Android-s
- `FoundationInternationalization`
- Android logging and platform APIs where needed.

The README states that all features except `AuthStorage` and `LocationFetcher` are intended to be available on Android. `KeychainBacked` uses `SkipKeychain` on Android when Skip dependencies are enabled. The JWT helpers currently live under the `AuthStorage.swift` Darwin guard, so their intended Android availability should be clarified before relying on them from Skip code.
The README states that all features except `AuthStorage` and `LocationFetcher` are intended to be available on Android. Some Android storage APIs require Skip-only modules: `KeychainBacked` and `CodableKeychainBacked` require `SkipKeychain`; `UserDefaultsBacked` and `CodableUserDefaultsBacked` require the Skip user defaults bridge. Build with `SKIP_ENABLED=1` so SwiftPM includes these dependencies. Without those modules, the wrappers remain present but unavailable with a targeted compiler diagnostic.

The JWT helpers currently live under the `AuthStorage.swift` Darwin guard, so their intended Android availability should be clarified before relying on them from Skip code.

## Browser WebAssembly Via SwiftWasm

The package compiles for WASI browser-hosted SwiftWasm. WASM dependencies are conditioned in `Package.swift`:

- `JavaScriptKit`
- `JavaScriptEventLoop`
- `JavaScriptFoundationCompat`
- `swift-log`

Browser networking is handled by the WASI-only fetch implementation. `APIClient` defaults to the browser fetch-backed network fetcher on WASM and keeps the URLSession-backed default on platforms where URLSession is available. Browser fetch follows browser security rules for TLS, CORS and mixed content; `Environment.shouldAllowInsecureConnections` is not emulated in the browser.

WASM consumers must install the JavaScriptKit event-loop executor once during host startup by calling `BSWBrowserRuntime.installJavaScriptEventLoop()` before creating an `APIClient` or starting async work that depends on JavaScript callbacks.

Persistence support on WASM is intentionally narrow:

- `UserDefaultsBacked` uses an internal browser `localStorage` adapter and supports only the value types documented in README. Use `CodableUserDefaultsBacked` for codable values.

Current WASM exclusions and limitations:

- `AuthStorage` and `LocationFetcher` are unavailable.
- `KeychainBacked` and `CodableKeychainBacked` are unavailable because browsers do not expose Keychain-equivalent secure storage to SwiftWasm.
- URL file uploads are unavailable; browser upload support must use the WASM browser upload body API.
- APIs that require Apple-only frameworks or URLSession delegates remain guarded out of WASI.

## Compatibility Rule

New shared APIs should compile on all declared Apple platforms unless intentionally guarded. New APIs meant to be available on Android should avoid Darwin-only dependencies or provide Android-specific branches.
New shared APIs should compile on all declared Apple platforms unless intentionally guarded. New APIs meant to be available on Android or WASM should avoid Darwin-only dependencies or provide platform-specific branches. Public APIs that are intentionally unavailable on Android or WASM should fail clearly at compile time or with a targeted runtime diagnostic.
6 changes: 3 additions & 3 deletions Docs/features/persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ The same source file includes JWT decoding helpers derived from Auth0's JWT deco

## KeychainBacked

`KeychainBacked` stores optional strings in Keychain. It supports Darwin through `KeychainAccess` and Android through `SkipKeychain`; Linux is excluded.
`KeychainBacked` stores optional strings in Keychain. It supports Darwin through `KeychainAccess` and Android through `SkipKeychain`; Linux and WebAssembly are excluded. Android consumers must build with `SKIP_ENABLED=1` so SwiftPM includes `SkipKeychain`.

`CodableKeychainBacked` stores optional `Codable` values by encoding them before persistence and decoding them on read.
`CodableKeychainBacked` stores optional `Codable` values by encoding them before persistence and decoding them on read. It is also unavailable on WebAssembly because browsers do not expose Keychain-equivalent secure storage to SwiftWasm.

## UserDefaultsBacked

`UserDefaultsBacked` stores optional primitive values in user defaults. Darwin can use standard defaults or an app group suite. The Android path currently supports a narrower set of value types.
`UserDefaultsBacked` stores optional primitive values in user defaults. Darwin can use standard defaults or an app group suite. The Android path requires Skip's user defaults bridge, which is included when building with `SKIP_ENABLED=1`, and currently supports a narrower set of value types.

`CodableUserDefaultsBacked` stores optional `Codable` values by encoding them into user defaults.

Expand Down
47 changes: 46 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 34 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,31 @@ let applePlatforms = TargetDependencyCondition.when(
]
)

let androidPlatforms = TargetDependencyCondition.when(platforms: [.android])

// Platforms where URLSession / FoundationNetworking exist. Excludes WASM (WASI), where
// HTTPTypesFoundation's URLSession bridge does not compile.
let foundationNetworkingPlatforms = TargetDependencyCondition.when(
platforms: [
.iOS,
.macOS,
.macCatalyst,
.tvOS,
.watchOS,
.visionOS,
.linux,
.android,
.windows
]
)

var packageDependencies: [Package.Dependency] = [
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.12.3"),
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.6.0"),
.package(url: "https://github.com/swiftwasm/JavaScriptKit.git", from: "0.56.1"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
.package(url: "https://source.skip.tools/swift-android-native.git", from: "1.4.1"),
]

if skipIsEnabled {
Expand All @@ -33,11 +54,13 @@ if skipIsEnabled {
var targetDependencies: [Target.Dependency] = [
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "KeychainAccess", package: "KeychainAccess", condition: applePlatforms),
// `HTTPTypes` is pure Swift (no Foundation) and links on every platform, including WASM.
.product(name: "HTTPTypes", package: "swift-http-types"),
// `HTTPTypesFoundation` bridges to URLSession/URLRequest. Its API compiles to nothing on
// WASI, so linking it everywhere is harmless; we only `import` it from the URLSession fetcher.
.product(name: "HTTPTypesFoundation", package: "swift-http-types"),
.product(name: "HTTPTypesFoundation", package: "swift-http-types", condition: foundationNetworkingPlatforms),
.product(name: "JavaScriptKit", package: "JavaScriptKit", condition: .when(platforms: [.wasi])),
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit", condition: .when(platforms: [.wasi])),
.product(name: "JavaScriptFoundationCompat", package: "JavaScriptKit", condition: .when(platforms: [.wasi])),
.product(name: "Logging", package: "swift-log", condition: .when(platforms: [.wasi])),
.product(name: "AndroidLogging", package: "swift-android-native", condition: androidPlatforms),
]

if skipIsEnabled {
Expand Down Expand Up @@ -70,7 +93,13 @@ let package = Package(
),
.testTarget(
name: "BSWFoundationTests",
dependencies: ["BSWFoundation"]
dependencies: [
"BSWFoundation",
// On wasm, linking this activates the JavaScriptKit event-loop executor for the
// test bundle, so async tests (Task.sleep, etc.) run instead of hitting an
// unsupported WASI async-io syscall.
.product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit", condition: .when(platforms: [.wasi])),
]
),
],
swiftLanguageModes: [.v6],
Expand Down
Loading
Loading