// `.swift` handling for One's native rolldown engine: submit the owning // swiftpm package to `/api/swift-compile` and answer every `.swift` import // with the module that mounts the published artifact. // // this is the remote twin of `vite-plugin-swift.ts`. that plugin runs the // local toolchain and serves the artifact from dev-server middleware; the // native engine is its own rolldown instance with no middleware seam, so // this plugin compiles nothing in-process and serves nothing. the artifact // url the service answers is absolute (compile-cdn), so there is nothing to // serve. same compiler, same store, same hash, same credential as the // browser worker — one path, no second toolchain to install for `one dev`. // // an app registers it through One's native plugin seam, not `config.plugins` // (which never reach the native engine): // // // vite.config.ts // export default { // plugins: [one({ // native: { // bundler: 'vite', // bundlerOptions: { // plugins: [swiftRemotePlugin({ // endpoint: 'https://contrast.dev', // assetReadToken: process.env.CONTRAST_CCDN_KEY, // })], // }, // }, // })], // } // // no babel anywhere in this path: resolveId and load only, emitting an ESM // module. offline `one dev` fails loud at the submit, the same as a browser // build without a credential; the standalone `rnx` CLI keeps the local // toolchain plugin for that case. import fs from 'node:fs' import path from 'node:path' import { enumerateBundleImages, enumerateBundleJson, } from '../../sootsim-swift/src/bundleAssets' import { swiftEntryModuleEsm } from '../../sootsim-swift/src/entryModule' import { compileSwiftProject, httpSwiftCompileSubmit } from './swift-submit' import { entryRootId } from './vite-plugin-swift' import type { SwiftModuleAddress } from './swift-submit' import type { Plugin } from 'vite' const VIRTUAL_PREFIX = '\0rnx-swift-remote:' export interface SwiftRemotePluginOptions { /** origin serving `/api/swift-compile` (the Contrast app). */ endpoint: string /** a `ccdn_` API key or dev session token the compile verifier accepts. */ assetReadToken: string /** * directory uploaded `.swift` imports resolve under. defaults to the * nearest `Package.swift` above each imported file — the swiftpm package * that owns it — which is the compile unit, exactly like the local host. * an app that keeps sources somewhere else names that package explicitly. */ packagePath?: string } interface RemoteProject { packagePath: string address: SwiftModuleAddress | null } // one address per package per process: a failed submit keeps serving the // last published artifact (and warns) rather than failing the module, so // the edit that fixes the error still has a graph to rebuild. const projects = new Map() function projectFor(packagePath: string): RemoteProject { const existing = projects.get(packagePath) if (existing) return existing const created: RemoteProject = { packagePath, address: null } projects.set(packagePath, created) return created } // every file under a directory, posix-relative, skipping what can never be // a source, a manifest, or bundle content. a nested Package.swift is a // separate package with its own compile: its files belong to that unit. // `.build` holds tens of thousands of intermediates and no bundle content; // walking it per import would tax every reload. function packageFiles(root: string): string[] { const found: string[] = [] const walk = (dir: string): void => { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if ( entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '.build' ) continue const full = path.join(dir, entry.name) if (entry.isDirectory()) { if (full !== root && fs.existsSync(path.join(full, 'Package.swift'))) continue walk(full) } else { found.push(path.relative(root, full).split(path.sep).join(path.posix.sep)) } } } walk(root) return found } // the file map the service compiles: the package's `.swift` sources plus // its root manifest, keyed package-relative. the manifest is partitioning // input only — the service emits its own manifest that links every target // against its SwiftUI module — so a standard local package travels as is. function sourceFiles(packagePath: string): Record { const files: Record = {} for (const relative of packageFiles(packagePath)) { if (!relative.endsWith('.swift')) continue files[relative] = fs.readFileSync( path.join(packagePath, ...relative.split('/')), 'utf8', ) } return files } // the bundle pack and the photo uploads, through the shared enumeration // both hosts pack with, so the collision winner here is the winner on the // local host too. binary bytes straight from disk: this host is node. function bundlePack(packagePath: string): { resources: Record images: Record watched: string[] } { const relative = packageFiles(packagePath) const disk = (file: string): string => path.join(packagePath, ...file.split('/')) const resources: Record = {} const watched: string[] = [] for (const { base, file } of enumerateBundleJson(relative)) { resources[base] = fs.readFileSync(disk(file)).toString('base64') watched.push(file) } const images: Record = {} const byRelative = new Map(relative.map((file) => [file, disk(file)])) for (const { name, file } of enumerateBundleImages(relative, (candidate) => { const absolute = byRelative.get(candidate) if (!absolute || path.basename(absolute) !== 'Contents.json') return null try { return fs.readFileSync(absolute, 'utf8') } catch { return null } })) { const absolute = byRelative.get(file) if (!absolute) continue images[name] = { file: path.posix.basename(file), data: fs.readFileSync(absolute).toString('base64'), } watched.push(file) } return { resources, images, watched } } // the swiftpm package that owns an imported file: the nearest directory at // or above the file that holds a Package.swift. a `.swift` file with no // package above it (a pod, an xcode target) is refused loud rather than // compiled as the wrong unit. function packageFor(file: string, stop?: string): string { let dir = path.dirname(file) for (;;) { if (fs.existsSync(path.join(dir, 'Package.swift'))) return dir if ((stop !== undefined && dir === stop) || path.dirname(dir) === dir) break dir = path.dirname(dir) } throw new Error( `no Package.swift above ${file}; a .swift import needs a swiftpm package like the one in packages/sootsim-swift/example`, ) } export function swiftRemotePlugin(options: SwiftRemotePluginOptions): Plugin { const submit = httpSwiftCompileSubmit({ assetReadToken: options.assetReadToken, endpoint: options.endpoint, }) if (!submit) { throw new Error( 'swiftRemotePlugin needs an assetReadToken: a build without a compile credential cannot compile .swift sources', ) } return { name: 'rnx-swift-remote', enforce: 'pre', async resolveId(source, importer) { if (source.startsWith(VIRTUAL_PREFIX)) return source if (!source.endsWith('.swift')) return null const resolved = await this.resolve(source, importer, { skipSelf: true }) if (!resolved) throw new Error(`cannot resolve swift source ${source}`) return VIRTUAL_PREFIX + resolved.id }, async load(id) { if (!id.startsWith(VIRTUAL_PREFIX)) return null const file = id.slice(VIRTUAL_PREFIX.length) if (!fs.existsSync(file)) throw new Error(`swift source ${file} no longer exists`) const packagePath = options.packagePath ?? packageFor(file) const project = projectFor(packagePath) const files = sourceFiles(packagePath) const entry = path.relative(packagePath, file).split(path.sep).join(path.posix.sep) for (const source of Object.keys(files)) { this.addWatchFile(path.join(packagePath, ...source.split('/'))) } const pack = bundlePack(packagePath) for (const watched of pack.watched) { this.addWatchFile(path.join(packagePath, ...watched.split('/'))) } try { project.address = await compileSwiftProject( submit, { files, resources: pack.resources, images: pack.images, entry }, options.endpoint, ) } catch (error) { // a compile error must not fail this module: the graph would stop // watching it and the fix would never rebuild. the last published // artifact keeps rendering while the diagnostic is reported here. if (!project.address) throw error this.warn(error instanceof Error ? error.message : String(error)) } const address = project.address // every import mounts through the one entry: the artifact answers // whether it is a view or an app root at runtime, so the transform // never re-derives the conformance from source. return swiftEntryModuleEsm({ url: address.url, hash: address.hash, rootId: entryRootId(packagePath, file), resources: address.imageUrls ?? {}, bundleFiles: pack.resources, }) }, } }