/** * Materialize the `views` map (`defineView` entries) into the two halves a view * DOMAIN MEMBER contributes — mirroring `extend-functions.ts`: * * - `ViewDeclarations` — the CONTRACT half, fed to `compileDomain`'s members * arg. The member ref (`view.`), the layout (`//views/`), * and the `of_domain` edge slug all derive from the map key. * - `ViewSchema[]` + per-slug `FunctionBinding` — the IMPL half, fed to * `serialize`. A view carries more than a function: a `UI.handshake` mode and * `view_for` target edges (resolved here to graph paths). The worker route is * `//`, DECOUPLED from the graph layout. * * A view is NOT a core/instance node: the kernel-core serializer attaches it to * the Domain via `of_domain` (slug `view.`) and emits its `view_for` edges, * so this module never touches `Core`. */ import type { FunctionBinding } from '@astrale-os/kernel-api/routed' import type { ViewDeclarations, ViewSchema } from '@astrale-os/kernel-core/domain' import type { Schema } from '@astrale-os/kernel-dsl' import { DomainPaths } from '@astrale-os/kernel-core/domain' import { buildDefOriginMap, scanImportedDefs } from '@astrale-os/kernel-core/domain' import { buildDefDescriptorMap, isAbstract, isCorePath, isSelfMarker, type EdgeEndpoint, } from '@astrale-os/kernel-dsl' import type { ViewDef } from '../define/view.js' import { joinWorkerPath, resolveBinding } from './binding.js' /** * Default URL segment for a view's worker route (`/views/`). Names the * HTTP route only — the graph layout (`//views/`) is a fixed kernel * convention, independent of this. */ export const DEFAULT_VIEWS_FOLDER = 'views' const SLUG_RE = /^[a-z][a-z0-9-]*$/ // oxlint-disable-next-line no-explicit-any type AnyViewDef = ViewDef function assertValidSlugs(views: Record): void { for (const slug of Object.keys(views)) { if (!SLUG_RE.test(slug)) { throw new Error(`defineRemoteDomain: invalid view slug "${slug}" — must match ${SLUG_RE}.`) } } } /** Enforce ViewDef's documented exclusivity: `mount` cannot combine with `render`/`binding`. */ function assertExclusivity(slug: string, def: AnyViewDef): void { if (def.mount && (def.render || def.binding)) { throw new Error( `defineRemoteDomain: view "${slug}" sets \`mount\` together with ` + `${def.render ? '`render`' : '`binding`'} — they are mutually exclusive.`, ) } } /** * Contract half — one declaration per `views` map key, for `compileDomain`'s * `views` arg. The slug drives ref/path/of_domain; impl/binding/view_for arrive * at serialize. */ export function buildViewDeclarations(views: Record): ViewDeclarations { assertValidSlugs(views) const out: Record = {} for (const slug of Object.keys(views)) out[slug] = { slug } return out } /** * Impl half — `ViewSchema[]` for `serialize` plus the per-slug `FunctionBinding` * map the worker mounts routes from. `viewFor` markers are resolved here to * concrete target paths (class meta-nodes) because the kernel-core serializer is * pure and does not resolve `defineCore` endpoints. */ export function buildViewSchemas( views: Record, url: string, viewsFolder: string, schema: Schema, ): { schemas: ViewSchema[]; bindings: Record } { const schemas: ViewSchema[] = [] const bindings: Record = {} for (const [slug, def] of Object.entries(views)) { assertExclusivity(slug, def) // `mount` is a worker-relative SPA path → ``; otherwise the // default/overridden `//`. const binding = def.mount ? { remoteUrl: joinWorkerPath(url, def.mount) } : resolveBinding(def.binding, url, viewsFolder, slug) bindings[slug] = binding // Inline `render` views ship no shell client → default handshake 'none'. const handshake = def.handshake ?? (def.render ? 'none' : undefined) schemas.push({ ref: `view.${slug}`, name: slug, binding, ...(handshake ? { handshake } : {}), viewFor: resolveViewForTargets(def.viewFor, schema), }) } return { schemas, bindings } } /** * Resolve `defineView`'s `viewFor` endpoints to concrete graph path raws. * Mirrors kernel-core `compileCore`'s endpoint resolution (the old channel that * carried view_for as core edges): `selfOf(Def)` → the def's class/interface * meta-node `//{class,interface}./self`; a `CorePath` * string → its anchored graph path. Empty when the view targets nothing. */ function resolveViewForTargets( viewFor: EdgeEndpoint | readonly EdgeEndpoint[] | undefined, schema: Schema, ): string[] { if (!viewFor) return [] const targets = Array.isArray(viewFor) ? viewFor : [viewFor] if (targets.length === 0) return [] const descriptorMap = buildDefDescriptorMap(schema) const defOrigins = buildDefOriginMap(schema, scanImportedDefs(schema)) return targets.map((target) => { if (isSelfMarker(target)) { const def = target.__def const desc = descriptorMap.get(def) if (!desc) { throw new Error( 'defineView: `viewFor: selfOf(...)` references a def that is neither in this ' + "schema nor any of its `imports`. Add the def's schema to `imports`.", ) } const origin = defOrigins.get(def) ?? schema.domain const dp = DomainPaths.of(origin) return isAbstract(def) ? dp.interface(desc.name).raw : dp.class(desc.name).raw } if (isCorePath(target)) { return DomainPaths.of(schema.domain).corePath(target).raw } throw new Error( 'defineView: unsupported `viewFor` endpoint — use `selfOf(Class)` or a CorePath.', ) }) }