social components inlay.at
atproto components sdui
86
fork

Configure Feed

Select the types of activity you want to include in your feed.

replace packs with dids

+485 -1070
+9 -28
README.md
··· 40 40 - **Template** — a stored element tree with Binding placeholders that get filled in with props. 41 41 - **External** — an XRPC endpoint that receives props and returns an element tree. 42 42 43 - Each component also imports a list of [packs](https://pdsls.dev/at://did:plc:mdg3w2kpadcyxy33pizokzf3/com.atproto.lexicon.schema/at.inlay.pack#schema). A pack maps NSIDs to component URIs — it's how the host knows which implementation to use. 43 + Each component also imports an ordered list of DIDs. When the host needs to resolve a child NSID, it walks the import list and looks up `at://{did}/at.inlay.component/{nsid}` — first match wins. 44 44 45 45 [Atsui (`org.atsui`)](https://pdsls.dev/at://did:plc:e4fjueijznwqm2yxvt7q4mba/com.atproto.lexicon.schema) is the first design system for Inlay — including `<Text>`, `<Avatar>`, `<List>`, and others. In a sense, it's like Inlay's HTML. Atsui is not a traditional component library because it solely defines *lexicons* (i.e. interfaces). Each host (an Inlay browser like [this demo](https://inlay-proto.up.railway.app/)) may choose which components are built-in, and how they work. A host could even decide to not implement Atsui, and instead to implement another set of primitives. 46 46 ··· 52 52 53 53 ### Pick an NSID 54 54 55 - An NSID describes *what* a component does, not *how*. If someone already defined one that fits (e.g. `com.pfrazee.BskyPost` with a `uri` prop), you can implement it yourself. Multiple implementations of the same NSID can coexist — packs control which one gets used. 55 + An NSID describes *what* a component does, not *how*. If someone already defined one that fits (e.g. `com.pfrazee.BskyPost` with a `uri` prop), you can implement it yourself. Multiple implementations of the same NSID can coexist — the importer's DID list controls which one gets used. 56 56 57 57 If you're creating a new NSID, pick one under a domain you control so you can [publish a Lexicon](https://atproto.com/specs/lexicon#lexicon-publication-and-resolution) for it later. 58 58 ··· 67 67 ```json 68 68 { 69 69 "$type": "at.inlay.component", 70 - "type": "mov.danabra.Greeting", 71 70 "body": { 72 71 "$type": "at.inlay.component#bodyTemplate", 73 72 "node": { ··· 91 90 } 92 91 }, 93 92 "imports": [ 94 - "at://did:plc:mdg3w2kpadcyxy33pizokzf3/at.inlay.pack/3me7vlpfkcj25", 95 - "at://did:plc:e4fjueijznwqm2yxvt7q4mba/at.inlay.pack/3me3pkxzcf22m" 93 + "did:plc:mdg3w2kpadcyxy33pizokzf3", 94 + "did:plc:e4fjueijznwqm2yxvt7q4mba" 96 95 ] 97 96 } 98 97 ``` 99 98 100 - The `imports` array lists the packs your component needs — typically the [Atsui pack](https://pdsls.dev/at/did:plc:e4fjueijznwqm2yxvt7q4mba/at.inlay.pack/3me3pkxzcf22m) so you can use [`org.atsui.*` primitives](https://pdsls.dev/at://did:plc:e4fjueijznwqm2yxvt7q4mba/com.atproto.lexicon.schema). 99 + The `imports` array lists the DIDs your component needs for resolution — typically the [Atsui DID](https://pdsls.dev/at/did:plc:e4fjueijznwqm2yxvt7q4mba) so you can use [`org.atsui.*` primitives](https://pdsls.dev/at://did:plc:e4fjueijznwqm2yxvt7q4mba/com.atproto.lexicon.schema). The record's rkey is the NSID it implements (e.g. `mov.danabra.Greeting`), so the full URI is `at://{your-did}/at.inlay.component/mov.danabra.Greeting`. 101 100 102 101 When [rendered](packages/@inlay/render) as `<mov.danabra.Greeting name="world" />`, this component resolves to: 103 102 ··· 112 111 113 112 As an alternative to templates (which are very limited), you can declare components as [XRPC](https://atproto.com/guides/glossary#xrpc) server endpoints. Then the Inlay host will hit your endpoint to resolve the component tree. 114 113 115 - Here's a [real external component record](https://pdsls.dev/at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/at.inlay.component/3mfoxe7h4fsj6) for `mov.danabra.Greeting`. Notice its `body` points to a service `did` instead of an inline `node`: 114 + Here's a [real external component record](https://pdsls.dev/at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/at.inlay.component/mov.danabra.Greeting) for `mov.danabra.Greeting`. Notice its `body` points to a service `did` instead of an inline `node`: 116 115 117 116 ```json 118 117 { 119 118 "$type": "at.inlay.component", 120 - "type": "mov.danabra.Greeting", 121 119 "body": { 122 120 "$type": "at.inlay.component#bodyExternal", 123 121 "did": "did:web:gaearon--019c954e8a7877ea8d8d19feb1d4bbbb.web.val.run" 124 122 }, 125 123 "imports": [ 126 - "at://did:plc:mdg3w2kpadcyxy33pizokzf3/at.inlay.pack/3me7vlpfkcj25", 127 - "at://did:plc:e4fjueijznwqm2yxvt7q4mba/at.inlay.pack/3me3pkxzcf22m" 124 + "did:plc:mdg3w2kpadcyxy33pizokzf3", 125 + "did:plc:e4fjueijznwqm2yxvt7q4mba" 128 126 ] 129 127 } 130 128 ``` ··· 150 148 151 149 It's recommended to tag XRPC return values as cacheable; see [`@inlay/cache`](packages/@inlay/cache) for how to do this. In the calling code, it's recommended to wrap XRPC components into `<at.inlay.Loading fallback=...>` so that they don't block the entire page. 152 150 153 - ### Pack record 154 - 155 - To let other components use yours, add it to a pack. Here's the [`danabra.mov/ui` pack](https://pdsls.dev/at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/at.inlay.pack/3meflbzhr7c2f): 156 - 157 - ```json 158 - { 159 - "$type": "at.inlay.pack", 160 - "name": "ui", 161 - "exports": [ 162 - { "type": "mov.danabra.Greeting", "component": "at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/at.inlay.component/3mfoxe7h4fsj6" }, 163 - ... 164 - ] 165 - } 166 - ``` 167 - 168 - Other components import this pack URI in their `imports` array to get access to `mov.danabra.Greeting`. 169 - 170 151 ### Lexicon (optional) 171 152 172 153 A [Lexicon](https://atproto.com/specs/lexicon) defines the prop schema for an NSID. You don't need one to get started, but publishing one enables type checking, validation, and codegen with [`@atproto/lex`](https://www.npmjs.com/package/@atproto/lex): ··· 201 182 202 183 ## Rendering as a host 203 184 204 - A host walks an element tree, resolves each component through its packs, and maps the resulting primitives to output (HTML, React, etc). 185 + A host walks an element tree, resolves each component through its import stack (DIDs), and maps the resulting primitives to output (HTML, React, etc). 205 186 206 187 [`@inlay/render`](packages/@inlay/render) does the resolution step — call `render()` on an element, get back the expanded tree or a primitive. The host calls it in a loop until everything is resolved. The [render README](packages/@inlay/render) has a minimal working example that turns Inlay elements into HTML. 207 188
-1
generated/at/inlay.ts
··· 3 3 */ 4 4 5 5 export * as Binding from "./inlay/Binding"; 6 - export * as pack from "./inlay/pack"; 7 6 export * as Fragment from "./inlay/Fragment"; 8 7 export * as Loading from "./inlay/Loading"; 9 8 export * as Maybe from "./inlay/Maybe";
+7 -13
generated/at/inlay/component.defs.ts
··· 9 9 10 10 export { $nsid }; 11 11 12 - /** Component record - declares an implementation of a type */ 12 + /** Component record. The rkey is the type NSID this component implements. */ 13 13 type Main = { 14 14 $type: "at.inlay.component"; 15 - 16 - /** 17 - * NSID this component implements (also the XRPC procedure) 18 - */ 19 - type: l.NsidString; 20 15 21 16 /** 22 17 * How this component is rendered. Omit for primitives rendered by the host. ··· 32 27 view?: View; 33 28 34 29 /** 35 - * Ordered list of pack URIs (import stack). First pack that exports an NSID wins. 30 + * Ordered list of DIDs (import stack). For each NSID, the first DID that has a component with that rkey wins. 36 31 */ 37 - imports?: l.AtUriString[]; 32 + imports?: l.DidString[]; 38 33 39 34 /** 40 35 * Platform-managed deployment metadata ··· 51 46 52 47 export type { Main }; 53 48 54 - /** Component record - declares an implementation of a type */ 55 - const main = l.record<"tid", Main>( 56 - "tid", 49 + /** Component record. The rkey is the type NSID this component implements. */ 50 + const main = l.record<"nsid", Main>( 51 + "nsid", 57 52 $nsid, 58 53 l.object({ 59 - type: l.string({ format: "nsid" }), 60 54 body: l.optional( 61 55 l.typedUnion( 62 56 [ ··· 67 61 ) 68 62 ), 69 63 view: l.optional(l.ref<View>((() => view) as any)), 70 - imports: l.optional(l.array(l.string({ format: "at-uri" }))), 64 + imports: l.optional(l.array(l.string({ format: "did" }))), 71 65 via: l.optional( 72 66 l.typedUnion( 73 67 [l.typedRef<InlayDefs.ViaValtown>((() => InlayDefs.viaValtown) as any)],
-80
generated/at/inlay/pack.defs.ts
··· 1 - /* 2 - * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT. 3 - */ 4 - 5 - import { l } from "@atproto/lex"; 6 - 7 - const $nsid = "at.inlay.pack"; 8 - 9 - export { $nsid }; 10 - 11 - /** A list of type to component exports */ 12 - type Main = { 13 - $type: "at.inlay.pack"; 14 - 15 - /** 16 - * Short slug for the pack (e.g. "core", "ui") 17 - */ 18 - name: string; 19 - 20 - /** 21 - * Type to component mappings 22 - */ 23 - exports: Export$0[]; 24 - createdAt?: l.DatetimeString; 25 - }; 26 - 27 - export type { Main }; 28 - 29 - /** A list of type to component exports */ 30 - const main = l.record<"tid", Main>( 31 - "tid", 32 - $nsid, 33 - l.object({ 34 - name: l.string({ maxLength: 64 }), 35 - exports: l.array(l.ref<Export$0>((() => export$0) as any)), 36 - createdAt: l.optional(l.string({ format: "datetime" })), 37 - }) 38 - ); 39 - 40 - export { main }; 41 - 42 - export const $isTypeOf = /*#__PURE__*/ main.isTypeOf.bind(main), 43 - $build = /*#__PURE__*/ main.build.bind(main), 44 - $type = /*#__PURE__*/ main.$type; 45 - export const $assert = /*#__PURE__*/ main.assert.bind(main), 46 - $check = /*#__PURE__*/ main.check.bind(main), 47 - $cast = /*#__PURE__*/ main.cast.bind(main), 48 - $ifMatches = /*#__PURE__*/ main.ifMatches.bind(main), 49 - $matches = /*#__PURE__*/ main.matches.bind(main), 50 - $parse = /*#__PURE__*/ main.parse.bind(main), 51 - $safeParse = /*#__PURE__*/ main.safeParse.bind(main), 52 - $validate = /*#__PURE__*/ main.validate.bind(main), 53 - $safeValidate = /*#__PURE__*/ main.safeValidate.bind(main); 54 - 55 - type Export$0 = { 56 - $type?: "at.inlay.pack#export"; 57 - 58 - /** 59 - * NSID of the type being exported 60 - */ 61 - type: l.NsidString; 62 - 63 - /** 64 - * AT-URI of the component record 65 - */ 66 - component: l.AtUriString; 67 - }; 68 - 69 - export type { Export$0 as Export }; 70 - 71 - const export$0 = l.typedObject<Export$0>( 72 - $nsid, 73 - "export", 74 - l.object({ 75 - type: l.string({ format: "nsid" }), 76 - component: l.string({ format: "at-uri" }), 77 - }) 78 - ); 79 - 80 - export { export$0 as "export" };
-6
generated/at/inlay/pack.ts
··· 1 - /* 2 - * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT. 3 - */ 4 - 5 - export * from "./pack.defs"; 6 - export * as $defs from "./pack.defs";
+5 -10
lexicons/at/inlay/component.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "record", 7 - "key": "tid", 8 - "description": "Component record - declares an implementation of a type", 7 + "key": "nsid", 8 + "description": "Component record. The rkey is the type NSID this component implements.", 9 9 "record": { 10 10 "type": "object", 11 - "required": ["type"], 11 + "required": [], 12 12 "properties": { 13 - "type": { 14 - "type": "string", 15 - "format": "nsid", 16 - "description": "NSID this component implements (also the XRPC procedure)" 17 - }, 18 13 "body": { 19 14 "type": "union", 20 15 "refs": ["#bodyExternal", "#bodyTemplate"], ··· 29 24 "type": "array", 30 25 "items": { 31 26 "type": "string", 32 - "format": "at-uri" 27 + "format": "did" 33 28 }, 34 - "description": "Ordered list of pack URIs (import stack). First pack that exports an NSID wins." 29 + "description": "Ordered list of DIDs (import stack). For each NSID, the first DID that has a component with that rkey wins." 35 30 }, 36 31 "via": { 37 32 "type": "union",
-50
lexicons/at/inlay/pack.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "at.inlay.pack", 4 - "defs": { 5 - "main": { 6 - "type": "record", 7 - "key": "tid", 8 - "description": "A list of type to component exports", 9 - "record": { 10 - "type": "object", 11 - "required": ["name", "exports"], 12 - "properties": { 13 - "name": { 14 - "type": "string", 15 - "maxLength": 64, 16 - "description": "Short slug for the pack (e.g. \"core\", \"ui\")" 17 - }, 18 - "exports": { 19 - "type": "array", 20 - "items": { 21 - "type": "ref", 22 - "ref": "#export" 23 - }, 24 - "description": "Type to component mappings" 25 - }, 26 - "createdAt": { 27 - "type": "string", 28 - "format": "datetime" 29 - } 30 - } 31 - } 32 - }, 33 - "export": { 34 - "type": "object", 35 - "required": ["type", "component"], 36 - "properties": { 37 - "type": { 38 - "type": "string", 39 - "format": "nsid", 40 - "description": "NSID of the type being exported" 41 - }, 42 - "component": { 43 - "type": "string", 44 - "format": "at-uri", 45 - "description": "AT-URI of the component record" 46 - } 47 - } 48 - } 49 - } 50 - }
+11 -21
packages/@inlay/render/README.md
··· 10 10 11 11 ## Example 12 12 13 - A minimal Hono server that accepts an element, resolves it through packs, and returns HTML. Uses Hono's JSX for safe output. 13 + A minimal Hono server that accepts an element, resolves it, and returns HTML. Uses Hono's JSX for safe output. 14 14 15 15 ```tsx 16 16 /** @jsxImportSource hono/jsx */ ··· 20 20 import { render } from "@inlay/render"; 21 21 import type { RenderContext } from "@inlay/render"; 22 22 23 - // Records — in-memory stand-in for AT Protocol 23 + // Records — in-memory stand-in for AT Protocol. 24 + // Each component's rkey is the NSID it implements. 24 25 const records = { 25 26 // Primitives: no body. The host renders these directly. 26 - "at://did:plc:host/at.inlay.component/stack": { 27 - $type: "at.inlay.component", type: "org.atsui.Stack", 27 + "at://did:plc:host/at.inlay.component/org.atsui.Stack": { 28 + $type: "at.inlay.component", 28 29 }, 29 - "at://did:plc:host/at.inlay.component/text": { 30 - $type: "at.inlay.component", type: "org.atsui.Text", 30 + "at://did:plc:host/at.inlay.component/org.atsui.Text": { 31 + $type: "at.inlay.component", 31 32 }, 32 33 33 34 // Template: a stored element tree with Binding placeholders. 34 - "at://did:plc:app/at.inlay.component/greeting": { 35 + "at://did:plc:app/at.inlay.component/com.example.Greeting": { 35 36 $type: "at.inlay.component", 36 - type: "com.example.Greeting", 37 37 body: { 38 38 $type: "at.inlay.component#bodyTemplate", 39 39 node: serializeTree( ··· 43 43 ) 44 44 ), 45 45 }, 46 - imports: ["at://did:plc:host/at.inlay.pack/main"], 47 - }, 48 - 49 - // Pack: maps NSIDs → component records 50 - "at://did:plc:host/at.inlay.pack/main": { 51 - $type: "at.inlay.pack", name: "host", 52 - exports: [ 53 - { type: "org.atsui.Stack", component: "at://did:plc:host/at.inlay.component/stack" }, 54 - { type: "org.atsui.Text", component: "at://did:plc:host/at.inlay.component/text" }, 55 - { type: "com.example.Greeting", component: "at://did:plc:app/at.inlay.component/greeting" }, 56 - ], 46 + imports: ["did:plc:host"], 57 47 }, 58 48 }; 59 49 ··· 112 102 const element = deserializeTree({ 113 103 $: "$", type: "com.example.Greeting", props: { name: "world" }, 114 104 }); 115 - const ctx = { imports: ["at://did:plc:host/at.inlay.pack/main"] }; 105 + const ctx = { imports: ["did:plc:host", "did:plc:app"] }; 116 106 const body = await renderNode(element, ctx); 117 107 return c.html(<html><body>{body}</body></html>); 118 108 }); ··· 148 138 `render()` resolves one element at a time. It returns `node: null` when the element is a primitive (the host renders it using `element.type` + `result.props`) or a non-null node tree when the element expanded into a subtree that needs more passes. The walk loop drives this to completion. 149 139 150 140 1. **Binding resolution** — `at.inlay.Binding` elements in props are resolved against the current scope. The concrete values are available on `result.props`. 151 - 2. **Type lookup** — the element's NSID is looked up across the import stack. First pack that exports the type wins. 141 + 2. **Type lookup** — the element's NSID is looked up across the import stack. For each DID, the renderer checks `at://{did}/at.inlay.component/{nsid}` — first match wins. 152 142 3. **Component rendering** — depends on the component's body: 153 143 - **No body** — a primitive; returns `node: null` with resolved props. 154 144 - **Template** — element tree expanded with prop bindings.
+16 -40
packages/@inlay/render/src/index.ts
··· 13 13 AtUri, 14 14 AtUriString, 15 15 AtIdentifierString, 16 + DidString, 16 17 NsidString, 17 18 ensureValidNsid, 18 19 ensureValidDid, ··· 24 25 View, 25 26 } from "../../../../generated/at/inlay/component.defs.js"; 26 27 import { viewRecord as viewRecordSchema } from "../../../../generated/at/inlay/component.defs.js"; 27 - import type { Main as PackRecord } from "../../../../generated/at/inlay/pack.defs.js"; 28 28 import type { CachePolicy } from "../../../../generated/at/inlay/defs.defs.js"; 29 29 30 30 // --- Public types --- ··· 54 54 /** 55 55 * Plain, serializable render context. 56 56 * 57 - * - `imports`: ordered pack URIs for type resolution 57 + * - `imports`: ordered DIDs for type resolution 58 58 * - `component`: when present, the first render uses this component directly 59 59 * (root render). Stripped from child contexts automatically. 60 60 * - `componentUri`: AT URI of the root component record. Passed to xrpc ··· 67 67 * component first — the "who rendered who" owner chain. 68 68 */ 69 69 export type RenderContext = { 70 - imports: AtUriString[]; 70 + imports: DidString[]; 71 71 component?: ComponentRecord; 72 72 componentUri?: string; 73 73 depth?: number; ··· 104 104 105 105 export function createContext( 106 106 component: ComponentRecord, 107 - componentUri?: string 107 + componentUri: string 108 108 ): RenderContext { 109 109 return { 110 110 imports: component.imports ?? [], ··· 136 136 137 137 // Root render: use component directly 138 138 if (ctx.component) { 139 - if (type !== ctx.component.type) { 140 - throw new Error( 141 - `render was given ${ctx.component.type}, cannot render ${type}` 142 - ); 139 + const nsid = new AtUri(ctx.componentUri!).rkey; 140 + if (type !== nsid) { 141 + throw new Error(`render was given ${nsid}, cannot render ${type}`); 143 142 } 144 143 errorStack = [type, ...(ctx.stack ?? [])]; 145 144 return await renderComponent( ··· 325 324 326 325 async function resolveType( 327 326 nsid: string, 328 - importStack: AtUriString[], 327 + importStack: DidString[], 329 328 resolver: Resolver 330 329 ): Promise<{ 331 330 componentUri: string; 332 - packUri: string; 333 331 component: ComponentRecord; 334 332 }> { 335 - const packPromises = importStack.map((uri) => resolver.fetchRecord(uri)); 333 + const uris = importStack.map( 334 + (did) => `at://${did}/at.inlay.component/${nsid}` as AtUriString 335 + ); 336 + const promises = uris.map((uri) => resolver.fetchRecord(uri)); 336 337 337 338 for (let i = 0; i < importStack.length; i++) { 338 - const pack = (await packPromises[i]) as PackRecord | null; 339 - if (!pack || !pack.exports) { 340 - continue; 341 - } 342 - 343 - const entry = pack.exports.find((e) => e.type === nsid); 344 - if (!entry) { 345 - continue; 346 - } 347 - 348 - const component = (await resolver.fetchRecord( 349 - entry.component 350 - )) as ComponentRecord | null; 351 - if (!component) { 352 - continue; 353 - } 354 - 355 - return { 356 - componentUri: entry.component, 357 - packUri: importStack[i], 358 - component, 359 - }; 339 + const component = (await promises[i]) as ComponentRecord | null; 340 + if (!component) continue; 341 + return { componentUri: uris[i], component }; 360 342 } 361 343 362 - throw new Error(`No pack exports type: ${nsid}`); 344 + throw new Error(`Unresolved type: ${nsid}`); 363 345 } 364 346 365 347 async function renderTemplate( ··· 373 355 const depth = ctx.depth ?? 0; 374 356 // Push this component onto the owner stack — it creates child elements. 375 357 const stack = [type, ...(ctx.stack ?? [])]; 376 - 377 - // Eagerly prefetch child packs so they overlap with record fetch 378 - const childImports = component.imports ?? []; 379 - for (const uri of childImports) { 380 - resolver.fetchRecord(uri).catch(() => {}); 381 - } 382 358 383 359 const tree = deserializeTree(body.node); 384 360
+433 -818
packages/@inlay/render/test/render.test.ts
··· 8 8 // is an element whose type is the NSID com.example.PostCard. An NSID is 9 9 // just a name; it says nothing about where the implementation lives. 10 10 // 11 - // A pack (at.inlay.pack) maps NSIDs to concrete implementations. Each 12 - // entry points an NSID at a component record URI (at://did/at.inlay.component/rkey), 13 - // so the same NSID can resolve to different implementations depending on context. 11 + // Resolution is DID-based. A component's `imports` is an ordered list of 12 + // DIDs. For a given NSID, the renderer constructs AT URIs by combining 13 + // each DID with the NSID as rkey: `at://{did}/at.inlay.component/{nsid}`. 14 + // All DIDs are tried concurrently; the first DID (in import order) that 15 + // has a component record wins. 14 16 // 15 17 // A component record (at.inlay.component) defines what happens when the 16 18 // element is rendered. It has one of three body kinds: ··· 22 24 // Resolution algorithm: 23 25 // 24 26 // 1. Start with an element like <com.example.PostCard uri="at://...">. 25 - // 2. Scan imported packs for a component that implements com.example.PostCard. 27 + // 2. For each DID in the import stack, try fetching 28 + // at://{did}/at.inlay.component/com.example.PostCard concurrently. 29 + // The first DID (in order) with a result wins. 26 30 // 3. Resolve the component record and switch based on body kind: 27 31 // a. no body → done, it's a primitive. 28 32 // b. bodyTemplate → substitute Binding placeholders with the ··· 54 58 type RenderContext, 55 59 type RenderOptions, 56 60 } from "@inlay/render"; 57 - import type { Main as PackRecord } from "../../../../generated/at/inlay/pack.defs.ts"; 58 61 import { 59 62 tagRecord, 60 63 tagLink, 61 64 } from "../../../../generated/at/inlay/defs.defs.ts"; 62 - import type { AtUriString } from "@atproto/syntax"; 65 + import type { AtUriString, DidString } from "@atproto/syntax"; 63 66 import type { Response as ComponentResponse } from "../../../../generated/at/inlay/defs.defs.ts"; 64 67 65 68 // ============================================================================ ··· 270 273 271 274 type TestMock = 272 275 | ComponentRecord 273 - | PackRecord 274 276 | Record<string, unknown> 275 277 | ((...args: any[]) => unknown); // XRPC 276 278 277 - // Tests work against an in-memory record store. The host design system pack 278 - // is always present — it maps each host primitive NSID to a component record 279 - // with no body (no body = primitive = host renders directly). 280 - const HOST_PACK_URI: AtUriString = "at://did:plc:host/at.inlay.pack/host"; 279 + // Tests work against an in-memory record store. The host design system DID 280 + // is always present — component records for each host primitive NSID live 281 + // at at://did:plc:host/at.inlay.component/{nsid} with no body (primitive). 282 + const HOST_DID = "did:plc:host"; 283 + const APP_DID = "did:plc:app"; 281 284 const HOST_RECORDS: Record<string, TestMock> = { 282 - // rkeys are semi-readable for tests, but are meant to be TIDs. 283 - ["at://did:plc:host/at.inlay.component/stck"]: { 285 + // Component rkeys are NSIDs — at://{did}/at.inlay.component/{nsid} 286 + [`at://${HOST_DID}/at.inlay.component/${Stack}`]: { 284 287 $type: "at.inlay.component", 285 - type: Stack, 286 288 }, 287 - ["at://did:plc:host/at.inlay.component/rw"]: { 289 + [`at://${HOST_DID}/at.inlay.component/${Row}`]: { 288 290 $type: "at.inlay.component", 289 - type: Row, 290 291 }, 291 - ["at://did:plc:host/at.inlay.component/grd"]: { 292 + [`at://${HOST_DID}/at.inlay.component/${Grid}`]: { 292 293 $type: "at.inlay.component", 293 - type: Grid, 294 294 }, 295 - ["at://did:plc:host/at.inlay.component/txt"]: { 295 + [`at://${HOST_DID}/at.inlay.component/${Text}`]: { 296 296 $type: "at.inlay.component", 297 - type: Text, 298 297 }, 299 - ["at://did:plc:host/at.inlay.component/lnk"]: { 298 + [`at://${HOST_DID}/at.inlay.component/${Link}`]: { 300 299 $type: "at.inlay.component", 301 - type: Link, 302 300 }, 303 - ["at://did:plc:host/at.inlay.component/lst"]: { 301 + [`at://${HOST_DID}/at.inlay.component/${List}`]: { 304 302 $type: "at.inlay.component", 305 - type: List, 306 303 }, 307 - ["at://did:plc:host/at.inlay.component/avtr"]: { 304 + [`at://${HOST_DID}/at.inlay.component/${Avatar}`]: { 308 305 $type: "at.inlay.component", 309 - type: Avatar, 310 306 }, 311 - ["at://did:plc:host/at.inlay.component/cvr"]: { 307 + [`at://${HOST_DID}/at.inlay.component/${Cover}`]: { 312 308 $type: "at.inlay.component", 313 - type: Cover, 314 309 }, 315 - ["at://did:plc:host/at.inlay.component/myb"]: { 310 + [`at://${HOST_DID}/at.inlay.component/${Maybe}`]: { 316 311 $type: "at.inlay.component", 317 - type: Maybe, 318 - }, 319 - [HOST_PACK_URI]: { 320 - $type: "at.inlay.pack", 321 - name: "host", 322 - exports: [ 323 - { type: Stack, component: "at://did:plc:host/at.inlay.component/stck" }, 324 - { type: Row, component: "at://did:plc:host/at.inlay.component/rw" }, 325 - { type: Grid, component: "at://did:plc:host/at.inlay.component/grd" }, 326 - { type: Text, component: "at://did:plc:host/at.inlay.component/txt" }, 327 - { type: Link, component: "at://did:plc:host/at.inlay.component/lnk" }, 328 - { type: List, component: "at://did:plc:host/at.inlay.component/lst" }, 329 - { type: Avatar, component: "at://did:plc:host/at.inlay.component/avtr" }, 330 - { type: Cover, component: "at://did:plc:host/at.inlay.component/cvr" }, 331 - { type: Maybe, component: "at://did:plc:host/at.inlay.component/myb" }, 332 - ], 333 312 }, 334 313 }; 335 314 ··· 388 367 return { resolver, options, log }; 389 368 } 390 369 391 - // Convenience: host pack + app-specific records. 370 + // Convenience: host DID records + app-specific records. 392 371 function world( 393 372 records: Record<AtUriString | `xrpc:${string}:${string}`, TestMock> = {} 394 373 ) { ··· 442 421 it("passes through with resolved props", async () => { 443 422 const stackComponent: ComponentRecord = { 444 423 $type: "at.inlay.component", 445 - type: Stack, 446 424 }; 447 425 const { options } = world(); 448 426 const output = await renderToCompletion( 449 427 $(Stack, { gap: "medium" }), 450 428 options, 451 - createContext(stackComponent) 429 + createContext( 430 + stackComponent, 431 + `at://${HOST_DID}/at.inlay.component/${Stack}` 432 + ) 452 433 ); 453 434 assert.deepEqual(output, h("div", { gap: "medium" })); 454 435 }); ··· 471 452 it("expands static element tree", async () => { 472 453 const greetingComponent: ComponentRecord = { 473 454 $type: "at.inlay.component", 474 - type: Greeting, 475 455 body: { 476 456 $type: "at.inlay.component#bodyTemplate", 477 457 node: serializeTree($(Text, { value: "Hello world" })), 478 458 }, 479 - imports: [HOST_PACK_URI], 459 + imports: [HOST_DID], 480 460 }; 481 461 482 462 const { options, log } = world({ 483 - ["at://did:plc:test/at.inlay.component/grtng"]: greetingComponent, 484 - ["at://did:plc:test/at.inlay.pack/app"]: { 485 - $type: "at.inlay.pack", 486 - name: "app", 487 - exports: [ 488 - { 489 - type: Greeting, 490 - component: "at://did:plc:test/at.inlay.component/grtng", 491 - }, 492 - ], 493 - }, 463 + [`at://${APP_DID}/at.inlay.component/${Greeting}`]: greetingComponent, 494 464 }); 495 465 496 466 const output = await renderToCompletion( 497 467 $(Greeting, {}), 498 468 options, 499 - createContext(greetingComponent) 469 + createContext( 470 + greetingComponent, 471 + `at://${APP_DID}/at.inlay.component/${Greeting}` 472 + ) 500 473 ); 501 474 502 475 assert.deepEqual(output, h("span", { value: "Hello world" })); 503 476 assertLog(log, [ 504 477 `lexicon ${Greeting}`, 505 - `fetch ${HOST_PACK_URI}`, 506 - `fetch ${"at://did:plc:host/at.inlay.component/txt"}`, 478 + `fetch at://${HOST_DID}/at.inlay.component/${Text}`, 507 479 `lexicon ${Text}`, 508 480 ]); 509 481 }); ··· 511 483 it("resolves bindings against element props", async () => { 512 484 const greetingComponent: ComponentRecord = { 513 485 $type: "at.inlay.component", 514 - type: Greeting, 515 486 body: { 516 487 $type: "at.inlay.component#bodyTemplate", 517 488 node: serializeTree( ··· 523 494 }) 524 495 ), 525 496 }, 526 - imports: [HOST_PACK_URI], 497 + imports: [HOST_DID], 527 498 }; 528 499 529 500 const { options } = world({ 530 - ["at://did:plc:test/at.inlay.component/grtng"]: greetingComponent, 531 - ["at://did:plc:test/at.inlay.pack/app"]: { 532 - $type: "at.inlay.pack", 533 - name: "app", 534 - exports: [ 535 - { 536 - type: Greeting, 537 - component: "at://did:plc:test/at.inlay.component/grtng", 538 - }, 539 - ], 540 - }, 501 + [`at://${APP_DID}/at.inlay.component/${Greeting}`]: greetingComponent, 541 502 }); 542 503 543 504 const output = await renderToCompletion( 544 505 $(Greeting, { name: "world" }), 545 506 options, 546 - createContext(greetingComponent) 507 + createContext( 508 + greetingComponent, 509 + `at://${APP_DID}/at.inlay.component/${Greeting}` 510 + ) 547 511 ); 548 512 549 513 assert.deepEqual( ··· 560 524 it("resolves nested property paths", async () => { 561 525 const greetingComponent: ComponentRecord = { 562 526 $type: "at.inlay.component", 563 - type: Greeting, 564 527 body: { 565 528 $type: "at.inlay.component#bodyTemplate", 566 529 node: serializeTree( ··· 569 532 }) 570 533 ), 571 534 }, 572 - imports: [HOST_PACK_URI], 535 + imports: [HOST_DID], 573 536 }; 574 537 575 538 const { options, log } = world({ 576 - ["at://did:plc:test/at.inlay.component/grtng"]: greetingComponent, 577 - ["at://did:plc:test/at.inlay.pack/app"]: { 578 - $type: "at.inlay.pack", 579 - name: "app", 580 - exports: [ 581 - { 582 - type: Greeting, 583 - component: "at://did:plc:test/at.inlay.component/grtng", 584 - }, 585 - ], 586 - }, 539 + [`at://${APP_DID}/at.inlay.component/${Greeting}`]: greetingComponent, 587 540 }); 588 541 589 542 const output = await renderToCompletion( 590 543 $(Greeting, { user: { displayName: "Alice" } }), 591 544 options, 592 - createContext(greetingComponent) 545 + createContext( 546 + greetingComponent, 547 + `at://${APP_DID}/at.inlay.component/${Greeting}` 548 + ) 593 549 ); 594 550 595 551 assert.deepEqual(output, h("span", { value: "Alice" })); 596 552 assertLog(log, [ 597 553 `lexicon ${Greeting}`, 598 - `fetch ${HOST_PACK_URI}`, 599 - `fetch ${"at://did:plc:host/at.inlay.component/txt"}`, 554 + `fetch at://${HOST_DID}/at.inlay.component/${Text}`, 600 555 `lexicon ${Text}`, 601 556 ]); 602 557 }); ··· 606 561 it("renders children passed as props", async () => { 607 562 const layoutComponent: ComponentRecord = { 608 563 $type: "at.inlay.component", 609 - type: Layout, 610 564 body: { 611 565 $type: "at.inlay.component#bodyTemplate", 612 566 node: serializeTree( 613 567 $(Stack, { children: $(Binding, { path: ["props", "children"] }) }) 614 568 ), 615 569 }, 616 - imports: [HOST_PACK_URI], 570 + imports: [HOST_DID], 617 571 }; 618 572 619 573 const { options } = world(); 620 574 const output = await renderToCompletion( 621 575 $(Layout, { children: $(Text, { value: "hello" }) }), 622 576 options, 623 - createContext(layoutComponent) 577 + createContext( 578 + layoutComponent, 579 + `at://${APP_DID}/at.inlay.component/${Layout}` 580 + ) 624 581 ); 625 582 626 583 assert.deepEqual( ··· 632 589 it("renders named element props", async () => { 633 590 const layoutComponent: ComponentRecord = { 634 591 $type: "at.inlay.component", 635 - type: Layout, 636 592 body: { 637 593 $type: "at.inlay.component#bodyTemplate", 638 594 node: serializeTree( ··· 644 600 }) 645 601 ), 646 602 }, 647 - imports: [HOST_PACK_URI], 603 + imports: [HOST_DID], 648 604 }; 649 605 650 606 const { options } = world(); ··· 654 610 right: $(Text, { value: "R" }), 655 611 }), 656 612 options, 657 - createContext(layoutComponent) 613 + createContext( 614 + layoutComponent, 615 + `at://${APP_DID}/at.inlay.component/${Layout}` 616 + ) 658 617 ); 659 618 660 619 assert.deepEqual( ··· 668 627 it("renders element props nested in objects", async () => { 669 628 const layoutComponent: ComponentRecord = { 670 629 $type: "at.inlay.component", 671 - type: Layout, 672 630 body: { 673 631 $type: "at.inlay.component#bodyTemplate", 674 632 node: serializeTree( ··· 680 638 }) 681 639 ), 682 640 }, 683 - imports: [HOST_PACK_URI], 641 + imports: [HOST_DID], 684 642 }; 685 643 686 644 const { options } = world(); ··· 692 650 }, 693 651 }), 694 652 options, 695 - createContext(layoutComponent) 653 + createContext( 654 + layoutComponent, 655 + `at://${APP_DID}/at.inlay.component/${Layout}` 656 + ) 696 657 ); 697 658 698 659 assert.deepEqual( ··· 706 667 it("can render same child twice", async () => { 707 668 const layoutComponent: ComponentRecord = { 708 669 $type: "at.inlay.component", 709 - type: Layout, 710 670 body: { 711 671 $type: "at.inlay.component#bodyTemplate", 712 672 node: serializeTree( ··· 718 678 }) 719 679 ), 720 680 }, 721 - imports: [HOST_PACK_URI], 681 + imports: [HOST_DID], 722 682 }; 723 683 724 684 const { options } = world(); 725 685 const output = await renderToCompletion( 726 686 $(Layout, { children: $(Text, { value: "x" }) }), 727 687 options, 728 - createContext(layoutComponent) 688 + createContext( 689 + layoutComponent, 690 + `at://${APP_DID}/at.inlay.component/${Layout}` 691 + ) 729 692 ); 730 693 731 694 assert.deepEqual( ··· 739 702 it("can ignore passed children", async () => { 740 703 const layoutComponent: ComponentRecord = { 741 704 $type: "at.inlay.component", 742 - type: Layout, 743 705 body: { 744 706 $type: "at.inlay.component#bodyTemplate", 745 707 node: serializeTree($(Text, { value: "static" })), 746 708 }, 747 - imports: [HOST_PACK_URI], 709 + imports: [HOST_DID], 748 710 }; 749 711 750 712 const { options } = world(); 751 713 const output = await renderToCompletion( 752 714 $(Layout, { children: $(Text, { value: "never seen" }) }), 753 715 options, 754 - createContext(layoutComponent) 716 + createContext( 717 + layoutComponent, 718 + `at://${APP_DID}/at.inlay.component/${Layout}` 719 + ) 755 720 ); 756 721 757 722 assert.deepEqual(output, h("span", { value: "static" })); ··· 765 730 // and merged into the binding scope. 766 731 const postCardComponent: ComponentRecord = { 767 732 $type: "at.inlay.component", 768 - type: PostCard, 769 733 body: { 770 734 $type: "at.inlay.component#bodyTemplate", 771 735 node: serializeTree( 772 736 $(Text, { value: $(Binding, { path: ["record", "text"] }) }) 773 737 ), 774 738 }, 775 - imports: [HOST_PACK_URI], 739 + imports: [HOST_DID], 776 740 view: { 777 741 $type: "at.inlay.component#view", 778 742 prop: "uri", ··· 790 754 text: "Hello world", 791 755 createdAt: "2026-02-22T00:00:00Z", 792 756 }, 793 - ["at://did:plc:test/at.inlay.component/grtng"]: postCardComponent, 794 - ["at://did:plc:test/at.inlay.pack/app"]: { 795 - $type: "at.inlay.pack", 796 - name: "app", 797 - exports: [ 798 - { 799 - type: PostCard, 800 - component: "at://did:plc:test/at.inlay.component/grtng", 801 - }, 802 - ], 803 - }, 804 757 }); 805 758 806 759 const output = await renderToCompletion( 807 760 $(PostCard, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 808 761 options, 809 - createContext(postCardComponent) 762 + createContext( 763 + postCardComponent, 764 + `at://${APP_DID}/at.inlay.component/${PostCard}` 765 + ) 810 766 ); 811 767 812 768 assert.deepEqual(output, h("span", { value: "Hello world" })); 813 769 assertLog(log, [ 814 770 `lexicon ${PostCard}`, 815 - // Pack and record fetched concurrently 816 - `fetch:start ${HOST_PACK_URI}`, 817 - `fetch:start ${"at://did:plc:alice/app.bsky.feed.post/123"}`, 818 - `fetch:end ${HOST_PACK_URI}`, 819 - `fetch:end ${"at://did:plc:alice/app.bsky.feed.post/123"}`, 820 - `fetch ${"at://did:plc:host/at.inlay.component/txt"}`, 771 + // Record fetch happens first, then component resolution 772 + `fetch:start at://did:plc:alice/app.bsky.feed.post/123`, 773 + `fetch:end at://did:plc:alice/app.bsky.feed.post/123`, 774 + `fetch at://${HOST_DID}/at.inlay.component/${Text}`, 821 775 `lexicon ${Text}`, 822 776 ]); 823 777 }); ··· 826 780 // A Binding for ["props", "uri", "$did"] decomposes the AT-URI string. 827 781 const postCardComponent: ComponentRecord = { 828 782 $type: "at.inlay.component", 829 - type: PostCard, 830 783 body: { 831 784 $type: "at.inlay.component#bodyTemplate", 832 785 node: serializeTree( 833 786 $(Text, { value: $(Binding, { path: ["props", "uri", "$did"] }) }) 834 787 ), 835 788 }, 836 - imports: [HOST_PACK_URI], 789 + imports: [HOST_DID], 837 790 view: { 838 791 $type: "at.inlay.component#view", 839 792 prop: "uri", ··· 846 799 }, 847 800 }; 848 801 849 - const { options, log } = world({ 850 - ["at://did:plc:test/at.inlay.component/grtng"]: postCardComponent, 851 - ["at://did:plc:test/at.inlay.pack/app"]: { 852 - $type: "at.inlay.pack", 853 - name: "app", 854 - exports: [ 855 - { 856 - type: PostCard, 857 - component: "at://did:plc:test/at.inlay.component/grtng", 858 - }, 859 - ], 860 - }, 861 - }); 802 + const { options, log } = world(); 862 803 863 804 const output = await renderToCompletion( 864 805 $(PostCard, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 865 806 options, 866 - createContext(postCardComponent) 807 + createContext( 808 + postCardComponent, 809 + `at://${APP_DID}/at.inlay.component/${PostCard}` 810 + ) 867 811 ); 868 812 869 813 assert.deepEqual(output, h("span", { value: "did:plc:alice" })); 870 814 assertLog(log, [ 871 815 `lexicon ${PostCard}`, 872 - `fetch ${HOST_PACK_URI}`, 873 - `fetch ${"at://did:plc:host/at.inlay.component/txt"}`, 816 + `fetch at://${HOST_DID}/at.inlay.component/${Text}`, 874 817 `lexicon ${Text}`, 875 818 ]); 876 819 }); ··· 879 822 // A Binding for ["record", "text"] requires fetching the record. 880 823 const postCardComponent: ComponentRecord = { 881 824 $type: "at.inlay.component", 882 - type: PostCard, 883 825 body: { 884 826 $type: "at.inlay.component#bodyTemplate", 885 827 node: serializeTree( ··· 890 832 }) 891 833 ), 892 834 }, 893 - imports: [HOST_PACK_URI], 835 + imports: [HOST_DID], 894 836 view: { 895 837 $type: "at.inlay.component#view", 896 838 prop: "uri", ··· 907 849 ["at://did:plc:alice/app.bsky.feed.post/123"]: { 908 850 text: "Hello world", 909 851 }, 910 - ["at://did:plc:test/at.inlay.component/grtng"]: postCardComponent, 911 - ["at://did:plc:test/at.inlay.pack/app"]: { 912 - $type: "at.inlay.pack", 913 - name: "app", 914 - exports: [ 915 - { 916 - type: PostCard, 917 - component: "at://did:plc:test/at.inlay.component/grtng", 918 - }, 919 - ], 920 - }, 921 852 }); 922 853 923 854 const output = await renderToCompletion( 924 855 $(PostCard, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 925 856 options, 926 - createContext(postCardComponent) 857 + createContext( 858 + postCardComponent, 859 + `at://${APP_DID}/at.inlay.component/${PostCard}` 860 + ) 927 861 ); 928 862 929 863 assert.deepEqual( ··· 932 866 ); 933 867 assertLog(log, [ 934 868 `lexicon ${PostCard}`, 935 - // Pack and record fetched concurrently 936 - `fetch:start ${HOST_PACK_URI}`, 937 - `fetch:start ${"at://did:plc:alice/app.bsky.feed.post/123"}`, 938 - `fetch:end ${HOST_PACK_URI}`, 939 - `fetch:end ${"at://did:plc:alice/app.bsky.feed.post/123"}`, 940 - `fetch ${"at://did:plc:host/at.inlay.component/stck"}`, 869 + // Record fetch happens first, then component resolution 870 + `fetch:start at://did:plc:alice/app.bsky.feed.post/123`, 871 + `fetch:end at://did:plc:alice/app.bsky.feed.post/123`, 872 + `fetch at://${HOST_DID}/at.inlay.component/${Stack}`, 941 873 `lexicon ${Stack}`, 942 - `fetch ${"at://did:plc:host/at.inlay.component/lnk"}`, 874 + `fetch at://${HOST_DID}/at.inlay.component/${Link}`, 943 875 `lexicon ${Link}`, 944 876 ]); 945 877 }); 946 878 }); 947 879 948 880 // ============================================================================ 949 - // 3. Pack resolution order 881 + // 3. DID resolution order 950 882 // ============================================================================ 951 883 // 952 - // A component's `imports` is an ordered list of pack URIs. When resolving 953 - // a type NSID, packs are searched in order — the first pack that exports 954 - // the type wins. This lets authors override primitives or shadow types. 884 + // A component's `imports` is an ordered list of DIDs. When resolving 885 + // a type NSID, the renderer constructs at://{did}/at.inlay.component/{nsid} 886 + // for each DID and fetches them all concurrently. The first DID (in import 887 + // order) that has a component record wins. This lets authors shadow types. 955 888 956 - describe("pack ordering", () => { 957 - // Two packs each export Greeting with different templates: 958 - // "fancy" → <Text value="Greetings, friend!" /> 959 - // "casual" → <Text value="Hi" /> 889 + describe("DID ordering", () => { 890 + // Two DIDs each provide Greeting with different templates: 891 + // did:plc:fancy → <Text value="Greetings, friend!" /> 892 + // did:plc:casual → <Text value="Hi" /> 960 893 // The import list order determines which one wins. 894 + 895 + const FANCY_DID = "did:plc:fancy"; 896 + const CASUAL_DID = "did:plc:casual"; 897 + const EMPTY_DID = "did:plc:empty"; 961 898 962 899 const fancyGreeting: ComponentRecord = { 963 900 $type: "at.inlay.component", 964 - type: Greeting, 965 901 body: { 966 902 $type: "at.inlay.component#bodyTemplate", 967 903 node: serializeTree($(Text, { value: "Greetings, friend!" })), 968 904 }, 969 - imports: [HOST_PACK_URI], 905 + imports: [HOST_DID], 970 906 }; 971 907 972 908 const casualGreeting: ComponentRecord = { 973 909 $type: "at.inlay.component", 974 - type: Greeting, 975 910 body: { 976 911 $type: "at.inlay.component#bodyTemplate", 977 912 node: serializeTree($(Text, { value: "Hi" })), 978 913 }, 979 - imports: [HOST_PACK_URI], 914 + imports: [HOST_DID], 980 915 }; 981 916 982 - it("first pack wins", async () => { 917 + it("first DID wins", async () => { 983 918 // Fancy is listed first, so <Greeting> resolves to "Greetings, friend!" 984 919 const rootComponent: ComponentRecord = { 985 920 $type: "at.inlay.component", 986 - type: Root, 987 921 body: { 988 922 $type: "at.inlay.component#bodyTemplate", 989 923 node: serializeTree($(Greeting, {})), 990 924 }, 991 - imports: [ 992 - "at://did:plc:test/at.inlay.pack/fancy", 993 - "at://did:plc:test/at.inlay.pack/casual", 994 - ] as AtUriString[], 925 + imports: [FANCY_DID, CASUAL_DID], 995 926 }; 996 927 997 928 const { options, log } = testResolver({ 998 929 ...HOST_RECORDS, 999 - ["at://did:plc:test/at.inlay.component/fncy"]: fancyGreeting, 1000 - ["at://did:plc:test/at.inlay.component/csl"]: casualGreeting, 1001 - ["at://did:plc:test/at.inlay.pack/fancy"]: { 1002 - $type: "at.inlay.pack", 1003 - name: "fancy", 1004 - exports: [ 1005 - { 1006 - type: Greeting, 1007 - component: "at://did:plc:test/at.inlay.component/fncy", 1008 - }, 1009 - ], 1010 - }, 1011 - ["at://did:plc:test/at.inlay.pack/casual"]: { 1012 - $type: "at.inlay.pack", 1013 - name: "casual", 1014 - exports: [ 1015 - { 1016 - type: Greeting, 1017 - component: "at://did:plc:test/at.inlay.component/csl", 1018 - }, 1019 - ], 1020 - }, 930 + [`at://${FANCY_DID}/at.inlay.component/${Greeting}`]: fancyGreeting, 931 + [`at://${CASUAL_DID}/at.inlay.component/${Greeting}`]: casualGreeting, 1021 932 }); 1022 933 1023 934 const output = await renderToCompletion( 1024 935 $(Root, {}), 1025 936 options, 1026 - createContext(rootComponent) 937 + createContext(rootComponent, `at://${APP_DID}/at.inlay.component/${Root}`) 1027 938 ); 1028 939 1029 940 assert.deepEqual(output, h("span", { value: "Greetings, friend!" })); 1030 941 assertLog(log, [ 1031 942 `lexicon ${Root}`, 1032 - // Both packs fetched concurrently 1033 - `fetch:start ${"at://did:plc:test/at.inlay.pack/fancy"}`, 1034 - `fetch:start ${"at://did:plc:test/at.inlay.pack/casual"}`, 1035 - `fetch:end ${"at://did:plc:test/at.inlay.pack/fancy"}`, 1036 - `fetch:end ${"at://did:plc:test/at.inlay.pack/casual"}`, 1037 - // Fancy pack wins — its Greeting component is fetched 1038 - `fetch ${"at://did:plc:test/at.inlay.component/fncy"}`, 943 + // Both DID lookups for Greeting fetched concurrently 944 + `fetch:start at://${FANCY_DID}/at.inlay.component/${Greeting}`, 945 + `fetch:start at://${CASUAL_DID}/at.inlay.component/${Greeting}`, 946 + `fetch:end at://${FANCY_DID}/at.inlay.component/${Greeting}`, 947 + `fetch:end at://${CASUAL_DID}/at.inlay.component/${Greeting}`, 948 + // Fancy DID wins — its Greeting component is used 1039 949 `lexicon ${Greeting}`, 1040 - `fetch ${HOST_PACK_URI}`, 1041 - `fetch ${"at://did:plc:host/at.inlay.component/txt"}`, 950 + `fetch at://${HOST_DID}/at.inlay.component/${Text}`, 1042 951 `lexicon ${Text}`, 1043 952 ]); 1044 953 }); 1045 954 1046 - it("falls through to later packs", async () => { 1047 - // First pack doesn't export Greeting, so it falls through to casual. 955 + it("falls through to later DIDs", async () => { 956 + // First DID doesn't have Greeting, so it falls through to casual. 1048 957 const rootComponent: ComponentRecord = { 1049 958 $type: "at.inlay.component", 1050 - type: Root, 1051 959 body: { 1052 960 $type: "at.inlay.component#bodyTemplate", 1053 961 node: serializeTree($(Greeting, {})), 1054 962 }, 1055 - imports: [ 1056 - "at://did:plc:test/at.inlay.pack/empty", 1057 - "at://did:plc:test/at.inlay.pack/casual", 1058 - ] as AtUriString[], 963 + imports: [EMPTY_DID, CASUAL_DID], 1059 964 }; 1060 965 1061 966 const { options, log } = testResolver({ 1062 967 ...HOST_RECORDS, 1063 - ["at://did:plc:test/at.inlay.component/csl"]: casualGreeting, 1064 - ["at://did:plc:test/at.inlay.pack/empty"]: { 1065 - $type: "at.inlay.pack", 1066 - name: "empty", 1067 - exports: [], 1068 - }, 1069 - ["at://did:plc:test/at.inlay.pack/casual"]: { 1070 - $type: "at.inlay.pack", 1071 - name: "casual", 1072 - exports: [ 1073 - { 1074 - type: Greeting, 1075 - component: "at://did:plc:test/at.inlay.component/csl", 1076 - }, 1077 - ], 1078 - }, 968 + [`at://${CASUAL_DID}/at.inlay.component/${Greeting}`]: casualGreeting, 969 + // EMPTY_DID has no Greeting record — fetchRecord returns null 1079 970 }); 1080 971 1081 972 const output = await renderToCompletion( 1082 973 $(Root, {}), 1083 974 options, 1084 - createContext(rootComponent) 975 + createContext(rootComponent, `at://${APP_DID}/at.inlay.component/${Root}`) 1085 976 ); 1086 977 1087 978 assert.deepEqual(output, h("span", { value: "Hi" })); 1088 979 assertLog(log, [ 1089 980 `lexicon ${Root}`, 1090 - // Both packs fetched concurrently, empty one has no match 1091 - `fetch:start ${"at://did:plc:test/at.inlay.pack/empty"}`, 1092 - `fetch:start ${"at://did:plc:test/at.inlay.pack/casual"}`, 1093 - `fetch:end ${"at://did:plc:test/at.inlay.pack/empty"}`, 1094 - `fetch:end ${"at://did:plc:test/at.inlay.pack/casual"}`, 1095 - // Falls through to casual pack 1096 - `fetch ${"at://did:plc:test/at.inlay.component/csl"}`, 981 + // Both DID lookups fetched concurrently, empty one returns null 982 + `fetch:start at://${EMPTY_DID}/at.inlay.component/${Greeting}`, 983 + `fetch:start at://${CASUAL_DID}/at.inlay.component/${Greeting}`, 984 + `fetch:end at://${EMPTY_DID}/at.inlay.component/${Greeting}`, 985 + `fetch:end at://${CASUAL_DID}/at.inlay.component/${Greeting}`, 986 + // Falls through to casual DID 1097 987 `lexicon ${Greeting}`, 1098 - `fetch ${HOST_PACK_URI}`, 1099 - `fetch ${"at://did:plc:host/at.inlay.component/txt"}`, 988 + `fetch at://${HOST_DID}/at.inlay.component/${Text}`, 1100 989 `lexicon ${Text}`, 1101 990 ]); 1102 991 }); 1103 992 1104 - it("produces Throw when no pack exports the type", async () => { 993 + it("produces Throw when no DID has the type", async () => { 1105 994 const unknown = "test.app.DoesNotExist"; 1106 995 const rootComponent: ComponentRecord = { 1107 996 $type: "at.inlay.component", 1108 - type: Root, 1109 997 body: { 1110 998 $type: "at.inlay.component#bodyTemplate", 1111 999 node: serializeTree($(unknown, {})), 1112 1000 }, 1113 - imports: [HOST_PACK_URI], 1001 + imports: [HOST_DID], 1114 1002 }; 1115 1003 1116 1004 const { options } = world(); ··· 1118 1006 const output = await renderToCompletion( 1119 1007 $(Root, {}), 1120 1008 options, 1121 - createContext(rootComponent) 1009 + createContext(rootComponent, `at://${APP_DID}/at.inlay.component/${Root}`) 1122 1010 ); 1123 1011 assertError( 1124 1012 output, 1125 - `No pack exports type: ${unknown}`, 1013 + `Unresolved type: ${unknown}`, 1126 1014 ` at ${unknown}\n at ${Root}` 1127 1015 ); 1128 1016 }); ··· 1138 1026 1139 1027 describe("import isolation", () => { 1140 1028 // Two implementations of Card: vertical wraps in Stack, horizontal in Row. 1029 + // Each lives under a different DID. 1030 + const VERTICAL_DID = "did:plc:vertical"; 1031 + const HORIZONTAL_DID = "did:plc:horizontal"; 1032 + 1141 1033 const verticalCard: ComponentRecord = { 1142 1034 $type: "at.inlay.component", 1143 - type: Card, 1144 1035 body: { 1145 1036 $type: "at.inlay.component#bodyTemplate", 1146 1037 node: serializeTree( ··· 1151 1042 }) 1152 1043 ), 1153 1044 }, 1154 - imports: [HOST_PACK_URI], 1045 + imports: [HOST_DID], 1155 1046 }; 1156 1047 1157 1048 const horizontalCard: ComponentRecord = { 1158 1049 $type: "at.inlay.component", 1159 - type: Card, 1160 1050 body: { 1161 1051 $type: "at.inlay.component#bodyTemplate", 1162 1052 node: serializeTree( ··· 1167 1057 }) 1168 1058 ), 1169 1059 }, 1170 - imports: [HOST_PACK_URI], 1060 + imports: [HOST_DID], 1171 1061 }; 1172 1062 1173 1063 const ISOLATION_RECORDS: Record<string, TestMock> = { 1174 1064 ...HOST_RECORDS, 1175 - ["at://did:plc:test/at.inlay.component/vcrd"]: verticalCard, 1176 - ["at://did:plc:test/at.inlay.component/hcrd"]: horizontalCard, 1177 - ["at://did:plc:test/at.inlay.pack/vertical"]: { 1178 - $type: "at.inlay.pack", 1179 - name: "vertical", 1180 - exports: [ 1181 - { 1182 - type: Card, 1183 - component: "at://did:plc:test/at.inlay.component/vcrd", 1184 - }, 1185 - ], 1186 - }, 1187 - ["at://did:plc:test/at.inlay.pack/horizontal"]: { 1188 - $type: "at.inlay.pack", 1189 - name: "horizontal", 1190 - exports: [ 1191 - { 1192 - type: Card, 1193 - component: "at://did:plc:test/at.inlay.component/hcrd", 1194 - }, 1195 - ], 1196 - }, 1065 + [`at://${VERTICAL_DID}/at.inlay.component/${Card}`]: verticalCard, 1066 + [`at://${HORIZONTAL_DID}/at.inlay.component/${Card}`]: horizontalCard, 1197 1067 }; 1198 1068 1199 1069 it("component resolves types through its own imports", async () => { 1200 - // Page imports vertical. Its <Card> resolves to Stack layout. 1070 + // Page imports vertical DID. Its <Card> resolves to Stack layout. 1201 1071 const pageComponent: ComponentRecord = { 1202 1072 $type: "at.inlay.component", 1203 - type: Page, 1204 1073 body: { 1205 1074 $type: "at.inlay.component#bodyTemplate", 1206 1075 node: serializeTree($(Card, { title: "hello" })), 1207 1076 }, 1208 - imports: ["at://did:plc:test/at.inlay.pack/vertical"] as AtUriString[], 1077 + imports: [VERTICAL_DID, HOST_DID], 1209 1078 }; 1210 1079 1211 1080 const { options } = testResolver(ISOLATION_RECORDS); ··· 1213 1082 const output = await renderToCompletion( 1214 1083 $(Page, {}), 1215 1084 options, 1216 - createContext(pageComponent) 1085 + createContext(pageComponent, `at://${APP_DID}/at.inlay.component/${Page}`) 1217 1086 ); 1218 1087 1219 1088 // vertical Card → Stack → <div> ··· 1224 1093 }); 1225 1094 1226 1095 it("parent and child resolve same type independently", async () => { 1227 - // Page imports vertical, Greeting imports horizontal. 1096 + // Page imports vertical DID, Greeting imports horizontal DID. 1228 1097 // Both render <Card> — each gets their own version. 1098 + const GREETING_DID = "did:plc:greeting"; 1229 1099 const greetingComponent: ComponentRecord = { 1230 1100 $type: "at.inlay.component", 1231 - type: Greeting, 1232 1101 body: { 1233 1102 $type: "at.inlay.component#bodyTemplate", 1234 1103 node: serializeTree($(Card, { title: "inner" })), 1235 1104 }, 1236 - imports: ["at://did:plc:test/at.inlay.pack/horizontal"] as AtUriString[], 1105 + imports: [HORIZONTAL_DID, HOST_DID], 1237 1106 }; 1238 1107 1239 1108 const pageComponent: ComponentRecord = { 1240 1109 $type: "at.inlay.component", 1241 - type: Page, 1242 1110 body: { 1243 1111 $type: "at.inlay.component#bodyTemplate", 1244 1112 node: serializeTree( ··· 1247 1115 }) 1248 1116 ), 1249 1117 }, 1250 - imports: [ 1251 - HOST_PACK_URI, 1252 - "at://did:plc:test/at.inlay.pack/vertical", 1253 - "at://did:plc:test/at.inlay.pack/app", 1254 - ] as AtUriString[], 1118 + imports: [HOST_DID, VERTICAL_DID, GREETING_DID], 1255 1119 }; 1256 1120 1257 1121 const { options } = testResolver({ 1258 1122 ...ISOLATION_RECORDS, 1259 - ["at://did:plc:test/at.inlay.component/grtng"]: greetingComponent, 1260 - ["at://did:plc:test/at.inlay.pack/app"]: { 1261 - $type: "at.inlay.pack", 1262 - name: "app", 1263 - exports: [ 1264 - { 1265 - type: Greeting, 1266 - component: "at://did:plc:test/at.inlay.component/grtng", 1267 - }, 1268 - ], 1269 - }, 1123 + [`at://${GREETING_DID}/at.inlay.component/${Greeting}`]: 1124 + greetingComponent, 1270 1125 }); 1271 1126 1272 1127 const output = await renderToCompletion( 1273 1128 $(Page, {}), 1274 1129 options, 1275 - createContext(pageComponent) 1130 + createContext(pageComponent, `at://${APP_DID}/at.inlay.component/${Page}`) 1276 1131 ); 1277 1132 1278 1133 assert.deepEqual( ··· 1291 1146 it("missing import fails even if parent has the type", async () => { 1292 1147 // Page imports vertical (which has Card). Greeting has no imports. 1293 1148 // Greeting's <Card> fails — it can't see Page's imports. 1149 + const GREETING_DID = "did:plc:greeting-noimport"; 1294 1150 const greetingComponent: ComponentRecord = { 1295 1151 $type: "at.inlay.component", 1296 - type: Greeting, 1297 1152 body: { 1298 1153 $type: "at.inlay.component#bodyTemplate", 1299 1154 node: serializeTree($(Card, { title: "unreachable" })), ··· 1303 1158 1304 1159 const pageComponent: ComponentRecord = { 1305 1160 $type: "at.inlay.component", 1306 - type: Page, 1307 1161 body: { 1308 1162 $type: "at.inlay.component#bodyTemplate", 1309 1163 node: serializeTree($(Greeting, {})), 1310 1164 }, 1311 - imports: [ 1312 - "at://did:plc:test/at.inlay.pack/vertical", 1313 - "at://did:plc:test/at.inlay.pack/app", 1314 - ] as AtUriString[], 1165 + imports: [VERTICAL_DID, GREETING_DID], 1315 1166 }; 1316 1167 1317 1168 const { options } = testResolver({ 1318 1169 ...ISOLATION_RECORDS, 1319 - ["at://did:plc:test/at.inlay.component/grtng"]: greetingComponent, 1320 - ["at://did:plc:test/at.inlay.pack/app"]: { 1321 - $type: "at.inlay.pack", 1322 - name: "app", 1323 - exports: [ 1324 - { 1325 - type: Greeting, 1326 - component: "at://did:plc:test/at.inlay.component/grtng", 1327 - }, 1328 - ], 1329 - }, 1170 + [`at://${GREETING_DID}/at.inlay.component/${Greeting}`]: 1171 + greetingComponent, 1330 1172 }); 1331 1173 1332 1174 const output = await renderToCompletion( 1333 1175 $(Page, {}), 1334 1176 options, 1335 - createContext(pageComponent) 1177 + createContext(pageComponent, `at://${APP_DID}/at.inlay.component/${Page}`) 1336 1178 ); 1337 1179 1338 1180 assertError( 1339 1181 output, 1340 - `No pack exports type: ${Card}`, 1182 + `Unresolved type: ${Card}`, 1341 1183 ` at ${Card}\n at ${Greeting}\n at ${Page}` 1342 1184 ); 1343 1185 }); ··· 1345 1187 it("composed children resolve through caller, not callee", async () => { 1346 1188 // Page passes <Card> as a child to Greeting. The passed-in Card 1347 1189 // resolves through Page's imports (vertical), not Greeting's. 1190 + const GREETING_DID = "did:plc:greeting-composed"; 1348 1191 const greetingComponent: ComponentRecord = { 1349 1192 $type: "at.inlay.component", 1350 - type: Greeting, 1351 1193 body: { 1352 1194 $type: "at.inlay.component#bodyTemplate", 1353 1195 node: serializeTree( ··· 1359 1201 }) 1360 1202 ), 1361 1203 }, 1362 - imports: [ 1363 - HOST_PACK_URI, 1364 - "at://did:plc:test/at.inlay.pack/horizontal", 1365 - ] as AtUriString[], 1204 + imports: [HOST_DID, HORIZONTAL_DID], 1366 1205 }; 1367 1206 1368 1207 const pageComponent: ComponentRecord = { 1369 1208 $type: "at.inlay.component", 1370 - type: Page, 1371 1209 body: { 1372 1210 $type: "at.inlay.component#bodyTemplate", 1373 1211 node: serializeTree( ··· 1376 1214 }) 1377 1215 ), 1378 1216 }, 1379 - imports: [ 1380 - HOST_PACK_URI, 1381 - "at://did:plc:test/at.inlay.pack/vertical", 1382 - "at://did:plc:test/at.inlay.pack/app", 1383 - ] as AtUriString[], 1217 + imports: [HOST_DID, VERTICAL_DID, GREETING_DID], 1384 1218 }; 1385 1219 1386 1220 const { options } = testResolver({ 1387 1221 ...ISOLATION_RECORDS, 1388 - ["at://did:plc:test/at.inlay.component/grtng"]: greetingComponent, 1389 - ["at://did:plc:test/at.inlay.pack/app"]: { 1390 - $type: "at.inlay.pack", 1391 - name: "app", 1392 - exports: [ 1393 - { 1394 - type: Greeting, 1395 - component: "at://did:plc:test/at.inlay.component/grtng", 1396 - }, 1397 - ], 1398 - }, 1222 + [`at://${GREETING_DID}/at.inlay.component/${Greeting}`]: 1223 + greetingComponent, 1399 1224 }); 1400 1225 1401 1226 const output = await renderToCompletion( 1402 1227 $(Page, {}), 1403 1228 options, 1404 - createContext(pageComponent) 1229 + createContext(pageComponent, `at://${APP_DID}/at.inlay.component/${Page}`) 1405 1230 ); 1406 1231 1407 1232 // Greeting's own Card → horizontal → Row → <section> ··· 1420 1245 it("passed child resolves even when callee can't", async () => { 1421 1246 // Greeting has no Card in its imports. Page passes <Card> as a child. 1422 1247 // Card resolves through Page's imports — Greeting doesn't need it. 1248 + const GREETING_DID = "did:plc:greeting-passthru"; 1423 1249 const greetingComponent: ComponentRecord = { 1424 1250 $type: "at.inlay.component", 1425 - type: Greeting, 1426 1251 body: { 1427 1252 $type: "at.inlay.component#bodyTemplate", 1428 1253 node: serializeTree( 1429 1254 $(Stack, { children: $(Binding, { path: ["props", "children"] }) }) 1430 1255 ), 1431 1256 }, 1432 - imports: [HOST_PACK_URI] as AtUriString[], 1257 + imports: [HOST_DID], 1433 1258 }; 1434 1259 1435 1260 const pageComponent: ComponentRecord = { 1436 1261 $type: "at.inlay.component", 1437 - type: Page, 1438 1262 body: { 1439 1263 $type: "at.inlay.component#bodyTemplate", 1440 1264 node: serializeTree( ··· 1443 1267 }) 1444 1268 ), 1445 1269 }, 1446 - imports: [ 1447 - HOST_PACK_URI, 1448 - "at://did:plc:test/at.inlay.pack/vertical", 1449 - "at://did:plc:test/at.inlay.pack/app", 1450 - ] as AtUriString[], 1270 + imports: [HOST_DID, VERTICAL_DID, GREETING_DID], 1451 1271 }; 1452 1272 1453 1273 const { options } = testResolver({ 1454 1274 ...ISOLATION_RECORDS, 1455 - ["at://did:plc:test/at.inlay.component/grtng"]: greetingComponent, 1456 - ["at://did:plc:test/at.inlay.pack/app"]: { 1457 - $type: "at.inlay.pack", 1458 - name: "app", 1459 - exports: [ 1460 - { 1461 - type: Greeting, 1462 - component: "at://did:plc:test/at.inlay.component/grtng", 1463 - }, 1464 - ], 1465 - }, 1275 + [`at://${GREETING_DID}/at.inlay.component/${Greeting}`]: 1276 + greetingComponent, 1466 1277 }); 1467 1278 1468 1279 const output = await renderToCompletion( 1469 1280 $(Page, {}), 1470 1281 options, 1471 - createContext(pageComponent) 1282 + createContext(pageComponent, `at://${APP_DID}/at.inlay.component/${Page}`) 1472 1283 ); 1473 1284 1474 1285 // Card resolves through Page's vertical imports → Stack → <div> ··· 1483 1294 it("slot isolation works through indirection", async () => { 1484 1295 // Page passes <Card> through Indirection (which just renders children 1485 1296 // and has no imports). Card still resolves through Page's imports. 1297 + const INDIR_DID = "did:plc:indirection"; 1486 1298 const indirectionComponent: ComponentRecord = { 1487 1299 $type: "at.inlay.component", 1488 - type: Indirection, 1489 1300 body: { 1490 1301 $type: "at.inlay.component#bodyTemplate", 1491 1302 node: serializeTree($(Binding, { path: ["props", "children"] })), ··· 1495 1306 1496 1307 const pageComponent: ComponentRecord = { 1497 1308 $type: "at.inlay.component", 1498 - type: Page, 1499 1309 body: { 1500 1310 $type: "at.inlay.component#bodyTemplate", 1501 1311 node: serializeTree( ··· 1504 1314 }) 1505 1315 ), 1506 1316 }, 1507 - imports: [ 1508 - "at://did:plc:test/at.inlay.pack/vertical", 1509 - "at://did:plc:test/at.inlay.pack/app", 1510 - ] as AtUriString[], 1317 + imports: [VERTICAL_DID, INDIR_DID], 1511 1318 }; 1512 1319 1513 1320 const { options } = testResolver({ 1514 1321 ...ISOLATION_RECORDS, 1515 - ["at://did:plc:test/at.inlay.component/indr"]: indirectionComponent, 1516 - ["at://did:plc:test/at.inlay.pack/app"]: { 1517 - $type: "at.inlay.pack", 1518 - name: "app", 1519 - exports: [ 1520 - { 1521 - type: Indirection, 1522 - component: "at://did:plc:test/at.inlay.component/indr", 1523 - }, 1524 - ], 1525 - }, 1322 + [`at://${INDIR_DID}/at.inlay.component/${Indirection}`]: 1323 + indirectionComponent, 1526 1324 }); 1527 1325 1528 1326 const output = await renderToCompletion( 1529 1327 $(Page, {}), 1530 1328 options, 1531 - createContext(pageComponent) 1329 + createContext(pageComponent, `at://${APP_DID}/at.inlay.component/${Page}`) 1532 1330 ); 1533 1331 1534 1332 // Card resolves through Page's vertical imports → Stack → <div> ··· 1557 1355 // Same result as the template Greeting, but built with code. 1558 1356 const greetingComponent: ComponentRecord = { 1559 1357 $type: "at.inlay.component", 1560 - type: Greeting, 1561 1358 body: { 1562 1359 $type: "at.inlay.component#bodyExternal", 1563 1360 did: SERVICE_DID, 1564 1361 }, 1565 - imports: [HOST_PACK_URI], 1362 + imports: [HOST_DID], 1566 1363 }; 1567 1364 1568 1365 const { options, log } = world({ ··· 1581 1378 const output = await renderToCompletion( 1582 1379 $(Greeting, { name: "world" }), 1583 1380 options, 1584 - createContext(greetingComponent) 1381 + createContext( 1382 + greetingComponent, 1383 + `at://${APP_DID}/at.inlay.component/${Greeting}` 1384 + ) 1585 1385 ); 1586 1386 1587 1387 assert.deepEqual( ··· 1596 1396 assertLog(log, [ 1597 1397 `lexicon ${Greeting}`, 1598 1398 `xrpc procedure ${Greeting} -> ${SERVICE_DID}`, 1599 - `fetch ${HOST_PACK_URI}`, 1600 - `fetch ${"at://did:plc:host/at.inlay.component/rw"}`, 1399 + `fetch at://${HOST_DID}/at.inlay.component/${Row}`, 1601 1400 `lexicon ${Row}`, 1602 - `fetch ${"at://did:plc:host/at.inlay.component/txt"}`, 1401 + `fetch at://${HOST_DID}/at.inlay.component/${Text}`, 1603 1402 `lexicon ${Text}`, 1604 1403 `lexicon ${Text}`, 1605 1404 ]); ··· 1609 1408 // Layout receives children, wraps them in a Stack. 1610 1409 const layoutComponent: ComponentRecord = { 1611 1410 $type: "at.inlay.component", 1612 - type: Layout, 1613 1411 body: { 1614 1412 $type: "at.inlay.component#bodyExternal", 1615 1413 did: SERVICE_DID, 1616 1414 }, 1617 - imports: [HOST_PACK_URI], 1415 + imports: [HOST_DID], 1618 1416 }; 1619 1417 1620 1418 const { options } = world({ ··· 1628 1426 const output = await renderToCompletion( 1629 1427 $(Layout, { children: $(Text, { value: "hello" }) }), 1630 1428 options, 1631 - createContext(layoutComponent) 1429 + createContext( 1430 + layoutComponent, 1431 + `at://${APP_DID}/at.inlay.component/${Layout}` 1432 + ) 1632 1433 ); 1633 1434 1634 1435 assert.deepEqual( ··· 1643 1444 // Service adds a divider between each caller child. 1644 1445 const layoutComponent: ComponentRecord = { 1645 1446 $type: "at.inlay.component", 1646 - type: Layout, 1647 1447 body: { 1648 1448 $type: "at.inlay.component#bodyExternal", 1649 1449 did: SERVICE_DID, 1650 1450 }, 1651 - imports: [HOST_PACK_URI], 1451 + imports: [HOST_DID], 1652 1452 }; 1653 1453 1654 1454 const { options } = world({ ··· 1669 1469 children: [$(Text, { value: "first" }), $(Text, { value: "second" })], 1670 1470 }), 1671 1471 options, 1672 - createContext(layoutComponent) 1472 + createContext( 1473 + layoutComponent, 1474 + `at://${APP_DID}/at.inlay.component/${Layout}` 1475 + ) 1673 1476 ); 1674 1477 1675 1478 assert.deepEqual( ··· 1687 1490 it("caller's children resolve through caller's imports", async () => { 1688 1491 // Page passes <Card> to an external Layout. The Card resolves through 1689 1492 // Page's imports (vertical), not the service's — same as templates. 1493 + const LAYOUT_DID = "did:plc:layout-ext"; 1494 + const VERT_DID = "did:plc:vert-ext"; 1690 1495 const layoutComponent: ComponentRecord = { 1691 1496 $type: "at.inlay.component", 1692 - type: Layout, 1693 1497 body: { 1694 1498 $type: "at.inlay.component#bodyExternal", 1695 1499 did: SERVICE_DID, 1696 1500 }, 1697 - imports: [HOST_PACK_URI], 1501 + imports: [HOST_DID], 1698 1502 }; 1699 1503 1700 1504 const pageComponent: ComponentRecord = { 1701 1505 $type: "at.inlay.component", 1702 - type: Page, 1703 1506 body: { 1704 1507 $type: "at.inlay.component#bodyTemplate", 1705 1508 node: serializeTree( 1706 1509 $(Layout, { children: $(Card, { title: "from caller" }) }) 1707 1510 ), 1708 1511 }, 1709 - imports: [ 1710 - HOST_PACK_URI, 1711 - "at://did:plc:test/at.inlay.pack/vertical", 1712 - "at://did:plc:test/at.inlay.pack/app", 1713 - ] as AtUriString[], 1512 + imports: [HOST_DID, VERT_DID, LAYOUT_DID], 1714 1513 }; 1715 1514 1716 1515 const verticalCard: ComponentRecord = { 1717 1516 $type: "at.inlay.component", 1718 - type: Card, 1719 1517 body: { 1720 1518 $type: "at.inlay.component#bodyTemplate", 1721 1519 node: serializeTree( ··· 1726 1524 }) 1727 1525 ), 1728 1526 }, 1729 - imports: [HOST_PACK_URI], 1527 + imports: [HOST_DID], 1730 1528 }; 1731 1529 1732 1530 const { options } = testResolver({ 1733 1531 ...HOST_RECORDS, 1734 - ["at://did:plc:test/at.inlay.component/vcrd"]: verticalCard, 1735 - ["at://did:plc:test/at.inlay.pack/vertical"]: { 1736 - $type: "at.inlay.pack", 1737 - name: "vertical", 1738 - exports: [ 1739 - { 1740 - type: Card, 1741 - component: "at://did:plc:test/at.inlay.component/vcrd", 1742 - }, 1743 - ], 1744 - }, 1745 - ["at://did:plc:test/at.inlay.component/lyout"]: layoutComponent, 1746 - ["at://did:plc:test/at.inlay.pack/app"]: { 1747 - $type: "at.inlay.pack", 1748 - name: "app", 1749 - exports: [ 1750 - { 1751 - type: Layout, 1752 - component: "at://did:plc:test/at.inlay.component/lyout", 1753 - }, 1754 - ], 1755 - }, 1532 + [`at://${VERT_DID}/at.inlay.component/${Card}`]: verticalCard, 1533 + [`at://${LAYOUT_DID}/at.inlay.component/${Layout}`]: layoutComponent, 1756 1534 [`xrpc:${SERVICE_DID}:${Layout}`]: (params: { 1757 1535 body?: Record<string, unknown>; 1758 1536 }) => ({ ··· 1763 1541 const output = await renderToCompletion( 1764 1542 $(Page, {}), 1765 1543 options, 1766 - createContext(pageComponent) 1544 + createContext(pageComponent, `at://${APP_DID}/at.inlay.component/${Page}`) 1767 1545 ); 1768 1546 1769 1547 // Card resolves through Page's vertical imports → Stack → <div> ··· 1782 1560 // wrapper, the server's key goes on the restored element inside. 1783 1561 const layoutComponent: ComponentRecord = { 1784 1562 $type: "at.inlay.component", 1785 - type: Layout, 1786 1563 body: { 1787 1564 $type: "at.inlay.component#bodyExternal", 1788 1565 did: SERVICE_DID, 1789 1566 }, 1790 - imports: [HOST_PACK_URI], 1567 + imports: [HOST_DID], 1791 1568 }; 1792 1569 1793 1570 const { options } = world({ ··· 1806 1583 children: [$(Text, { key: "caller-key", value: "hello" })], 1807 1584 }), 1808 1585 options, 1809 - createContext(layoutComponent) 1586 + createContext( 1587 + layoutComponent, 1588 + `at://${APP_DID}/at.inlay.component/${Layout}` 1589 + ) 1810 1590 ); 1811 1591 1812 1592 assert.deepEqual( ··· 1837 1617 it("template chain shows owner stack", async () => { 1838 1618 // Page renders Card, Card renders Greeting. Greeting fails. 1839 1619 // The stack should read bottom-up: Greeting, Card, Page. 1620 + const APP_DID = "did:plc:err-chain"; 1840 1621 const pageComponent: ComponentRecord = { 1841 1622 $type: "at.inlay.component", 1842 - type: Page, 1843 1623 body: { 1844 1624 $type: "at.inlay.component#bodyTemplate", 1845 1625 node: serializeTree($(Card, {})), 1846 1626 }, 1847 - imports: ["at://did:plc:test/at.inlay.pack/app"] as AtUriString[], 1627 + imports: [APP_DID], 1848 1628 }; 1849 1629 1850 1630 const cardComponent: ComponentRecord = { 1851 1631 $type: "at.inlay.component", 1852 - type: Card, 1853 1632 body: { 1854 1633 $type: "at.inlay.component#bodyTemplate", 1855 1634 node: serializeTree($(Greeting, {})), 1856 1635 }, 1857 - imports: ["at://did:plc:test/at.inlay.pack/app"] as AtUriString[], 1636 + imports: [APP_DID], 1858 1637 }; 1859 1638 1860 1639 const greetingComponent: ComponentRecord = { 1861 1640 $type: "at.inlay.component", 1862 - type: Greeting, 1863 1641 body: { 1864 1642 $type: "at.inlay.component#bodyTemplate", 1865 1643 node: serializeTree($(Stack, {})), 1866 1644 }, 1867 - imports: [HOST_PACK_URI], 1645 + imports: [HOST_DID], 1868 1646 }; 1869 1647 1870 1648 const { options } = testResolver({ 1871 1649 ...HOST_RECORDS, 1872 - ["at://did:plc:test/at.inlay.component/page"]: pageComponent, 1873 - ["at://did:plc:test/at.inlay.component/card"]: cardComponent, 1874 - ["at://did:plc:test/at.inlay.component/greet"]: greetingComponent, 1875 - ["at://did:plc:test/at.inlay.pack/app"]: { 1876 - $type: "at.inlay.pack", 1877 - name: "app", 1878 - exports: [ 1879 - { 1880 - type: Page, 1881 - component: "at://did:plc:test/at.inlay.component/page", 1882 - }, 1883 - { 1884 - type: Card, 1885 - component: "at://did:plc:test/at.inlay.component/card", 1886 - }, 1887 - { 1888 - type: Greeting, 1889 - component: "at://did:plc:test/at.inlay.component/greet", 1890 - }, 1891 - ], 1892 - }, 1650 + [`at://${APP_DID}/at.inlay.component/${Page}`]: pageComponent, 1651 + [`at://${APP_DID}/at.inlay.component/${Card}`]: cardComponent, 1652 + [`at://${APP_DID}/at.inlay.component/${Greeting}`]: greetingComponent, 1893 1653 }); 1894 1654 1895 1655 // Greeting's resolver fails — error should bubble with full owner chain. ··· 1901 1661 ...options.resolver, 1902 1662 fetchRecord: ((original) => async (uri: AtUriString) => { 1903 1663 const result = await original(uri); 1904 - if (result && (result as ComponentRecord).type === Greeting) { 1664 + if ( 1665 + result && 1666 + uri === `at://${APP_DID}/at.inlay.component/${Greeting}` 1667 + ) { 1905 1668 // Return a component whose template references a missing type. 1906 1669 return { 1907 1670 ...result, ··· 1917 1680 resolveLexicon: options.resolver.resolveLexicon, 1918 1681 }, 1919 1682 }, 1920 - createContext(pageComponent) 1683 + createContext(pageComponent, `at://${APP_DID}/at.inlay.component/${Page}`) 1921 1684 ); 1922 1685 assertError( 1923 1686 output, 1924 - "No pack exports type: test.app.DoesNotExist", 1687 + "Unresolved type: test.app.DoesNotExist", 1925 1688 [ 1926 1689 " at test.app.DoesNotExist", 1927 1690 " at test.app.Greeting", ··· 1934 1697 it("error stack is owner stack, not parent stack", async () => { 1935 1698 // Page renders <Card><Greeting /></Card>. Greeting fails. 1936 1699 // Stack is [Greeting, Page] — Card is not the owner of Greeting. 1700 + const APP_DID = "did:plc:err-owner"; 1937 1701 const pageComponent: ComponentRecord = { 1938 1702 $type: "at.inlay.component", 1939 - type: Page, 1940 1703 body: { 1941 1704 $type: "at.inlay.component#bodyTemplate", 1942 1705 node: serializeTree($(Card, { children: $(Greeting, {}) })), 1943 1706 }, 1944 - imports: ["at://did:plc:test/at.inlay.pack/app"] as AtUriString[], 1707 + imports: [APP_DID], 1945 1708 }; 1946 1709 1947 1710 const cardComponent: ComponentRecord = { 1948 1711 $type: "at.inlay.component", 1949 - type: Card, 1950 1712 body: { 1951 1713 $type: "at.inlay.component#bodyTemplate", 1952 1714 node: serializeTree( 1953 1715 $(Stack, { children: $(Binding, { path: ["props", "children"] }) }) 1954 1716 ), 1955 1717 }, 1956 - imports: [HOST_PACK_URI], 1718 + imports: [HOST_DID], 1957 1719 }; 1958 1720 1959 1721 const greetingComponent: ComponentRecord = { 1960 1722 $type: "at.inlay.component", 1961 - type: Greeting, 1962 1723 body: { 1963 1724 $type: "at.inlay.component#bodyTemplate", 1964 1725 node: serializeTree($("test.app.DoesNotExist", {})), 1965 1726 }, 1966 - imports: ["at://did:plc:test/at.inlay.pack/app"] as AtUriString[], 1727 + imports: [APP_DID], 1967 1728 }; 1968 1729 1969 1730 const { options } = testResolver({ 1970 1731 ...HOST_RECORDS, 1971 - ["at://did:plc:test/at.inlay.component/page"]: pageComponent, 1972 - ["at://did:plc:test/at.inlay.component/card"]: cardComponent, 1973 - ["at://did:plc:test/at.inlay.component/greet"]: greetingComponent, 1974 - ["at://did:plc:test/at.inlay.pack/app"]: { 1975 - $type: "at.inlay.pack", 1976 - name: "app", 1977 - exports: [ 1978 - { 1979 - type: Page, 1980 - component: "at://did:plc:test/at.inlay.component/page", 1981 - }, 1982 - { 1983 - type: Card, 1984 - component: "at://did:plc:test/at.inlay.component/card", 1985 - }, 1986 - { 1987 - type: Greeting, 1988 - component: "at://did:plc:test/at.inlay.component/greet", 1989 - }, 1990 - ], 1991 - }, 1732 + [`at://${APP_DID}/at.inlay.component/${Page}`]: pageComponent, 1733 + [`at://${APP_DID}/at.inlay.component/${Card}`]: cardComponent, 1734 + [`at://${APP_DID}/at.inlay.component/${Greeting}`]: greetingComponent, 1992 1735 }); 1993 1736 1994 1737 const output = await renderToCompletion( 1995 1738 $(Page, {}), 1996 1739 options, 1997 - createContext(pageComponent) 1740 + createContext(pageComponent, `at://${APP_DID}/at.inlay.component/${Page}`) 1998 1741 ); 1999 1742 2000 1743 assertError( 2001 1744 output, 2002 - "No pack exports type: test.app.DoesNotExist", 1745 + "Unresolved type: test.app.DoesNotExist", 2003 1746 [ 2004 1747 " at test.app.DoesNotExist", 2005 1748 " at test.app.Greeting", ··· 2009 1752 }); 2010 1753 2011 1754 it("infinite recursion stack shows the repeated component", async () => { 1755 + const LOOP_DID = "did:plc:loop"; 2012 1756 const loopComponent: ComponentRecord = { 2013 1757 $type: "at.inlay.component", 2014 - type: Loop, 2015 1758 body: { 2016 1759 $type: "at.inlay.component#bodyTemplate", 2017 1760 node: serializeTree($(Loop, {})), 2018 1761 }, 2019 - imports: ["at://did:plc:test/at.inlay.pack/loop"] as AtUriString[], 1762 + imports: [LOOP_DID], 2020 1763 }; 2021 1764 2022 1765 const { options } = testResolver({ 2023 - ["at://did:plc:test/at.inlay.component/lp"]: loopComponent, 2024 - ["at://did:plc:test/at.inlay.pack/loop"]: { 2025 - $type: "at.inlay.pack", 2026 - name: "loop", 2027 - exports: [ 2028 - { type: Loop, component: "at://did:plc:test/at.inlay.component/lp" }, 2029 - ], 2030 - }, 1766 + [`at://${LOOP_DID}/at.inlay.component/${Loop}`]: loopComponent, 2031 1767 }); 2032 1768 2033 1769 const output = await renderToCompletion( 2034 1770 $(Loop, {}), 2035 1771 { ...options, maxDepth: 5 }, 2036 - createContext(loopComponent) 1772 + createContext( 1773 + loopComponent, 1774 + `at://${LOOP_DID}/at.inlay.component/${Loop}` 1775 + ) 2037 1776 ); 2038 1777 assertError( 2039 1778 output, ··· 2050 1789 }); 2051 1790 2052 1791 it("xrpc error includes the external component in the stack", async () => { 1792 + const APP_DID = "did:plc:xrpc-err"; 2053 1793 const pageComponent: ComponentRecord = { 2054 1794 $type: "at.inlay.component", 2055 - type: Page, 2056 1795 body: { 2057 1796 $type: "at.inlay.component#bodyTemplate", 2058 1797 node: serializeTree($(Greeting, {})), 2059 1798 }, 2060 - imports: ["at://did:plc:test/at.inlay.pack/app"] as AtUriString[], 1799 + imports: [APP_DID], 2061 1800 }; 2062 1801 2063 1802 const greetingComponent: ComponentRecord = { 2064 1803 $type: "at.inlay.component", 2065 - type: Greeting, 2066 1804 body: { 2067 1805 $type: "at.inlay.component#bodyExternal", 2068 1806 did: SERVICE_DID, 2069 1807 }, 2070 - imports: [HOST_PACK_URI], 1808 + imports: [HOST_DID], 2071 1809 }; 2072 1810 2073 1811 const { options } = world({ 2074 1812 [`xrpc:${SERVICE_DID}:${Greeting}`]: () => { 2075 1813 throw new Error("service unavailable"); 2076 1814 }, 2077 - ["at://did:plc:test/at.inlay.component/page"]: pageComponent, 2078 - ["at://did:plc:test/at.inlay.component/greet"]: greetingComponent, 2079 - ["at://did:plc:test/at.inlay.pack/app"]: { 2080 - $type: "at.inlay.pack", 2081 - name: "app", 2082 - exports: [ 2083 - { 2084 - type: Page, 2085 - component: "at://did:plc:test/at.inlay.component/page", 2086 - }, 2087 - { 2088 - type: Greeting, 2089 - component: "at://did:plc:test/at.inlay.component/greet", 2090 - }, 2091 - ], 2092 - }, 1815 + [`at://${APP_DID}/at.inlay.component/${Page}`]: pageComponent, 1816 + [`at://${APP_DID}/at.inlay.component/${Greeting}`]: greetingComponent, 2093 1817 }); 2094 1818 2095 1819 const output = await renderToCompletion( 2096 1820 $(Page, {}), 2097 1821 options, 2098 - createContext(pageComponent) 1822 + createContext(pageComponent, `at://${APP_DID}/at.inlay.component/${Page}`) 2099 1823 ); 2100 1824 assertError( 2101 1825 output, ··· 2107 1831 it("catches resolver errors", async () => { 2108 1832 const rootComponent: ComponentRecord = { 2109 1833 $type: "at.inlay.component", 2110 - type: Root, 2111 1834 body: { 2112 1835 $type: "at.inlay.component#bodyTemplate", 2113 1836 node: serializeTree($(Stack, {})), 2114 1837 }, 2115 - imports: [HOST_PACK_URI], 1838 + imports: [HOST_DID], 2116 1839 }; 2117 1840 2118 1841 const errorOptions: RenderOptions = { ··· 2130 1853 const output = await renderToCompletion( 2131 1854 $(Root, {}), 2132 1855 errorOptions, 2133 - createContext(rootComponent) 1856 + createContext(rootComponent, `at://${APP_DID}/at.inlay.component/${Root}`) 2134 1857 ); 2135 1858 assertError(output, "network down", ` at ${Stack}\n at ${Root}`); 2136 1859 }); ··· 2141 1864 // occurs because the child is never rendered. 2142 1865 const ignoreComponent: ComponentRecord = { 2143 1866 $type: "at.inlay.component", 2144 - type: Layout, 2145 1867 body: { 2146 1868 $type: "at.inlay.component#bodyTemplate", 2147 1869 node: serializeTree($(Text, { value: "ok" })), 2148 1870 }, 2149 - imports: [HOST_PACK_URI], 1871 + imports: [HOST_DID], 2150 1872 }; 2151 1873 2152 1874 const crashComponent: ComponentRecord = { 2153 1875 $type: "at.inlay.component", 2154 - type: Greeting, 2155 1876 body: { 2156 1877 $type: "at.inlay.component#bodyExternal", 2157 1878 did: SERVICE_DID, 2158 1879 }, 2159 - imports: [HOST_PACK_URI], 1880 + imports: [HOST_DID], 2160 1881 }; 2161 1882 1883 + const UNUSED_DID = "did:plc:unused"; 2162 1884 const { options } = world({ 2163 1885 [`xrpc:${SERVICE_DID}:${Greeting}`]: () => { 2164 1886 throw new Error("boom"); 2165 1887 }, 2166 - ["at://did:plc:test/at.inlay.component/ignr"]: ignoreComponent, 2167 - ["at://did:plc:test/at.inlay.component/crsh"]: crashComponent, 2168 - ["at://did:plc:test/at.inlay.pack/app"]: { 2169 - $type: "at.inlay.pack", 2170 - name: "app", 2171 - exports: [ 2172 - { 2173 - type: Layout, 2174 - component: "at://did:plc:test/at.inlay.component/ignr", 2175 - }, 2176 - { 2177 - type: Greeting, 2178 - component: "at://did:plc:test/at.inlay.component/crsh", 2179 - }, 2180 - ], 2181 - }, 1888 + [`at://${UNUSED_DID}/at.inlay.component/${Layout}`]: ignoreComponent, 1889 + [`at://${UNUSED_DID}/at.inlay.component/${Greeting}`]: crashComponent, 2182 1890 }); 2183 1891 2184 1892 const output = await renderToCompletion( 2185 1893 $(Layout, { children: $(Greeting, {}) }), 2186 1894 options, 2187 - createContext(ignoreComponent) 1895 + createContext( 1896 + ignoreComponent, 1897 + `at://${UNUSED_DID}/at.inlay.component/${Layout}` 1898 + ) 2188 1899 ); 2189 1900 assert.deepEqual(output, h("span", { value: "ok" })); 2190 1901 }); ··· 2204 1915 // before either finishes — the host can walk siblings in parallel. 2205 1916 const layoutComponent: ComponentRecord = { 2206 1917 $type: "at.inlay.component", 2207 - type: Layout, 2208 1918 body: { 2209 1919 $type: "at.inlay.component#bodyTemplate", 2210 1920 node: serializeTree({ ··· 2212 1922 second: $(Text, {}), 2213 1923 }), 2214 1924 }, 2215 - imports: [HOST_PACK_URI], 1925 + imports: [HOST_DID], 2216 1926 }; 2217 1927 2218 1928 const { options, log } = world(); ··· 2220 1930 await renderToCompletion( 2221 1931 $(Layout, {}), 2222 1932 options, 2223 - createContext(layoutComponent) 1933 + createContext( 1934 + layoutComponent, 1935 + `at://${APP_DID}/at.inlay.component/${Layout}` 1936 + ) 2224 1937 ); 2225 1938 2226 1939 assertLog(log, [ 2227 1940 `lexicon ${Layout}`, 2228 - `fetch ${HOST_PACK_URI}`, 2229 1941 // Both component fetches start concurrently (siblings) 2230 - `fetch:start ${"at://did:plc:host/at.inlay.component/stck"}`, 2231 - `fetch:start ${"at://did:plc:host/at.inlay.component/txt"}`, 2232 - `fetch:end ${"at://did:plc:host/at.inlay.component/stck"}`, 2233 - `fetch:end ${"at://did:plc:host/at.inlay.component/txt"}`, 1942 + `fetch:start at://${HOST_DID}/at.inlay.component/${Stack}`, 1943 + `fetch:start at://${HOST_DID}/at.inlay.component/${Text}`, 1944 + `fetch:end at://${HOST_DID}/at.inlay.component/${Stack}`, 1945 + `fetch:end at://${HOST_DID}/at.inlay.component/${Text}`, 2234 1946 `lexicon ${Stack}`, 2235 1947 `lexicon ${Text}`, 2236 1948 ]); 2237 1949 }); 2238 1950 2239 - it("prefetches child packs during record fetch", async () => { 1951 + it("prefetches child component during record fetch", async () => { 1952 + const CHILD_DID = "did:plc:child"; 2240 1953 const layoutComponent: ComponentRecord = { 2241 1954 $type: "at.inlay.component", 2242 - type: Layout, 2243 1955 body: { 2244 1956 $type: "at.inlay.component#bodyTemplate", 2245 1957 node: serializeTree( 2246 1958 $(Text, { value: $(Binding, { path: ["record", "text"] }) }) 2247 1959 ), 2248 1960 }, 2249 - imports: ["at://did:plc:test/at.inlay.pack/child"] as AtUriString[], 1961 + imports: [CHILD_DID], 2250 1962 view: { 2251 1963 $type: "at.inlay.component#view", 2252 1964 prop: "uri", ··· 2263 1975 ["at://did:plc:alice/app.bsky.feed.post/123"]: { 2264 1976 text: "Hello world", 2265 1977 }, 2266 - ["at://did:plc:test/at.inlay.component/grtng"]: layoutComponent, 2267 - ["at://did:plc:host/at.inlay.component/txt"]: { 1978 + [`at://${CHILD_DID}/at.inlay.component/${Text}`]: { 2268 1979 $type: "at.inlay.component", 2269 - type: Text, 2270 - }, 2271 - ["at://did:plc:test/at.inlay.pack/parent"]: { 2272 - $type: "at.inlay.pack", 2273 - name: "test", 2274 - exports: [ 2275 - { 2276 - type: Layout, 2277 - component: "at://did:plc:test/at.inlay.component/grtng", 2278 - }, 2279 - ], 2280 - }, 2281 - ["at://did:plc:test/at.inlay.pack/child"]: { 2282 - $type: "at.inlay.pack", 2283 - name: "child", 2284 - exports: [ 2285 - { type: Text, component: "at://did:plc:host/at.inlay.component/txt" }, 2286 - ], 2287 1980 }, 2288 1981 }); 2289 1982 2290 1983 await renderToCompletion( 2291 1984 $(Layout, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 2292 1985 options, 2293 - createContext(layoutComponent) 1986 + createContext( 1987 + layoutComponent, 1988 + `at://${APP_DID}/at.inlay.component/${Layout}` 1989 + ) 2294 1990 ); 2295 1991 2296 - // Child pack prefetch overlaps with record fetch 1992 + // Record fetch completes, then component is resolved 2297 1993 assertLog(log, [ 2298 1994 `lexicon ${Layout}`, 2299 - `fetch:start ${"at://did:plc:test/at.inlay.pack/child"}`, 2300 - `fetch:start ${"at://did:plc:alice/app.bsky.feed.post/123"}`, 2301 - `fetch:end ${"at://did:plc:test/at.inlay.pack/child"}`, 2302 - `fetch:end ${"at://did:plc:alice/app.bsky.feed.post/123"}`, 2303 - `fetch ${"at://did:plc:host/at.inlay.component/txt"}`, 1995 + `fetch ${"at://did:plc:alice/app.bsky.feed.post/123"}`, 1996 + `fetch at://${CHILD_DID}/at.inlay.component/${Text}`, 2304 1997 `lexicon ${Text}`, 2305 1998 ]); 2306 1999 }); ··· 2308 2001 it("sibling xrpc calls run concurrently", async () => { 2309 2002 // A template expands to two external siblings. Both xrpc calls 2310 2003 // should start before either finishes. 2004 + const XRPC_DID = "did:plc:xrpc-concurrent"; 2311 2005 const wrapperComponent: ComponentRecord = { 2312 2006 $type: "at.inlay.component", 2313 - type: Root, 2314 2007 body: { 2315 2008 $type: "at.inlay.component#bodyTemplate", 2316 2009 node: serializeTree( ··· 2319 2012 }) 2320 2013 ), 2321 2014 }, 2322 - imports: ["at://did:plc:test/at.inlay.pack/app"] as AtUriString[], 2015 + imports: [XRPC_DID], 2323 2016 }; 2324 2017 2325 2018 const greetingComponent: ComponentRecord = { 2326 2019 $type: "at.inlay.component", 2327 - type: Greeting, 2328 2020 body: { $type: "at.inlay.component#bodyExternal", did: SERVICE_DID }, 2329 - imports: [HOST_PACK_URI], 2021 + imports: [HOST_DID], 2330 2022 }; 2331 2023 2332 2024 const cardComponent: ComponentRecord = { 2333 2025 $type: "at.inlay.component", 2334 - type: Card, 2335 2026 body: { $type: "at.inlay.component#bodyExternal", did: SERVICE_DID }, 2336 - imports: [HOST_PACK_URI], 2027 + imports: [HOST_DID], 2337 2028 }; 2338 2029 2339 2030 const { options, log } = testResolver({ 2340 2031 ...HOST_RECORDS, 2341 - ["at://did:plc:test/at.inlay.component/grtng"]: greetingComponent, 2342 - ["at://did:plc:test/at.inlay.component/crd"]: cardComponent, 2343 - ["at://did:plc:test/at.inlay.pack/app"]: { 2344 - $type: "at.inlay.pack", 2345 - name: "app", 2346 - exports: [ 2347 - { 2348 - type: Greeting, 2349 - component: "at://did:plc:test/at.inlay.component/grtng", 2350 - }, 2351 - { type: Card, component: "at://did:plc:test/at.inlay.component/crd" }, 2352 - ], 2353 - }, 2032 + [`at://${XRPC_DID}/at.inlay.component/${Greeting}`]: greetingComponent, 2033 + [`at://${XRPC_DID}/at.inlay.component/${Card}`]: cardComponent, 2354 2034 [`xrpc:${SERVICE_DID}:${Greeting}`]: () => ({ 2355 2035 node: $(Text, { value: "hi" }), 2356 2036 }), ··· 2362 2042 await renderToCompletion( 2363 2043 $(Root, {}), 2364 2044 options, 2365 - createContext(wrapperComponent) 2045 + createContext( 2046 + wrapperComponent, 2047 + `at://${APP_DID}/at.inlay.component/${Root}` 2048 + ) 2366 2049 ); 2367 2050 2368 - // Both xrpc starts appear before either ends 2051 + // Both component fetches start concurrently, then both xrpc calls start concurrently 2369 2052 assertLog(log, [ 2370 2053 `lexicon ${Root}`, 2371 - `fetch ${"at://did:plc:test/at.inlay.pack/app"}`, 2372 - `fetch:start ${"at://did:plc:test/at.inlay.component/grtng"}`, 2373 - `fetch:start ${"at://did:plc:test/at.inlay.component/crd"}`, 2374 - `fetch:end ${"at://did:plc:test/at.inlay.component/grtng"}`, 2375 - `fetch:end ${"at://did:plc:test/at.inlay.component/crd"}`, 2054 + `fetch:start at://${XRPC_DID}/at.inlay.component/${Greeting}`, 2055 + `fetch:start at://${XRPC_DID}/at.inlay.component/${Card}`, 2056 + `fetch:end at://${XRPC_DID}/at.inlay.component/${Greeting}`, 2057 + `fetch:end at://${XRPC_DID}/at.inlay.component/${Card}`, 2376 2058 `lexicon ${Greeting}`, 2377 2059 `lexicon ${Card}`, 2378 2060 `xrpc:start procedure ${Greeting} -> ${SERVICE_DID}`, 2379 2061 `xrpc:start procedure ${Card} -> ${SERVICE_DID}`, 2380 2062 `xrpc:end procedure ${Greeting} -> ${SERVICE_DID}`, 2381 2063 `xrpc:end procedure ${Card} -> ${SERVICE_DID}`, 2382 - `fetch ${HOST_PACK_URI}`, 2383 - `fetch ${"at://did:plc:host/at.inlay.component/txt"}`, 2064 + `fetch:start at://${HOST_DID}/at.inlay.component/${Text}`, 2065 + `fetch:end at://${HOST_DID}/at.inlay.component/${Text}`, 2384 2066 `lexicon ${Text}`, 2385 2067 `lexicon ${Text}`, 2386 2068 ]); ··· 2468 2150 // PostCard displays the post's text field from a fetched record. 2469 2151 const postCardComponent: ComponentRecord = { 2470 2152 $type: "at.inlay.component", 2471 - type: PostCard, 2472 2153 body: { 2473 2154 $type: "at.inlay.component#bodyTemplate", 2474 2155 node: serializeTree( 2475 2156 $(Text, { value: $(Binding, { path: ["record", "text"] }) }) 2476 2157 ), 2477 2158 }, 2478 - imports: [HOST_PACK_URI], 2159 + imports: [HOST_DID], 2479 2160 view: { 2480 2161 $type: "at.inlay.component#view", 2481 2162 prop: "uri", ··· 2490 2171 2491 2172 const postUri = "at://did:plc:alice/app.bsky.feed.post/123"; 2492 2173 const cache = withGlobalCache({ [postUri]: { text: "Hello" } }); 2493 - const ctx = createContext(postCardComponent); 2174 + const ctx = createContext( 2175 + postCardComponent, 2176 + `at://${APP_DID}/at.inlay.component/${PostCard}` 2177 + ); 2494 2178 2495 2179 // Request 1 — cold, fetches post record 2496 2180 const r1 = cache.request(); ··· 2502 2186 assert.deepEqual(out1, h("span", { value: "Hello" })); 2503 2187 assertLog(r1.log, [ 2504 2188 `lexicon ${PostCard}`, 2505 - `fetch:start ${HOST_PACK_URI}`, 2506 - `fetch:start ${postUri}`, 2507 - `fetch:end ${HOST_PACK_URI}`, 2508 - `fetch:end ${postUri}`, 2509 - `fetch ${"at://did:plc:host/at.inlay.component/txt"}`, 2189 + `fetch ${postUri}`, 2190 + `fetch at://${HOST_DID}/at.inlay.component/${Text}`, 2510 2191 `lexicon ${Text}`, 2511 2192 ]); 2512 2193 ··· 2542 2223 // host can invalidate when the profile changes. 2543 2224 const greetingComponent: ComponentRecord = { 2544 2225 $type: "at.inlay.component", 2545 - type: Greeting, 2546 2226 body: { 2547 2227 $type: "at.inlay.component#bodyExternal", 2548 2228 did: SERVICE_DID, 2549 2229 }, 2550 - imports: [HOST_PACK_URI], 2230 + imports: [HOST_DID], 2551 2231 }; 2552 2232 2553 2233 const profileUri = "at://did:plc:alice/app.bsky.actor.profile/self"; ··· 2565 2245 }, 2566 2246 }; 2567 2247 2568 - const componentUri = "at://did:plc:test/at.inlay.component/grtng"; 2248 + const componentUri = `at://${APP_DID}/at.inlay.component/${Greeting}`; 2569 2249 const cache = withGlobalCache(records); 2570 2250 const ctx = createContext(greetingComponent, componentUri); 2571 2251 ··· 2576 2256 assertLog(r1.log, [ 2577 2257 `lexicon ${Greeting}`, 2578 2258 `xrpc procedure ${Greeting} -> ${SERVICE_DID}`, 2579 - `fetch ${HOST_PACK_URI}`, 2580 - `fetch ${"at://did:plc:host/at.inlay.component/txt"}`, 2259 + `fetch at://${HOST_DID}/at.inlay.component/${Text}`, 2581 2260 `lexicon ${Text}`, 2582 2261 ]); 2583 2262 ··· 2627 2306 // post record. When the post changes, Layout's xrpc stays cached 2628 2307 // but the dynamiic child within it re-renders with new data. 2629 2308 // This is possible because Layout is cached with "holes" for children. 2309 + const CACHE_DID = "did:plc:cache-test"; 2630 2310 const layoutComponent: ComponentRecord = { 2631 2311 $type: "at.inlay.component", 2632 - type: Layout, 2633 2312 body: { 2634 2313 $type: "at.inlay.component#bodyExternal", 2635 2314 did: SERVICE_DID, 2636 2315 }, 2637 - imports: [HOST_PACK_URI], 2316 + imports: [HOST_DID], 2638 2317 }; 2639 2318 2640 2319 const postCardComponent: ComponentRecord = { 2641 2320 $type: "at.inlay.component", 2642 - type: PostCard, 2643 2321 body: { 2644 2322 $type: "at.inlay.component#bodyTemplate", 2645 2323 node: serializeTree( 2646 2324 $(Text, { value: $(Binding, { path: ["record", "text"] }) }) 2647 2325 ), 2648 2326 }, 2649 - imports: [HOST_PACK_URI], 2327 + imports: [HOST_DID], 2650 2328 view: { 2651 2329 $type: "at.inlay.component#view", 2652 2330 prop: "uri", ··· 2660 2338 }; 2661 2339 2662 2340 const postUri = "at://did:plc:alice/app.bsky.feed.post/123"; 2663 - const layoutUri = "at://did:plc:test/at.inlay.component/lyout"; 2664 - const appPackUri = "at://did:plc:test/at.inlay.pack/app"; 2341 + const layoutUri = `at://${CACHE_DID}/at.inlay.component/${Layout}`; 2665 2342 const cache = withGlobalCache({ 2666 2343 [postUri]: { text: "Hello" }, 2667 - ["at://did:plc:test/at.inlay.component/pstcrd"]: postCardComponent, 2668 - [appPackUri]: { 2669 - $type: "at.inlay.pack", 2670 - name: "app", 2671 - exports: [ 2672 - { 2673 - type: PostCard, 2674 - component: "at://did:plc:test/at.inlay.component/pstcrd", 2675 - }, 2676 - ], 2677 - }, 2344 + [`at://${CACHE_DID}/at.inlay.component/${PostCard}`]: postCardComponent, 2678 2345 [`xrpc:${SERVICE_DID}:${Layout}`]: (params: { 2679 2346 body?: Record<string, unknown>; 2680 2347 }) => ({ ··· 2689 2356 }); 2690 2357 2691 2358 const ctx: RenderContext = { 2692 - imports: [appPackUri, HOST_PACK_URI], 2359 + imports: [CACHE_DID, HOST_DID], 2693 2360 component: layoutComponent, 2694 2361 componentUri: layoutUri, 2695 2362 }; ··· 2713 2380 assertLog(r1.log, [ 2714 2381 `lexicon ${Layout}`, 2715 2382 `xrpc procedure ${Layout} -> ${SERVICE_DID}`, 2716 - `fetch:start ${HOST_PACK_URI}`, 2717 - `fetch:end ${HOST_PACK_URI}`, 2718 - `fetch:start ${"at://did:plc:host/at.inlay.component/stck"}`, 2719 - `fetch:end ${"at://did:plc:host/at.inlay.component/stck"}`, 2383 + `fetch at://${HOST_DID}/at.inlay.component/${Stack}`, 2720 2384 `lexicon ${Stack}`, 2721 - `fetch:start ${"at://did:plc:host/at.inlay.component/txt"}`, 2722 - `fetch:start ${appPackUri}`, 2723 - `fetch:end ${"at://did:plc:host/at.inlay.component/txt"}`, 2724 - `fetch:end ${appPackUri}`, 2725 - `fetch:start ${"at://did:plc:test/at.inlay.component/pstcrd"}`, 2385 + `fetch at://${HOST_DID}/at.inlay.component/${Text}`, 2386 + `fetch:start at://${CACHE_DID}/at.inlay.component/${PostCard}`, 2387 + `fetch:start at://${HOST_DID}/at.inlay.component/${PostCard}`, 2388 + `fetch:end at://${CACHE_DID}/at.inlay.component/${PostCard}`, 2389 + `fetch:end at://${HOST_DID}/at.inlay.component/${PostCard}`, 2726 2390 `lexicon ${Text}`, 2727 - `fetch:end ${"at://did:plc:test/at.inlay.component/pstcrd"}`, 2728 2391 `lexicon ${PostCard}`, 2729 2392 `fetch ${postUri}`, 2730 2393 `lexicon ${Text}`, ··· 2806 2469 const profile = "test.app.Profile" as const; 2807 2470 const profileComponent: ComponentRecord = { 2808 2471 $type: "at.inlay.component", 2809 - type: profile, 2810 2472 body: { 2811 2473 $type: "at.inlay.component#bodyTemplate", 2812 2474 node: serializeTree( 2813 2475 $(Text, { value: $(Binding, { path: ["record", "displayName"] }) }) 2814 2476 ), 2815 2477 }, 2816 - imports: [HOST_PACK_URI], 2478 + imports: [HOST_DID], 2817 2479 view: { 2818 2480 $type: "at.inlay.component#view", 2819 2481 prop: "uri", ··· 2836 2498 const output = await renderToCompletion( 2837 2499 $(profile, { uri: "did:plc:alice" }), 2838 2500 options, 2839 - createContext(profileComponent) 2501 + createContext( 2502 + profileComponent, 2503 + `at://${APP_DID}/at.inlay.component/${profile}` 2504 + ) 2840 2505 ); 2841 2506 2842 2507 assert.deepEqual(output, h("span", { value: "Alice" })); ··· 2846 2511 const profile = "test.app.Profile" as const; 2847 2512 const profileComponent: ComponentRecord = { 2848 2513 $type: "at.inlay.component", 2849 - type: profile, 2850 2514 body: { 2851 2515 $type: "at.inlay.component#bodyTemplate", 2852 2516 node: serializeTree( 2853 2517 $(Text, { value: $(Binding, { path: ["record", "displayName"] }) }) 2854 2518 ), 2855 2519 }, 2856 - imports: [HOST_PACK_URI], 2520 + imports: [HOST_DID], 2857 2521 view: { 2858 2522 $type: "at.inlay.component#view", 2859 2523 prop: "uri", ··· 2876 2540 const output = await renderToCompletion( 2877 2541 $(profile, { uri: "did:plc:alice" }), 2878 2542 options, 2879 - createContext(profileComponent) 2543 + createContext( 2544 + profileComponent, 2545 + `at://${APP_DID}/at.inlay.component/${profile}` 2546 + ) 2880 2547 ); 2881 2548 2882 2549 assert.deepEqual(output, h("span", { value: "Alice" })); ··· 2886 2553 const profile = "test.app.Profile" as const; 2887 2554 const profileComponent: ComponentRecord = { 2888 2555 $type: "at.inlay.component", 2889 - type: profile, 2890 2556 body: { 2891 2557 $type: "at.inlay.component#bodyTemplate", 2892 2558 node: serializeTree( 2893 2559 $(Text, { value: $(Binding, { path: ["record", "displayName"] }) }) 2894 2560 ), 2895 2561 }, 2896 - imports: [HOST_PACK_URI], 2562 + imports: [HOST_DID], 2897 2563 view: { 2898 2564 $type: "at.inlay.component#view", 2899 2565 prop: "uri", ··· 2919 2585 uri: "at://did:plc:alice/app.bsky.actor.profile/self", 2920 2586 }), 2921 2587 options, 2922 - createContext(profileComponent) 2588 + createContext( 2589 + profileComponent, 2590 + `at://${APP_DID}/at.inlay.component/${profile}` 2591 + ) 2923 2592 ); 2924 2593 2925 2594 assert.deepEqual(output, h("span", { value: "Alice" })); ··· 2929 2598 const post = "test.app.Post" as const; 2930 2599 const postComponent: ComponentRecord = { 2931 2600 $type: "at.inlay.component", 2932 - type: post, 2933 2601 body: { 2934 2602 $type: "at.inlay.component#bodyTemplate", 2935 2603 node: serializeTree( 2936 2604 $(Text, { value: $(Binding, { path: ["props", "uri"] }) }) 2937 2605 ), 2938 2606 }, 2939 - imports: [HOST_PACK_URI], 2607 + imports: [HOST_DID], 2940 2608 view: { 2941 2609 $type: "at.inlay.component#view", 2942 2610 prop: "uri", ··· 2954 2622 const output = await renderToCompletion( 2955 2623 $(post, { uri: "did:plc:alice" }), 2956 2624 { resolver }, 2957 - createContext(postComponent) 2625 + createContext(postComponent, `at://${APP_DID}/at.inlay.component/${post}`) 2958 2626 ); 2959 2627 2960 2628 assert.equal((output as any).tag, "error"); ··· 2978 2646 it("validates props against published lexicon", async () => { 2979 2647 const viewComponent: ComponentRecord = { 2980 2648 $type: "at.inlay.component", 2981 - type: View, 2982 2649 body: { 2983 2650 $type: "at.inlay.component#bodyTemplate", 2984 2651 node: { $: "$", type: Text, props: {} }, 2985 2652 }, 2986 - imports: [HOST_PACK_URI], 2653 + imports: [HOST_DID], 2987 2654 }; 2988 2655 2989 2656 const { resolver } = world(); ··· 3012 2679 }; 3013 2680 3014 2681 const opts: RenderOptions = { resolver }; 3015 - const ctx = createContext(viewComponent); 2682 + const ctx = createContext( 2683 + viewComponent, 2684 + `at://${APP_DID}/at.inlay.component/${View}` 2685 + ); 3016 2686 3017 2687 const ok = await renderToCompletion( 3018 2688 $(View, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), ··· 3028 2698 it("synthesizes validation from viewRecord", async () => { 3029 2699 const cardComponent: ComponentRecord = { 3030 2700 $type: "at.inlay.component", 3031 - type: Card, 3032 2701 body: { 3033 2702 $type: "at.inlay.component#bodyTemplate", 3034 2703 node: { $: "$", type: Text, props: {} }, 3035 2704 }, 3036 - imports: [HOST_PACK_URI], 2705 + imports: [HOST_DID], 3037 2706 view: { 3038 2707 $type: "at.inlay.component#view", 3039 2708 prop: "uri", ··· 3047 2716 }; 3048 2717 3049 2718 const { options } = world(); 3050 - const ctx = createContext(cardComponent); 2719 + const ctx = createContext( 2720 + cardComponent, 2721 + `at://${APP_DID}/at.inlay.component/${Card}` 2722 + ); 3051 2723 3052 2724 const ok = await renderToCompletion( 3053 2725 $(Card, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), ··· 3067 2739 it("synthesizes validation from viewPrimitive", async () => { 3068 2740 const timestampComponent: ComponentRecord = { 3069 2741 $type: "at.inlay.component", 3070 - type: Timestamp, 3071 2742 body: { 3072 2743 $type: "at.inlay.component#bodyTemplate", 3073 2744 node: { $: "$", type: Text, props: {} }, 3074 2745 }, 3075 - imports: [HOST_PACK_URI], 2746 + imports: [HOST_DID], 3076 2747 view: { 3077 2748 $type: "at.inlay.component#view", 3078 2749 prop: "value", ··· 3087 2758 }; 3088 2759 3089 2760 const { options } = world(); 3090 - const ctx = createContext(timestampComponent); 2761 + const ctx = createContext( 2762 + timestampComponent, 2763 + `at://${APP_DID}/at.inlay.component/${Timestamp}` 2764 + ); 3091 2765 3092 2766 const ok = await renderToCompletion( 3093 2767 $(Timestamp, { value: "2026-01-01T00:00:00Z" }), ··· 3107 2781 it("validates collection constraints from viewRecord", async () => { 3108 2782 const cardComponent: ComponentRecord = { 3109 2783 $type: "at.inlay.component", 3110 - type: Card, 3111 2784 body: { 3112 2785 $type: "at.inlay.component#bodyTemplate", 3113 2786 node: { $: "$", type: Text, props: {} }, 3114 2787 }, 3115 - imports: [HOST_PACK_URI], 2788 + imports: [HOST_DID], 3116 2789 view: { 3117 2790 $type: "at.inlay.component#view", 3118 2791 prop: "uri", ··· 3126 2799 }; 3127 2800 3128 2801 const { options } = world(); 3129 - const ctx = createContext(cardComponent); 2802 + const ctx = createContext( 2803 + cardComponent, 2804 + `at://${APP_DID}/at.inlay.component/${Card}` 2805 + ); 3130 2806 3131 2807 const ok = await renderToCompletion( 3132 2808 $(Card, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), ··· 3150 2826 it("validates builtin (no body) props against lexicon", async () => { 3151 2827 const pageComponent: ComponentRecord = { 3152 2828 $type: "at.inlay.component", 3153 - type: Page, 3154 2829 body: { 3155 2830 $type: "at.inlay.component#bodyTemplate", 3156 2831 node: serializeTree($(Text, {})), 3157 2832 }, 3158 - imports: [HOST_PACK_URI], 2833 + imports: [HOST_DID], 3159 2834 }; 3160 2835 3161 - const { resolver } = world({ 3162 - ["at://did:plc:test/at.inlay.component/page"]: pageComponent, 3163 - ["at://did:plc:test/at.inlay.pack/app"]: { 3164 - $type: "at.inlay.pack", 3165 - name: "app", 3166 - exports: [ 3167 - { 3168 - type: Page, 3169 - component: "at://did:plc:test/at.inlay.component/page", 3170 - }, 3171 - ], 3172 - }, 3173 - }); 2836 + const { resolver } = world(); 3174 2837 3175 2838 resolver.resolveLexicon = async (nsid: string) => { 3176 2839 if (nsid === Text) { ··· 3195 2858 return null; 3196 2859 }; 3197 2860 3198 - const ctx = createContext(pageComponent); 2861 + const ctx = createContext( 2862 + pageComponent, 2863 + `at://${APP_DID}/at.inlay.component/${Page}` 2864 + ); 3199 2865 const bad = await renderToCompletion($(Page, {}), { resolver }, ctx); 3200 2866 3201 2867 assertError( ··· 3221 2887 // A host may implement this with try/catch or UI framework error boundaries. 3222 2888 // 3223 2889 describe("missing bindings and Maybe", () => { 2890 + const MAYBE_DID = "did:plc:maybe"; 3224 2891 const missingCard = { 3225 2892 $type: "at.inlay.component", 3226 - type: MissingCard, 3227 2893 body: { 3228 2894 $type: "at.inlay.component#bodyTemplate", 3229 2895 node: serializeTree( 3230 2896 $(Text, { value: $(Binding, { path: ["props", "text"] }) }) 3231 2897 ), 3232 2898 }, 3233 - imports: [HOST_PACK_URI], 2899 + imports: [HOST_DID], 3234 2900 }; 3235 2901 const externalComponent: ComponentRecord = { 3236 2902 $type: "at.inlay.component", 3237 - type: External, 3238 2903 body: { 3239 2904 $type: "at.inlay.component#bodyExternal", 3240 2905 did: SERVICE_DID, 3241 2906 }, 3242 - imports: [HOST_PACK_URI], 2907 + imports: [HOST_DID], 3243 2908 }; 3244 2909 const APP_PACK: Record<AtUriString, TestMock> = { 3245 - ["at://did:plc:test/at.inlay.component/msng"]: missingCard, 3246 - ["at://did:plc:test/at.inlay.component/extrnl"]: externalComponent, 3247 - ["at://did:plc:test/at.inlay.pack/app"]: { 3248 - $type: "at.inlay.pack", 3249 - name: "app", 3250 - exports: [ 3251 - { 3252 - type: MissingCard, 3253 - component: "at://did:plc:test/at.inlay.component/msng", 3254 - }, 3255 - { 3256 - type: External, 3257 - component: "at://did:plc:test/at.inlay.component/extrnl", 3258 - }, 3259 - ], 3260 - }, 2910 + [`at://${MAYBE_DID}/at.inlay.component/${MissingCard}`]: missingCard, 2911 + [`at://${MAYBE_DID}/at.inlay.component/${External}`]: externalComponent, 3261 2912 }; 3262 2913 3263 2914 it("present bindings resolve normally", async () => { 3264 2915 const cardComponent: ComponentRecord = { 3265 2916 $type: "at.inlay.component", 3266 - type: Card, 3267 2917 body: { 3268 2918 $type: "at.inlay.component#bodyTemplate", 3269 2919 node: serializeTree( 3270 2920 $(Text, { value: $(Binding, { path: ["record", "title"] }) }) 3271 2921 ), 3272 2922 }, 3273 - imports: [HOST_PACK_URI], 2923 + imports: [HOST_DID], 3274 2924 view: { 3275 2925 $type: "at.inlay.component#view", 3276 2926 prop: "uri", ··· 3290 2940 const output = await renderToCompletion( 3291 2941 $(Card, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 3292 2942 options, 3293 - createContext(cardComponent) 2943 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 3294 2944 ); 3295 2945 assert.deepEqual(output, h("span", { value: "Hello" })); 3296 2946 }); ··· 3299 2949 // No props match the binding path → MissingError → error output 3300 2950 const cardComponent: ComponentRecord = { 3301 2951 $type: "at.inlay.component", 3302 - type: Card, 3303 2952 body: { 3304 2953 $type: "at.inlay.component#bodyTemplate", 3305 2954 node: serializeTree( 3306 2955 $(Stack, { children: $(Binding, { path: ["props", "text"] }) }) 3307 2956 ), 3308 2957 }, 3309 - imports: [HOST_PACK_URI], 2958 + imports: [HOST_DID], 3310 2959 }; 3311 2960 3312 2961 const { options } = world(); 3313 2962 const output = await renderToCompletion( 3314 2963 $(Card, {}), 3315 2964 options, 3316 - createContext(cardComponent) 2965 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 3317 2966 ); 3318 2967 assertMissing(output, ["props", "text"]); 3319 2968 }); ··· 3323 2972 // optional atproto record fields. 3324 2973 const cardComponent: ComponentRecord = { 3325 2974 $type: "at.inlay.component", 3326 - type: Card, 3327 2975 body: { 3328 2976 $type: "at.inlay.component#bodyTemplate", 3329 2977 node: serializeTree( ··· 3332 2980 }) 3333 2981 ), 3334 2982 }, 3335 - imports: [HOST_PACK_URI], 2983 + imports: [HOST_DID], 3336 2984 }; 3337 2985 3338 2986 const { options } = world(); 3339 2987 const output = await renderToCompletion( 3340 2988 $(Card, {}), 3341 2989 options, 3342 - createContext(cardComponent) 2990 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 3343 2991 ); 3344 2992 assertMissing(output, ["record", "reply", "parent", "uri"]); 3345 2993 }); ··· 3349 2997 // doesn't guarantee all bindings resolve. 3350 2998 const cardComponent: ComponentRecord = { 3351 2999 $type: "at.inlay.component", 3352 - type: Card, 3353 3000 body: { 3354 3001 $type: "at.inlay.component#bodyTemplate", 3355 3002 node: serializeTree( ··· 3363 3010 }) 3364 3011 ), 3365 3012 }, 3366 - imports: [HOST_PACK_URI], 3013 + imports: [HOST_DID], 3367 3014 view: { 3368 3015 $type: "at.inlay.component#view", 3369 3016 prop: "uri", ··· 3383 3030 const output = await renderToCompletion( 3384 3031 $(Card, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 3385 3032 options, 3386 - createContext(cardComponent) 3033 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 3387 3034 ); 3388 3035 assertMissing(output, ["record", "reply", "parent", "uri"]); 3389 3036 }); ··· 3404 3051 // second Text. Only "outside" survives. 3405 3052 const cardComponent: ComponentRecord = { 3406 3053 $type: "at.inlay.component", 3407 - type: Card, 3408 3054 body: { 3409 3055 $type: "at.inlay.component#bodyTemplate", 3410 3056 node: serializeTree( ··· 3430 3076 }) 3431 3077 ), 3432 3078 }, 3433 - imports: [HOST_PACK_URI], 3079 + imports: [HOST_DID], 3434 3080 view: { 3435 3081 $type: "at.inlay.component#view", 3436 3082 prop: "uri", ··· 3450 3096 const output = await renderToCompletion( 3451 3097 $(Card, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 3452 3098 options, 3453 - createContext(cardComponent) 3099 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 3454 3100 ); 3455 3101 assert.deepEqual( 3456 3102 output, ··· 3461 3107 it("Maybe renders fallback when provided", async () => { 3462 3108 const cardComponent: ComponentRecord = { 3463 3109 $type: "at.inlay.component", 3464 - type: Card, 3465 3110 body: { 3466 3111 $type: "at.inlay.component#bodyTemplate", 3467 3112 node: serializeTree( ··· 3471 3116 }) 3472 3117 ), 3473 3118 }, 3474 - imports: [ 3475 - HOST_PACK_URI, 3476 - "at://did:plc:test/at.inlay.pack/app", 3477 - ] as AtUriString[], 3119 + imports: [HOST_DID, MAYBE_DID], 3478 3120 }; 3479 3121 3480 3122 const { options } = world(APP_PACK); ··· 3482 3124 const output = await renderToCompletion( 3483 3125 $(Card, {}), 3484 3126 options, 3485 - createContext(cardComponent) 3127 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 3486 3128 ); 3487 3129 assert.deepEqual(output, h("span", { value: "unavailable" })); 3488 3130 }); ··· 3496 3138 // </Stack> 3497 3139 const postCardComponent: ComponentRecord = { 3498 3140 $type: "at.inlay.component", 3499 - type: PostCard, 3500 3141 body: { 3501 3142 $type: "at.inlay.component#bodyTemplate", 3502 3143 node: serializeTree( ··· 3517 3158 }) 3518 3159 ), 3519 3160 }, 3520 - imports: [HOST_PACK_URI], 3161 + imports: [HOST_DID], 3521 3162 view: { 3522 3163 $type: "at.inlay.component#view", 3523 3164 prop: "uri", ··· 3539 3180 const output = await renderToCompletion( 3540 3181 $(PostCard, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 3541 3182 options, 3542 - createContext(postCardComponent) 3183 + createContext( 3184 + postCardComponent, 3185 + `at://${APP_DID}/at.inlay.component/${PostCard}` 3186 + ) 3543 3187 ); 3544 3188 3545 3189 assert.deepEqual( ··· 3556 3200 it("MissingError propagates through external component slots", async () => { 3557 3201 const postCardComponent: ComponentRecord = { 3558 3202 $type: "at.inlay.component", 3559 - type: PostCard, 3560 3203 body: { 3561 3204 $type: "at.inlay.component#bodyTemplate", 3562 3205 node: serializeTree( ··· 3567 3210 }) 3568 3211 ), 3569 3212 }, 3570 - imports: [ 3571 - HOST_PACK_URI, 3572 - "at://did:plc:test/at.inlay.pack/app", 3573 - ] as AtUriString[], 3213 + imports: [HOST_DID, MAYBE_DID], 3574 3214 view: { 3575 3215 $type: "at.inlay.component#view", 3576 3216 prop: "uri", ··· 3598 3238 const output = await renderToCompletion( 3599 3239 $(PostCard, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 3600 3240 options, 3601 - createContext(postCardComponent) 3241 + createContext( 3242 + postCardComponent, 3243 + `at://${APP_DID}/at.inlay.component/${PostCard}` 3244 + ) 3602 3245 ); 3603 3246 assertMissing(output, ["record", "reply", "parent", "uri"]); 3604 3247 }); ··· 3612 3255 // </Stack> 3613 3256 const postCardComponent: ComponentRecord = { 3614 3257 $type: "at.inlay.component", 3615 - type: PostCard, 3616 3258 body: { 3617 3259 $type: "at.inlay.component#bodyTemplate", 3618 3260 node: serializeTree( ··· 3633 3275 }) 3634 3276 ), 3635 3277 }, 3636 - imports: [ 3637 - HOST_PACK_URI, 3638 - "at://did:plc:test/at.inlay.pack/app", 3639 - ] as AtUriString[], 3278 + imports: [HOST_DID, MAYBE_DID], 3640 3279 view: { 3641 3280 $type: "at.inlay.component#view", 3642 3281 prop: "uri", ··· 3664 3303 const output = await renderToCompletion( 3665 3304 $(PostCard, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 3666 3305 options, 3667 - createContext(postCardComponent) 3306 + createContext( 3307 + postCardComponent, 3308 + `at://${APP_DID}/at.inlay.component/${PostCard}` 3309 + ) 3668 3310 ); 3669 3311 3670 3312 assert.deepEqual( ··· 3688 3330 // </Maybe> 3689 3331 const cardComponent: ComponentRecord = { 3690 3332 $type: "at.inlay.component", 3691 - type: Card, 3692 3333 body: { 3693 3334 $type: "at.inlay.component#bodyTemplate", 3694 3335 node: serializeTree( ··· 3706 3347 }) 3707 3348 ), 3708 3349 }, 3709 - imports: [ 3710 - HOST_PACK_URI, 3711 - "at://did:plc:test/at.inlay.pack/app", 3712 - ] as AtUriString[], 3350 + imports: [HOST_DID, MAYBE_DID], 3713 3351 view: { 3714 3352 $type: "at.inlay.component#view", 3715 3353 prop: "uri", ··· 3735 3373 const output = await renderToCompletion( 3736 3374 $(Card, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 3737 3375 options, 3738 - createContext(cardComponent) 3376 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 3739 3377 ); 3740 3378 assert.deepEqual(output, h("span", { value: "caught" })); 3741 3379 }); ··· 3754 3392 // Maybe should catch the missing binding and render "fallback". 3755 3393 // Result: header + "fallback". 3756 3394 const SafeWrapper = "test.app.SafeWrapper" as const; 3395 + const SAFE_DID = "did:plc:safe"; 3757 3396 const safeWrapperComponent: ComponentRecord = { 3758 3397 $type: "at.inlay.component", 3759 - type: SafeWrapper, 3760 3398 body: { 3761 3399 $type: "at.inlay.component#bodyTemplate", 3762 3400 node: serializeTree( ··· 3771 3409 }) 3772 3410 ), 3773 3411 }, 3774 - imports: [HOST_PACK_URI], 3412 + imports: [HOST_DID], 3775 3413 }; 3776 3414 3777 3415 const cardComponent: ComponentRecord = { 3778 3416 $type: "at.inlay.component", 3779 - type: Card, 3780 3417 body: { 3781 3418 $type: "at.inlay.component#bodyTemplate", 3782 3419 node: serializeTree($(SafeWrapper, {})), 3783 3420 }, 3784 - imports: [ 3785 - HOST_PACK_URI, 3786 - "at://did:plc:test/at.inlay.pack/safe", 3787 - ] as AtUriString[], 3421 + imports: [HOST_DID, SAFE_DID], 3788 3422 }; 3789 3423 3790 3424 const { options } = world({ 3791 - ["at://did:plc:test/at.inlay.component/safe"]: safeWrapperComponent, 3792 - ["at://did:plc:test/at.inlay.pack/safe"]: { 3793 - $type: "at.inlay.pack", 3794 - name: "safe", 3795 - exports: [ 3796 - { 3797 - type: SafeWrapper, 3798 - component: "at://did:plc:test/at.inlay.component/safe", 3799 - }, 3800 - ], 3801 - }, 3425 + [`at://${SAFE_DID}/at.inlay.component/${SafeWrapper}`]: 3426 + safeWrapperComponent, 3802 3427 }); 3803 3428 3804 3429 const output = await renderToCompletion( 3805 3430 $(Card, {}), 3806 3431 options, 3807 - createContext(cardComponent) 3432 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 3808 3433 ); 3809 3434 assert.deepEqual( 3810 3435 output, ··· 3822 3447 // reply is absent → Missing element in uri prop → throws at primitive 3823 3448 const cardComponent: ComponentRecord = { 3824 3449 $type: "at.inlay.component", 3825 - type: Card, 3826 3450 body: { 3827 3451 $type: "at.inlay.component#bodyTemplate", 3828 3452 node: serializeTree( ··· 3831 3455 }) 3832 3456 ), 3833 3457 }, 3834 - imports: [HOST_PACK_URI], 3458 + imports: [HOST_DID], 3835 3459 view: { 3836 3460 $type: "at.inlay.component#view", 3837 3461 prop: "uri", ··· 3851 3475 const output = await renderToCompletion( 3852 3476 $(Card, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 3853 3477 options, 3854 - createContext(cardComponent) 3478 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 3855 3479 ); 3856 3480 assertMissing(output, ["record", "reply", "parent", "uri"]); 3857 3481 }); ··· 3862 3486 // </Maybe> 3863 3487 const cardComponent: ComponentRecord = { 3864 3488 $type: "at.inlay.component", 3865 - type: Card, 3866 3489 body: { 3867 3490 $type: "at.inlay.component#bodyTemplate", 3868 3491 node: serializeTree( ··· 3878 3501 }) 3879 3502 ), 3880 3503 }, 3881 - imports: [HOST_PACK_URI], 3504 + imports: [HOST_DID], 3882 3505 view: { 3883 3506 $type: "at.inlay.component#view", 3884 3507 prop: "uri", ··· 3898 3521 const output = await renderToCompletion( 3899 3522 $(Card, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 3900 3523 options, 3901 - createContext(cardComponent) 3524 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 3902 3525 ); 3903 3526 assert.deepEqual(output, h("span", { value: "nope" })); 3904 3527 }); ··· 3908 3531 // Missing element ends up nested inside an object prop value 3909 3532 const cardComponent: ComponentRecord = { 3910 3533 $type: "at.inlay.component", 3911 - type: Card, 3912 3534 body: { 3913 3535 $type: "at.inlay.component#bodyTemplate", 3914 3536 node: serializeTree( ··· 3921 3543 }) 3922 3544 ), 3923 3545 }, 3924 - imports: [HOST_PACK_URI], 3546 + imports: [HOST_DID], 3925 3547 view: { 3926 3548 $type: "at.inlay.component#view", 3927 3549 prop: "uri", ··· 3941 3563 const output = await renderToCompletion( 3942 3564 $(Card, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 3943 3565 options, 3944 - createContext(cardComponent) 3566 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 3945 3567 ); 3946 3568 assertMissing(output, ["record", "reply", "parent", "uri"]); 3947 3569 }); ··· 3967 3589 // SafeWrapper's Maybe is closer to the error — it catches first. 3968 3590 // The outer Maybe never fires. Result: header + "inner" + "sibling". 3969 3591 const SafeWrapper = "test.app.SafeWrapper" as const; 3592 + const SAFE_DID = "did:plc:safe-inner"; 3970 3593 const safeWrapperComponent: ComponentRecord = { 3971 3594 $type: "at.inlay.component", 3972 - type: SafeWrapper, 3973 3595 body: { 3974 3596 $type: "at.inlay.component#bodyTemplate", 3975 3597 node: serializeTree( ··· 3984 3606 }) 3985 3607 ), 3986 3608 }, 3987 - imports: [HOST_PACK_URI], 3609 + imports: [HOST_DID], 3988 3610 }; 3989 3611 3990 3612 const cardComponent: ComponentRecord = { 3991 3613 $type: "at.inlay.component", 3992 - type: Card, 3993 3614 body: { 3994 3615 $type: "at.inlay.component#bodyTemplate", 3995 3616 node: serializeTree( ··· 4010 3631 }) 4011 3632 ), 4012 3633 }, 4013 - imports: [ 4014 - HOST_PACK_URI, 4015 - "at://did:plc:test/at.inlay.pack/safe", 4016 - ] as AtUriString[], 3634 + imports: [HOST_DID, SAFE_DID], 4017 3635 view: { 4018 3636 $type: "at.inlay.component#view", 4019 3637 prop: "uri", ··· 4027 3645 }; 4028 3646 4029 3647 const { options } = world({ 4030 - ["at://did:plc:test/at.inlay.component/safe"]: safeWrapperComponent, 4031 - ["at://did:plc:test/at.inlay.pack/safe"]: { 4032 - $type: "at.inlay.pack", 4033 - name: "safe", 4034 - exports: [ 4035 - { 4036 - type: SafeWrapper, 4037 - component: "at://did:plc:test/at.inlay.component/safe", 4038 - }, 4039 - ], 4040 - }, 3648 + [`at://${SAFE_DID}/at.inlay.component/${SafeWrapper}`]: 3649 + safeWrapperComponent, 4041 3650 ["at://did:plc:alice/app.bsky.feed.post/123"]: { text: "Hello" }, 4042 3651 }); 4043 3652 4044 3653 const output = await renderToCompletion( 4045 3654 $(Card, { uri: "at://did:plc:alice/app.bsky.feed.post/123" }), 4046 3655 options, 4047 - createContext(cardComponent) 3656 + createContext(cardComponent, `at://${APP_DID}/at.inlay.component/${Card}`) 4048 3657 ); 4049 3658 assert.deepEqual( 4050 3659 output, ··· 4114 3723 function setup() { 4115 3724 const rootComponent: ComponentRecord = { 4116 3725 $type: "at.inlay.component", 4117 - type: Root, 4118 3726 body: { 4119 3727 $type: "at.inlay.component#bodyTemplate", 4120 3728 node: serializeTree( 4121 3729 $(Fragment, { children: $(Binding, { path: ["props", "children"] }) }) 4122 3730 ), 4123 3731 }, 4124 - imports: [HOST_PACK_URI], 3732 + imports: [HOST_DID], 4125 3733 }; 4126 3734 4127 3735 const { options, log } = world(); ··· 4145 3753 4146 3754 async function continuePage(cursor: Record<string, unknown>) { 4147 3755 const { imports, source, next } = cursor as { 4148 - imports: AtUriString[]; 3756 + imports: DidString[]; 4149 3757 source: { did: string; nsid: string }; 4150 3758 next: string; 4151 3759 }; ··· 4165 3773 4166 3774 it("two independent lists paginate statelessly via opaque cursors", async () => { 4167 3775 const { options, rootComponent, extra, continuePage } = setup(); 4168 - const ctx = createContext(rootComponent); 3776 + const ctx = createContext( 3777 + rootComponent, 3778 + `at://${APP_DID}/at.inlay.component/${Root}` 3779 + ); 4169 3780 4170 3781 // First list: followers 4171 3782 const fCursor = { 4172 - imports: [HOST_PACK_URI], 3783 + imports: [HOST_DID], 4173 3784 source: { did: "did:plc:svc", nsid: "app.bsky.getFollowers" }, 4174 3785 next: "f2", 4175 3786 }; ··· 4198 3809 4199 3810 // Second list: posts 4200 3811 const pCursor = { 4201 - imports: [HOST_PACK_URI], 3812 + imports: [HOST_DID], 4202 3813 source: { did: "did:plc:svc", nsid: "app.bsky.getPosts" }, 4203 3814 next: "p2", 4204 3815 }; ··· 4242 3853 it("forwards personalized: true to resolver.xrpc", async () => { 4243 3854 const greetingComponent: ComponentRecord = { 4244 3855 $type: "at.inlay.component", 4245 - type: Greeting, 4246 3856 body: { 4247 3857 $type: "at.inlay.component#bodyExternal", 4248 3858 did: SERVICE_DID, 4249 3859 personalized: true, 4250 3860 }, 4251 - imports: [HOST_PACK_URI], 3861 + imports: [HOST_DID], 4252 3862 }; 4253 3863 4254 3864 let capturedPersonalized: boolean | undefined; ··· 4268 3878 await renderToCompletion( 4269 3879 $(Greeting, { name: "world" }), 4270 3880 options, 4271 - createContext(greetingComponent) 3881 + createContext( 3882 + greetingComponent, 3883 + `at://${APP_DID}/at.inlay.component/${Greeting}` 3884 + ) 4272 3885 ); 4273 3886 4274 3887 assert.equal(capturedPersonalized, true); ··· 4277 3890 it("defaults personalized to false when not set", async () => { 4278 3891 const greetingComponent: ComponentRecord = { 4279 3892 $type: "at.inlay.component", 4280 - type: Greeting, 4281 3893 body: { 4282 3894 $type: "at.inlay.component#bodyExternal", 4283 3895 did: SERVICE_DID, 4284 3896 }, 4285 - imports: [HOST_PACK_URI], 3897 + imports: [HOST_DID], 4286 3898 }; 4287 3899 4288 3900 let capturedPersonalized: boolean | undefined; ··· 4302 3914 await renderToCompletion( 4303 3915 $(Greeting, { name: "world" }), 4304 3916 options, 4305 - createContext(greetingComponent) 3917 + createContext( 3918 + greetingComponent, 3919 + `at://${APP_DID}/at.inlay.component/${Greeting}` 3920 + ) 4306 3921 ); 4307 3922 4308 3923 assert.equal(capturedPersonalized, false);
+4 -3
proto/src/index.tsx
··· 5 5 import { ErrorBoundary } from "hono/jsx"; 6 6 import { serveStatic } from "@hono/node-server/serve-static"; 7 7 import { serve } from "@hono/node-server"; 8 + import { AtUri } from "@atproto/syntax"; 8 9 import { createResolver } from "./resolver.ts"; 9 10 import { 10 11 renderNode, ··· 41 42 42 43 app.get("/", (c) => 43 44 c.redirect( 44 - "/at/did:plc:ragtjsm2j2vknwkz3zp4oxrd/app.bsky.actor.profile/self?componentUri=at%3A%2F%2Fdid%3Aplc%3Afpruhuo22xkm5o7ttr2ktxdo%2Fat.inlay.component%2F3mf6ahin36s2e&layout=page" 45 + "/at/did:plc:ragtjsm2j2vknwkz3zp4oxrd/app.bsky.actor.profile/self?componentUri=at%3A%2F%2Fdid%3Aplc%3Afpruhuo22xkm5o7ttr2ktxdo%2Fat.inlay.component%2Fmov.danabra.ProfilePage&layout=page" 45 46 ) 46 47 ); 47 48 ··· 224 225 } 225 226 226 227 const componentRecord = component as { 227 - type: string; 228 228 imports?: string[]; 229 229 body?: unknown; 230 230 view?: unknown[]; 231 231 }; 232 + const nsid = new AtUri(componentUri).rkey; 232 233 const uri = `at://${did}/${collection}/${rkey}`; 233 - const element = $(componentRecord.type, { uri }); 234 + const element = $(nsid, { uri }); 234 235 const context = createContext(componentRecord as any, componentUri); 235 236 setQueryString(new URL(c.req.url).search.slice(1)); 236 237