import type { BuiltManifest } from './manifest-builder'; import { registeredComponent, entryKey, type CompileContext, type MetadataEntry, } from './definitions/component'; // Existing CLI loaders use this stable marker to request deferred source compilation. const manifestBuilds = Symbol.for('@rippling/sdk/package-sources'); type DeferredBuild = { readonly index: number; readonly entry: MetadataEntry }; /** Compose new definitions; legacy ManifestBuilder remains an independent authoring flow. */ export type ManifestInput = object; export interface DefineManifestOptions { components: readonly ManifestInput[]; } export interface ManifestDefinition extends BuiltManifest { components: Array & { type: string }>; readonly [manifestBuilds]?: readonly DeferredBuild[]; } /** Run the build hooks supplied by component implementations, preserving the metadata JSON contract. */ export async function compileManifest( definition: Readonly, bundle: (entrypoint: URL) => Promise, ): Promise> { const builds = definition[manifestBuilds] ?? []; if (builds.length === 0) return definition; const compiled: ManifestDefinition = JSON.parse(JSON.stringify({ ...definition })); for (const { index, entry } of builds) { const config = await entry.build!({ bundle }); compiled.components[index] = JSON.parse(JSON.stringify(config)); } return freezeManifest(compiled); } function canonicalJson(value: unknown): string { return JSON.stringify(value, (_key, item: unknown) => item != null && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([left], [right]) => left.localeCompare(right))) : item, ); } /** Collect references, then compile batches. Component-specific behavior lives in definitions/. */ export function defineManifest(options: DefineManifestOptions): Readonly { const definitions: object[] = []; const visited = new Set(); const batches = new Map< object, { implementation: NonNullable>['implementation']; definitions: object[]; } >(); const entries: MetadataEntry[] = []; const visit = (definition: object): void => { if (visited.has(definition)) return; visited.add(definition); const registered = registeredComponent(definition); if (registered != null) { definitions.push(definition); for (const dependency of registered.references()) visit(dependency); const implementation = registered.implementation; const batch = batches.get(implementation.batchKey) ?? { implementation, definitions: [] }; batch.definitions.push(definition); batches.set(implementation.batchKey, batch); } else { throw new Error( 'Use defineX helpers in defineManifest; existing constructors belong in ManifestBuilder.', ); } }; options.components.forEach(visit); const context: CompileContext = { definitions }; for (const batch of batches.values()) { for (const output of batch.implementation.compileBatch(batch.definitions, context)) entries.push('type' in output ? { config: output } : output); } const snapshots: ManifestDefinition['components'] = JSON.parse( JSON.stringify(entries.map((entry) => entry.config)), ); const unique = new Map< string, { config: ManifestDefinition['components'][number]; entry: MetadataEntry } >(); for (const [index, config] of snapshots.entries()) { const entry = entries[index]!; const key = entryKey(entry); const existing = unique.get(key); if (existing != null) { for (const parent of ['model_name', 'model_rql_name', 'object_rql_name']) { if (existing.config[parent] !== config[parent]) throw new Error(`Conflicting manifest component ${key}: apiName must be unique across objects.`); } if (entry.fallback && !existing.entry.fallback) continue; if (!entry.fallback && existing.entry.fallback) { unique.set(key, { config, entry }); continue; } if (canonicalJson(existing.config) !== canonicalJson(config)) throw new Error(`Conflicting manifest component ${key}`); if (existing.entry.build != null && entry.build != null && existing.entry.buildKey !== entry.buildKey) throw new Error(`Conflicting manifest source ${key}`); if (entry.build == null && existing.entry.build != null) continue; } unique.set(key, { config, entry }); } const builds = [...unique.values()].flatMap(({ entry }, index) => entry.build == null ? [] : [{ index, entry }], ); const manifest: ManifestDefinition = { metadata: {}, components: [...unique.values()].map(({ config }) => config), }; if (builds.length > 0) { Object.defineProperty(manifest, manifestBuilds, { value: Object.freeze(builds) }); Object.defineProperty(manifest, 'toJSON', { value(): never { throw new Error( 'Manifest source entrypoints must be compiled before JSON export. Use rippling project export or compileManifest.', ); }, }); } return freezeManifest(manifest); } function freezeManifest(value: T): T { if (value != null && typeof value === 'object') { Object.values(value).forEach(freezeManifest); Object.freeze(value); } return value; }