/** * swift-compile's caller half: ask the compile host for this project's wasm * artifact, and poll until it is published. * * This is the `.swift` transform's one request path, shared by every host * that renders a project's swift sources: the in-IDE bundler worker (posts * its own origin's `/api/swift-compile`), the node bundle builder used by * the test-drive CLI and the deploy publisher (posts an absolute endpoint), * and the One/rolldown `.swift` transform (same, with a `ccdn_` credential). * A caller never compiles in-process: the container is the one compiler, and * no bundler is that instance. * * It lives here (rather than in the app's `src/bundler/`) because the One * transform ships in this package and cannot import app code; the app's own * callers import this subpath back. The route path and the outcome shape are * the wire coupling to `src/compile/swiftCompileContract.ts`, which owns the * server-side caps, parsers, and refusals and imports these back. * * Browser-safe: node-only submitters live beside their call sites. */ export const SWIFT_COMPILE_PATH = '/api/swift-compile' export interface SwiftImageUpload { /** the raster file picked for the asset (has the extension). */ file: string /** the raster bytes, base64: carried once so the service can publish them. */ data: string } export type SwiftCompileOutcome = /** * the artifact is published; the bundler mounts it by url and hash. * * `artifactUrl` is root-relative (`/compile/asset/`), because the * host that compiles has no browser origin in front of it: the local node * dev server and the cf-build container would each have to guess one. The * caller that renders the module resolves it against its own origin, which * is the origin the artifact route is served from in dev and in prod alike. */ | { status: 'published' artifactUrl: string hash: string /** * the published photo rasters, bundle asset name to absolute url. * root-relative like the artifact url; the caller resolves it against * its own origin, and the entry embeds it as the image table. absent * when the request packs no images. */ imageUrls?: Record } /** * the build is running; poll the same source set again. One build is tens of * seconds and a service-binding request is cancelled at about four, so the * container answers this before the connection dies and the caller polls. */ | { status: 'compiling'; hash: string } /** refused before any work: no entry, too many files, over a cap, over a rate limit. */ | { status: 'refused'; detail: string } /** the compiler ran and failed; detail carries its diagnostic verbatim. */ | { status: 'failed'; detail: string } const POLL_INTERVAL_MS = 1500 export interface SwiftCompileContext { /** * the compile credential: the signed preview token `BundlerSessionRuntime` * mints for a live project session, or a `ccdn_` API key. `compileCredential.server.ts` * is the one verifier. absent means this build has no credential and a * project with `.swift` sources cannot be compiled. */ assetReadToken?: string /** * absolute origin of the app serving `/api/swift-compile`. The browser * worker omits it and posts its own origin; a node caller that is not that * app must pass one. */ endpoint?: string } export type SwiftCompileSubmit = (work: { files: Record /** the bundle pack; absent means the project packs none. */ resources?: Record /** the photo rasters to publish; absent means the project packs none. */ images?: Record entry: string }) => Promise /** the HTTP submitter, shared by the browser worker and the node build. */ export function httpSwiftCompileSubmit( context: SwiftCompileContext, ): SwiftCompileSubmit | undefined { const { assetReadToken, endpoint } = context if (!assetReadToken) return undefined const url = endpoint ? `${endpoint.replace(/\/+$/, '')}${SWIFT_COMPILE_PATH}` : SWIFT_COMPILE_PATH return async (work) => { let response: Response try { response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ...work, token: assetReadToken }), }) } catch (error) { throw new Error( `contrast-bundler: fetch failed for ${url} while compiling ${work.entry}: ${error instanceof Error ? error.message : String(error)}`, { cause: error }, ) } if (!response.ok) { const detail = (await response.text()).trim() throw new Error( `contrast-bundler: the swift compile for ${work.entry} answered ${response.status}${detail ? `: ${detail}` : ''}`, ) } return (await response.json()) as SwiftCompileOutcome } } export interface SwiftModuleAddress { /** absolute url the tenant fetches the wasm from. */ url: string hash: string /** bundle asset name to absolute photo url, when the pack publishes any. */ imageUrls?: Record } /** the service answers root-relative; the caller that knows the origin resolves. */ function resolveImageUrls( urls: Record, origin: string, ): Record { const resolved: Record = {} for (const [name, url] of Object.entries(urls)) { resolved[name] = new URL(url, origin).href } return resolved } /** * one project's swift sources as a published artifact address. * * `origin` is the origin the artifact route answers on — the app's own origin * for the browser worker, the caller's compile origin for a node build. The * contract's url is root-relative because the compile host has no browser * origin in front of it, so the resolution happens here, at the one caller * that knows both. */ export async function compileSwiftProject( submit: SwiftCompileSubmit, work: { files: Record resources?: Record images?: Record entry: string }, origin: string, ): Promise { while (true) { const outcome = await submit(work) switch (outcome.status) { case 'published': return { url: new URL(outcome.artifactUrl, origin).href, hash: outcome.hash, ...(outcome.imageUrls === undefined ? {} : { imageUrls: resolveImageUrls(outcome.imageUrls, origin) }), } case 'compiling': await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) continue case 'refused': case 'failed': throw new Error( `contrast-bundler: the swift sources for ${work.entry} cannot be compiled: ${outcome.detail}`, ) } } }