one
Version:
One is a React framework for web, iOS, and Android apps.
113 lines (101 loc) • 6.11 kB
text/typescript
import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterAll, describe, expect, it } from 'vitest'
import { swiftPackagePlugin } from './swiftPackagePlugin'
import { renderSwiftPackageModule } from './swiftPackageModule'
const roots: string[] = []
function swiftFixture(expo = false) {
const root = mkdtempSync(join(tmpdir(), 'one-metro-swift-'))
roots.push(root)
symlinkSync(resolve(__dirname, '../../../../../node_modules'), join(root, 'node_modules'))
writeFileSync(join(root, 'package.json'), JSON.stringify({ dependencies: expo ? { expo: '*' } : {} }))
const packageDir = join(root, 'native-source')
mkdirSync(packageDir)
writeFileSync(join(packageDir, 'Package.swift'), 'let package = Package(name: "Audio")\n')
const source = join(packageDir, 'Audio.swift')
const sibling = join(packageDir, 'Helper.swift')
writeFileSync(source, 'final class AudioMath: PeachModule { func rms(_ values: [Double]) -> Double { 1 } }\n')
writeFileSync(sibling, 'struct Helper { let value = 1 }\n')
return { root, packageDir, source, sibling }
}
afterAll(() => {
for (const root of roots) rmSync(root, { recursive: true, force: true })
})
describe('swift package native module', () => {
it('renders byte-identical code for the vite loader and metro, with a package-wide hash', async () => {
const fixture = swiftFixture()
const watched: string[] = []
const plugin = swiftPackagePlugin('ios', fixture.root)
const hook = plugin.load
if (!hook || typeof hook === 'function') throw new Error('expected vite load hook')
const viteCode = await Reflect.apply(hook.handler, { addWatchFile: (file: string) => watched.push(file) }, [fixture.source])
const metro = renderSwiftPackageModule(fixture.source, 'ios', fixture.root)
expect(viteCode).toBe(metro.code)
expect(metro.code).toContain('AudioMath')
expect(metro.code).toContain('callNativeSource')
expect(watched).toEqual(metro.watchFiles)
expect(watched).toContain(fixture.sibling)
expect(readFileSync(join(fixture.packageDir, '.one-native-source.json'), 'utf8')).toContain('AudioMath')
writeFileSync(fixture.sibling, 'final class Helper: PeachModule { func next() -> Int { 2 } }\n')
const changed = renderSwiftPackageModule(fixture.source, 'ios', fixture.root)
expect(changed.code).not.toBe(metro.code)
expect(changed.code).toContain('AudioMath')
})
it('exports a public swift view as a component bound to the package contract', () => {
const fixture = swiftFixture()
const level = join(fixture.packageDir, 'Level.swift')
writeFileSync(level, 'public struct Level: View {\n let value: Double\n let onChange: (Double) -> Void\n public var body: some View { Text("") }\n}\n')
const module = renderSwiftPackageModule(level, 'ios', fixture.root)
const descriptor = JSON.parse(readFileSync(join(fixture.packageDir, '.one-native-source.json'), 'utf8'))
expect(descriptor.views).toEqual({ Level: ['value', 'onChange'] })
expect(module.code).toContain(`export function Level(props) { return createElement(SwiftPackageView, { packageName: "native_source", view: "Level", contractHash: ${JSON.stringify(descriptor.hash)}, props, fill: false }) }`)
})
it('rejects Android and Swift outside a package, and Expo prebuild without generated glue', () => {
const fixture = swiftFixture()
expect(() => renderSwiftPackageModule(fixture.source, 'android', fixture.root)).toThrow('Android build')
const expo = swiftFixture(true)
expect(() => renderSwiftPackageModule(expo.source, 'ios', expo.root)).toThrow('nativeSource')
const loose = join(fixture.root, 'Loose.swift')
writeFileSync(loose, 'struct Loose {}\n')
expect(() => renderSwiftPackageModule(loose, 'ios', fixture.root)).toThrow('no Package.swift')
})
it('bundles Swift in Expo prebuild apps once the plugin generated the pod', () => {
const expo = swiftFixture(true)
const podDir = join(expo.root, 'ios', 'OneSwiftPackages', 'native_source')
mkdirSync(podDir, { recursive: true })
writeFileSync(join(podDir, 'native_source.podspec'), "Pod::Spec.new do |s|\nend\n")
const module = renderSwiftPackageModule(expo.source, 'ios', expo.root)
expect(module.code).toContain('AudioMath')
expect(module.code).toContain('callNativeSource')
})
it('wraps wired methods with encode and revive, keeping plain methods untouched', () => {
const fixture = swiftFixture()
const plain = renderSwiftPackageModule(fixture.source, 'ios', fixture.root)
expect(plain.code).not.toContain('encodeNativeSourceArgs')
const atlas = join(fixture.root, 'native-source', 'Atlas.swift')
writeFileSync(
atlas,
'@MainActor final class Atlas: PeachModule {\n init() {}\n func at(_ when: Date) -> Date { when }\n}\n'
)
const module = renderSwiftPackageModule(atlas, 'ios', fixture.root)
expect(module.code).toContain('encodeNativeSourceArgs')
expect(module.code).toContain('decodeNativeSourceResult')
expect(module.code).toContain('const nativeSourceStructs = {}')
expect(module.code).toContain('wire.native.js')
})
it('renders subscriptions as event listeners', () => {
const fixture = swiftFixture()
const clock = join(fixture.root, 'native-source', 'Clock.swift')
writeFileSync(
clock,
'@MainActor final class Clock: PeachModule {\n init() {}\n func onTick(_ callback: @escaping (Double) -> Void) {}\n}\n'
)
const module = renderSwiftPackageModule(clock, 'ios', fixture.root)
expect(module.code).toContain('events.native.js')
expect(module.code).toContain('subscribeNativeSourceEvent("native_source.Clock", "onTick"')
// the clock package has no views: the subscribe glue it writes must
// still define the args helper the subscribe cases call.
expect(readFileSync(join(fixture.packageDir, '.one-native-source.swift.txt'), 'utf8')).toContain('private static func args(_ values: [any Encodable]) -> String')
})
})