import type { ManifestComponentType, ManifestScope, ManifestComponent } from '../manifest-builder'; export type ComponentType = ManifestComponentType | 'CUSTOM_OBJECT_SETTINGS' | 'CUSTOM_OBJECT_SEARCH_CONFIG'; /** New definitions compose explicitly; legacy constructors still register through their original scopes. */ export const detachedManifestScope: ManifestScope = { _register() {} }; /** Shared by components whose backend writers require the custom API-name suffix. */ export function validateCustomApiName(apiName: string): void { if (typeof apiName !== 'string' || !apiName.endsWith('__c')) throw new Error(`apiName must end with __c: ${apiName}`); } /** Decorate only a newly created instance; the existing class and serializer stay unchanged. */ export function withApiName( definition: D, apiName: string, extra: Record = {}, ) { const serialize = definition.toDict.bind(definition); return Object.assign(definition, { getApiName: () => apiName, toDict: () => ({ ...serialize(), ...extra, api_name: apiName, identity_format_version: 2 as const }), }); } /** Identifying fields belong to the component's serialized metadata. */ export type MetadataConfig = | { readonly type: Exclude< ComponentType, | 'CUSTOM_OBJECT_FIELD' | 'CUSTOM_OBJECT_FIELD_SECTION' | 'CUSTOM_OBJECT_ACTION' | 'CUSTOM_OBJECT_LIST_VIEW' | 'CUSTOM_OBJECT_PAGE_LAYOUT' | 'CUSTOM_OBJECT_RECORD_TRIGGERED_FLOW' | 'CUSTOM_OBJECT_VALIDATION_RULE' | 'CUSTOM_FIELD_SECTION' | 'CUSTOM_FIELD_V2' | 'CUSTOM_OBJECT_SETTINGS' | 'CUSTOM_OBJECT_SEARCH_CONFIG' >; readonly api_name: string; } | { readonly type: 'CUSTOM_OBJECT_FIELD'; readonly custom_object_api_name: string; readonly field_api_name: string; } | { readonly type: 'CUSTOM_OBJECT_FIELD_SECTION'; readonly api_name: string; readonly model_name: string; } | { readonly type: 'CUSTOM_OBJECT_ACTION' | 'CUSTOM_OBJECT_LIST_VIEW' | 'CUSTOM_OBJECT_PAGE_LAYOUT'; readonly api_name: string; readonly object_rql_name: string; } | { readonly type: | 'CUSTOM_OBJECT_RECORD_TRIGGERED_FLOW' | 'CUSTOM_OBJECT_VALIDATION_RULE' | 'CUSTOM_FIELD_SECTION' | 'CUSTOM_FIELD_V2'; readonly api_name: string; readonly model_rql_name: string; } | { readonly type: 'CUSTOM_OBJECT_SETTINGS'; readonly object_rql_name: string; readonly setting_id: string } | { readonly type: 'CUSTOM_OBJECT_SEARCH_CONFIG'; readonly model_rql_name: string }; export interface BuildContext { bundle(entrypoint: URL): Promise; } export interface MetadataEntry { readonly config: W; /** Generated defaults yield to authored metadata with the same type, parent, and API name. */ readonly fallback?: boolean; readonly buildKey?: string; readonly build?: (context: BuildContext) => Promise; } export interface CompileContext { readonly definitions: readonly object[]; } type DefinitionFactory = (...args: never[]) => object; interface ComponentFactory { /** Wire kind used to register this adapter; does not register an API endpoint. */ readonly type: ComponentType; /** Creates the public definition. The wrapper registers the result without adding public properties. */ readonly define: F; /** * Receives the factory result and its original arguments. Return fixed references * or a callback that reads current dependencies when setters can change them. */ references?( definition: ReturnType, ...args: Parameters ): readonly object[] | (() => readonly object[]); } /** Use a single-definition serializer normally; batch compilers handle schema expansion and merging. */ export type ComponentImplementation< F extends DefinitionFactory, W extends MetadataConfig, > = ComponentFactory & ( | { serialize(definition: ReturnType): W | MetadataEntry; compileBatch?: never; } | { serialize?: never; compileBatch( definitions: readonly ReturnType[], context: CompileContext, ): readonly (W | MetadataEntry)[]; } | (ReturnType extends ManifestComponent ? { serialize?: never; compileBatch?: never } : never) ); /** Keep public declarations compact without expanding generic factories into their field unions. */ export interface ComponentDefinition { readonly define: F; /** @internal Register a constructed value, preserving its concrete generic type. */ register(definition: D, ...args: unknown[]): D; } interface RegisteredComponent { readonly type: ComponentType; references(): readonly object[]; readonly implementation: { readonly batchKey: object; compileBatch( definitions: readonly object[], context: CompileContext, ): readonly (MetadataConfig | MetadataEntry)[]; }; } // Shared across SDK copies, and works for frozen schema definitions without mutating them. const registryKey = Symbol.for('@rippling/sdk/component-registry/v3'); const shared = globalThis as typeof globalThis & { [registryKey]?: WeakMap }; const registry = (shared[registryKey] ??= new WeakMap()); export function registeredComponent(definition: object): RegisteredComponent | undefined { return registry.get(definition); } /** Retains the exact factory signature, including its generic type parameters. */ export function defineComponent( implementation: ComponentImplementation, ): ComponentDefinition { const adapter = { batchKey: implementation.compileBatch ?? implementation, compileBatch( definitions: readonly object[], context: CompileContext, ): readonly (MetadataConfig | MetadataEntry)[] { // Type erasure is confined to registry dispatch; authoring and each implementation stay typed. const typed = definitions as ReturnType[]; if (implementation.compileBatch != null) return implementation.compileBatch(typed, context); return typed.map((definition) => implementation.serialize != null ? implementation.serialize(definition) : ((definition as ManifestComponent).toDict() as W), ); }, }; const register = (definition: D, ...args: unknown[]): D => { const references = implementation.references?.(definition as ReturnType, ...(args as Parameters)) ?? []; const snapshot = typeof references === 'function' ? references : Object.freeze([...references]); registry.set(definition, { type: implementation.type, references: typeof snapshot === 'function' ? snapshot : () => snapshot, implementation: adapter, }); return definition; }; const define = new Proxy(implementation.define, { apply(target, receiver, args) { return register(Reflect.apply(target, receiver, args) as ReturnType, ...(args as Parameters)); }, }); return { define, register }; } /** Mirror backend identity and installation-map scopes, using wire fields rather than source IDs. */ export function entryKey({ config }: MetadataEntry): string { switch (config.type) { case 'CUSTOM_OBJECT_FIELD': return JSON.stringify([config.type, config.custom_object_api_name, config.field_api_name]); case 'CUSTOM_OBJECT_FIELD_SECTION': case 'CUSTOM_OBJECT_ACTION': case 'CUSTOM_OBJECT_LIST_VIEW': case 'CUSTOM_OBJECT_RECORD_TRIGGERED_FLOW': case 'CUSTOM_OBJECT_VALIDATION_RULE': // These components still use company-wide API-name lookups or installation maps. return JSON.stringify([config.type, config.api_name]); case 'CUSTOM_OBJECT_PAGE_LAYOUT': return JSON.stringify([config.type, config.object_rql_name, config.api_name]); case 'CUSTOM_FIELD_SECTION': case 'CUSTOM_FIELD_V2': return JSON.stringify([config.type, config.model_rql_name, config.api_name]); case 'CUSTOM_OBJECT_SETTINGS': return JSON.stringify([config.type, config.object_rql_name]); case 'CUSTOM_OBJECT_SEARCH_CONFIG': return JSON.stringify([config.type, config.model_rql_name]); default: return JSON.stringify([config.type, config.api_name]); } } /** Deferred builds must retain the same detached configuration snapshot as manifest composition. */ function snapshotMetadata(config: W): W { return JSON.parse(JSON.stringify(config)) as W; } /** Shared source-build plumbing; each component supplies its own wire format. */ export function sourceEntry( metadata: W, source: URL | undefined, build: (config: W, content: string) => W, ): MetadataEntry { const config = snapshotMetadata(metadata); const href = source?.href; return { config, ...(href === undefined ? {} : { buildKey: href, build: async ({ bundle }: BuildContext) => build(config, await bundle(new URL(href))), }), }; }