diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index 8beb888a223..9163cadf036 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -479,10 +479,10 @@ export type GetSearchResourceFunc = ( export interface CardStore { // The VirtualNetwork that owns this store's realm mappings, used for - // prefix/RRI resolution during (de)serialization. Optional so test doubles - // don't need to implement it; resolution sites degrade — URL-form refs - // still URL-join, prefix-form refs pass through unchanged. - virtualNetwork?: VirtualNetwork; + // prefix/RRI resolution during (de)serialization. Required — every store + // implementation must supply one (production stores, test stubs, the + // FallbackCardStore). + virtualNetwork: VirtualNetwork; getCard(url: string): CardDef | undefined; getFileMeta(url: string): FileDef | undefined; setCard(url: string, instance: CardDef): void; @@ -1450,7 +1450,7 @@ class LinksTo implements Field { if (reference == null || reference === '') { return null; } - let href = resolveRef(store, reference, relativeTo); + let href = resolveRef(store.virtualNetwork, reference, relativeTo); let cachedInstance = isFileDef(this.card) ? store.getFileMeta(href) : store.getCard(href); @@ -2030,7 +2030,11 @@ class LinksToMany implements Field< if (reference == null) { return null; } - let normalizedReference = resolveRef(store, reference, relativeTo); + let normalizedReference = resolveRef( + store.virtualNetwork, + reference, + relativeTo, + ); let cachedInstance = isFileDef(this.card) ? store.getFileMeta(normalizedReference) : store.getCard(normalizedReference); @@ -2480,7 +2484,7 @@ export class BaseDef { return maybeRelativeReference; } return resolveRef( - getStore(value), + getStore(value).virtualNetwork, maybeRelativeReference, value[relativeTo], ); @@ -2507,7 +2511,7 @@ export class BaseDef { let normalizedId = rawValue.reference; if (value[relativeTo]) { normalizedId = resolveRef( - getStore(value), + getStore(value).virtualNetwork, normalizedId, value[relativeTo], ); @@ -3480,7 +3484,11 @@ function lazilyLoadLink( inflightLinkLoads.set(instance, inflightLoads); } let store = getStore(instance); - let reference = resolveRef(store, link, instance.id ?? instance[relativeTo]); + let reference = resolveRef( + store.virtualNetwork, + link, + instance.id ?? instance[relativeTo], + ); let key = `${field.name}/${reference}`; let promise = inflightLoads.get(key); if (promise) { @@ -3552,7 +3560,7 @@ function lazilyLoadLink( continue; } let notLoadedRef = resolveRef( - store, + store.virtualNetwork, item.reference, instance.id ?? instance[relativeTo], ); @@ -3640,7 +3648,7 @@ function lazilyLoadLink( continue; } let notLoadedRef = resolveRef( - store, + store.virtualNetwork, item.reference, instance.id ?? instance[relativeTo], ); @@ -4682,30 +4690,34 @@ function getStore(instance: BaseDef): CardStore { } // The VirtualNetwork associated with an instance's store, for prefix/RRI -// resolution outside this module. Returns undefined when the store can't -// supply one — callers handle that by degrading to URL math or throwing. +// resolution outside this module. Returns undefined when the instance is +// detached (no store, no loader-attached VN) — callers handle that by +// degrading to URL math or throwing. export function virtualNetworkFor( instance: BaseDef, ): VirtualNetwork | undefined { - return getStore(instance).virtualNetwork; + try { + return getStore(instance).virtualNetwork; + } catch { + return undefined; + } } // Resolve a (possibly prefix-form or relative) reference to an absolute URL -// string through the store's VirtualNetwork. When the store doesn't carry -// a VN (test stubs, detached instances), fall back to plain URL math: it +// string through the supplied VirtualNetwork. When the caller can't supply +// one (test stubs, detached instances), fall back to plain URL math: it // covers URL-form refs and relative refs against URL-form bases. Prefix-form // refs and refs against prefix-form bases can't be resolved without a VN — // `new URL()` throws on those, so we return the raw reference unchanged // instead of bubbling the error to callers (e.g. relationship deserialize // uses the returned string as a "did this resolve?" signal). function resolveRef( - store: CardStore | undefined, + virtualNetwork: VirtualNetwork | undefined, reference: string, relativeTo: RealmResourceIdentifier | URL | undefined, ): string { - let vn = store?.virtualNetwork; - if (vn) { - return vn.resolveURL(reference, relativeTo).href; + if (virtualNetwork) { + return virtualNetwork.resolveURL(reference, relativeTo).href; } let base: URL | string | undefined; if (relativeTo instanceof URL) { @@ -4742,8 +4754,14 @@ class FallbackCardStore implements CardStore { #inFlight: Set> = new Set(); #loadGeneration = 0; // mirrors host store tracking to detect new loads - get virtualNetwork(): VirtualNetwork | undefined { - return myLoader().getVirtualNetwork(); + get virtualNetwork(): VirtualNetwork { + let vn = myLoader().getVirtualNetwork(); + if (!vn) { + throw new Error( + `FallbackCardStore.virtualNetwork requires the active Loader to have a VirtualNetwork`, + ); + } + return vn; } getCard(id: string) { @@ -4804,13 +4822,7 @@ class FallbackCardStore implements CardStore { opts?: { dependencyTrackingContext?: RuntimeDependencyTrackingContext }, ) { trackRuntimeInstanceDependency(url, opts?.dependencyTrackingContext); - let vn = this.virtualNetwork; - if (!vn) { - throw new Error( - `CardStore.loadCardDocument requires a Loader with a VirtualNetwork`, - ); - } - let promise = loadCardDocument(fetch, url, vn); + let promise = loadCardDocument(fetch, url, this.virtualNetwork); this.trackLoad(promise); return await promise; } @@ -4820,13 +4832,7 @@ class FallbackCardStore implements CardStore { opts?: { dependencyTrackingContext?: RuntimeDependencyTrackingContext }, ) { trackRuntimeFileDependency(url, opts?.dependencyTrackingContext); - let vn = this.virtualNetwork; - if (!vn) { - throw new Error( - `CardStore.loadFileMetaDocument requires a Loader with a VirtualNetwork`, - ); - } - let promise = loadFileMetaDocument(fetch, url, vn); + let promise = loadFileMetaDocument(fetch, url, this.virtualNetwork); this.trackLoad(promise); return await promise; } diff --git a/packages/base/card-serialization.ts b/packages/base/card-serialization.ts index 8888d61f3ed..529f7bb6cd1 100644 --- a/packages/base/card-serialization.ts +++ b/packages/base/card-serialization.ts @@ -73,10 +73,8 @@ export interface SerializeOpts { maybeRelativeReference?: (possibleReference: string) => string; overrides?: Map; // The VirtualNetwork to consult for prefix/RRI resolution during - // serialization. Optional: when absent, the `maybeRelativeReference` - // closures degrade — prefix-form refs pass through unchanged and only - // URL-form bases support URL math. - virtualNetwork?: VirtualNetwork; + // serialization. Required — every caller must thread a VN. + virtualNetwork: VirtualNetwork; } export interface DeserializeOpts { @@ -212,7 +210,7 @@ export function resourceFrom( export function serializeCard( model: CardDef, - opts?: SerializeOpts, + opts: SerializeOpts, ): LooseSingleCardDocument { let doc = { data: { @@ -222,40 +220,22 @@ export function serializeCard( }; let modelRelativeTo: RealmResourceIdentifier | URL | undefined = model.id ?? model[relativeTo]; + let vn = opts.virtualNetwork; let data = serializeCardResource(model, doc, { ...opts, ...{ maybeRelativeReference(possibleReference: string) { - let vn = opts?.virtualNetwork; // Registered prefix refs (e.g. @cardstack/catalog/foo) are already - // in their canonical portable form — return as-is. Without a VN - // we can't know which prefixes are registered, so the most we can - // do for prefix-form refs is pass them through unchanged. - if (vn ? vn.isRegisteredPrefix(possibleReference) : false) { + // in their canonical portable form — return as-is. + if (vn.isRegisteredPrefix(possibleReference)) { return possibleReference; } - let modelRelativeToForURL: URL | undefined; - if (typeof modelRelativeTo === 'string') { - if (vn) { - modelRelativeToForURL = vn.toURL(modelRelativeTo); - } else if ( - modelRelativeTo.startsWith('http://') || - modelRelativeTo.startsWith('https://') - ) { - modelRelativeToForURL = new URL(modelRelativeTo); - } - } else { - modelRelativeToForURL = modelRelativeTo; - } + let modelRelativeToForURL: URL | undefined = + typeof modelRelativeTo === 'string' + ? vn.toURL(modelRelativeTo) + : modelRelativeTo; let url = maybeURL(possibleReference, modelRelativeToForURL); if (!url) { - if (!vn) { - // Without a VN we can't resolve a prefix-form reference. Pass - // through unchanged so callers that don't need relativization - // (e.g. test adapters serializing same-realm cards by URL) - // don't blow up on portable refs. - return possibleReference; - } throw new Error( `could not determine url from '${possibleReference}' relative to ${modelRelativeTo}`, ); @@ -302,7 +282,12 @@ export function serializeCardResource( usedLinksToFieldsOnly: !opts?.includeUnrenderedFields, }); let overrides = getFieldOverrides(model); - opts = { ...(opts ?? {}), overrides }; + // `serializeCardResource` is reachable from the recursive field-serialize + // symbol path without opts (e.g. callSerializeHook with no opts arg). + // That path doesn't read `opts.virtualNetwork`, so the synthesized + // working opts can lack it; cast through SerializeOpts | undefined to + // satisfy the required-VN type while preserving runtime behavior. + opts = { ...(opts ?? {}), overrides } as SerializeOpts | undefined; let fieldResources = Object.entries(fields) .filter( ([_fieldName, field]) => @@ -330,7 +315,7 @@ export function serializeCardResource( export function serializeFileDef( model: FileDef, - opts?: SerializeOpts, + opts: SerializeOpts, ): LooseSingleFileMetaDocument { let doc = { data: { @@ -340,6 +325,7 @@ export function serializeFileDef( }; let modelRelativeTo: RealmResourceIdentifier | URL | undefined = model.id ?? model[relativeTo]; + let vn = opts.virtualNetwork; let data = serializeCardResource( model, doc, @@ -347,33 +333,17 @@ export function serializeFileDef( ...opts, ...{ maybeRelativeReference(possibleReference: string) { - let vn = opts?.virtualNetwork; // Registered prefix refs (e.g. @cardstack/catalog/foo) are // already in their canonical portable form — return as-is. - // Without a VN we can't know which prefixes are registered, - // so the most we can do for prefix-form refs is pass them - // through unchanged. - if (vn ? vn.isRegisteredPrefix(possibleReference) : false) { + if (vn.isRegisteredPrefix(possibleReference)) { return possibleReference; } - let modelRelativeToForURL: URL | undefined; - if (typeof modelRelativeTo === 'string') { - if (vn) { - modelRelativeToForURL = vn.toURL(modelRelativeTo); - } else if ( - modelRelativeTo.startsWith('http://') || - modelRelativeTo.startsWith('https://') - ) { - modelRelativeToForURL = new URL(modelRelativeTo); - } - } else { - modelRelativeToForURL = modelRelativeTo; - } + let modelRelativeToForURL: URL | undefined = + typeof modelRelativeTo === 'string' + ? vn.toURL(modelRelativeTo) + : modelRelativeTo; let url = maybeURL(possibleReference, modelRelativeToForURL); if (!url) { - if (!vn) { - return possibleReference; - } throw new Error( `could not determine url from '${possibleReference}' relative to ${modelRelativeTo}`, ); diff --git a/packages/host/app/commands/copy-and-edit.ts b/packages/host/app/commands/copy-and-edit.ts index f01d1068738..08209655d6c 100644 --- a/packages/host/app/commands/copy-and-edit.ts +++ b/packages/host/app/commands/copy-and-edit.ts @@ -279,7 +279,13 @@ export default class CopyAndEditCommand extends HostBaseCommand< fieldName: string, ): string | undefined { try { - let serialized = this.#cardAPI?.serializeCard(card); + let vn = this.loaderService.loader.getVirtualNetwork(); + if (!vn) { + return undefined; + } + let serialized = this.#cardAPI?.serializeCard(card, { + virtualNetwork: vn, + }); let relationships = (serialized?.data as any)?.relationships ?? {}; return Object.keys(relationships).find( (key) => key === fieldName || key.endsWith(`.${fieldName}`), diff --git a/packages/host/app/lib/file-def-manager.ts b/packages/host/app/lib/file-def-manager.ts index eb3543d3bba..a370cd2443b 100644 --- a/packages/host/app/lib/file-def-manager.ts +++ b/packages/host/app/lib/file-def-manager.ts @@ -315,7 +315,7 @@ export default class FileDefManagerImpl serialization: LooseSingleCardDocument; }[] = await Promise.all( cards.map(async (card) => { - let opts: CardAPI.SerializeOpts = { + let opts: Omit = { useAbsoluteURL: true, includeComputeds: true, }; diff --git a/packages/host/app/services/card-service.ts b/packages/host/app/services/card-service.ts index b2f375f3dbd..676dc8edbab 100644 --- a/packages/host/app/services/card-service.ts +++ b/packages/host/app/services/card-service.ts @@ -200,7 +200,7 @@ export default class CardService extends Service { async serializeCard( card: CardDef, - opts?: SerializeOpts & { withIncluded?: true }, + opts?: Omit & { withIncluded?: true }, ): Promise { let api = await this.getAPI(); let serialized = api.serializeCard(card, { diff --git a/packages/host/app/services/render-service.ts b/packages/host/app/services/render-service.ts index 32c93bc47fb..9780a3f10e0 100644 --- a/packages/host/app/services/render-service.ts +++ b/packages/host/app/services/render-service.ts @@ -68,6 +68,10 @@ export class CardStoreWithErrors implements CardStore { this.#virtualNetwork = virtualNetwork; } + get virtualNetwork(): VirtualNetwork { + return this.#virtualNetwork; + } + getCard(id: string): CardDef | undefined { id = this.normalizeKey(id); return this.#cards.get(id); diff --git a/packages/host/tests/helpers/adapter.ts b/packages/host/tests/helpers/adapter.ts index 3a788729bea..02b02006e73 100644 --- a/packages/host/tests/helpers/adapter.ts +++ b/packages/host/tests/helpers/adapter.ts @@ -265,9 +265,13 @@ export class TestRealmAdapter implements RealmAdapter { `${baseRealm.url}card-api`, ); if (cardApi.isCard(value)) { - let doc = cardApi.serializeCard(value, { - virtualNetwork: this.#loader.getVirtualNetwork(), - }); + let vn = this.#loader.getVirtualNetwork(); + if (!vn) { + throw new Error( + `TestRealmAdapter.openFile needs the test loader to have a VirtualNetwork to serialize ${path}`, + ); + } + let doc = cardApi.serializeCard(value, { virtualNetwork: vn }); fileRefContent = JSON.stringify(doc); } else { fileRefContent = diff --git a/packages/host/tests/helpers/base-realm.ts b/packages/host/tests/helpers/base-realm.ts index 426361a0591..a85f1387b4c 100644 --- a/packages/host/tests/helpers/base-realm.ts +++ b/packages/host/tests/helpers/base-realm.ts @@ -157,8 +157,40 @@ let MaybeBase64Field: (typeof CardAPIModule)['MaybeBase64Field']; let CSSField: (typeof CardAPIModule)['CSSField']; let createFromSerialized: (typeof CardAPIModule)['createFromSerialized']; let updateFromSerialized: (typeof CardAPIModule)['updateFromSerialized']; -let serializeCard: (typeof CardAPIModule)['serializeCard']; -let serializeFileDef: (typeof CardAPIModule)['serializeFileDef']; +let rawSerializeCard: (typeof CardAPIModule)['serializeCard']; +let rawSerializeFileDef: (typeof CardAPIModule)['serializeFileDef']; + +// Test-side wrappers around the raw card-api serialize functions that +// auto-supply `virtualNetwork` from the active loader. Tests that need a +// non-default VN can still pass one in `opts` and it overrides the +// defaulted value. +function serializeCard( + card: Parameters<(typeof CardAPIModule)['serializeCard']>[0], + opts?: Partial[1]>, +): ReturnType<(typeof CardAPIModule)['serializeCard']> { + let loader = getService('loader-service').loader; + let vn = loader.getVirtualNetwork(); + if (!vn) { + throw new Error( + `base-realm test helper's serializeCard requires the active loader to have a VirtualNetwork`, + ); + } + return rawSerializeCard(card, { virtualNetwork: vn, ...opts }); +} + +function serializeFileDef( + fileDef: Parameters<(typeof CardAPIModule)['serializeFileDef']>[0], + opts?: Partial[1]>, +): ReturnType<(typeof CardAPIModule)['serializeFileDef']> { + let loader = getService('loader-service').loader; + let vn = loader.getVirtualNetwork(); + if (!vn) { + throw new Error( + `base-realm test helper's serializeFileDef requires the active loader to have a VirtualNetwork`, + ); + } + return rawSerializeFileDef(fileDef, { virtualNetwork: vn, ...opts }); +} let isSaved: (typeof CardAPIModule)['isSaved']; let getRelationship: (typeof CardAPIModule)['getRelationship']; let getBrokenLinks: (typeof CardAPIModule)['getBrokenLinks']; @@ -362,8 +394,8 @@ async function initialize() { getFields, createFromSerialized, updateFromSerialized, - serializeCard, - serializeFileDef, + serializeCard: rawSerializeCard, + serializeFileDef: rawSerializeFileDef, isSaved, getRelationship, getBrokenLinks, diff --git a/packages/host/tests/helpers/index.gts b/packages/host/tests/helpers/index.gts index 3baa0ce5f56..79bbd8aa405 100644 --- a/packages/host/tests/helpers/index.gts +++ b/packages/host/tests/helpers/index.gts @@ -1468,7 +1468,13 @@ export async function saveCard( realmURL?: RealmIdentifier, ) { let api = await loader.import(`${baseRealm.url}card-api`); - let doc = api.serializeCard(instance); + let vn = loader.getVirtualNetwork(); + if (!vn) { + throw new Error( + `setCardAsSavedWithId test helper needs the loader to have a VirtualNetwork`, + ); + } + let doc = api.serializeCard(instance, { virtualNetwork: vn }); doc.data.id = id; if (realmURL) { doc.data.meta = { diff --git a/packages/host/tests/helpers/indexer.ts b/packages/host/tests/helpers/indexer.ts index 560f6cd5fa4..23589227231 100644 --- a/packages/host/tests/helpers/indexer.ts +++ b/packages/host/tests/helpers/indexer.ts @@ -77,7 +77,14 @@ export async function getTypes(instance: CardDef): Promise { export async function serializeCard(card: CardDef): Promise { let api = await apiFor(card); - return api.serializeCard(card).data as CardResource; + let loader = loaderFor(card); + let virtualNetwork = loader.getVirtualNetwork(); + if (!virtualNetwork) { + throw new Error( + `serializeCard test helper requires a Loader with an attached VirtualNetwork`, + ); + } + return api.serializeCard(card, { virtualNetwork }).data as CardResource; } // we can relax the resource here since we will be asserting an ID when we diff --git a/packages/host/tests/integration/field-configuration-test.gts b/packages/host/tests/integration/field-configuration-test.gts index 03649b4baa3..a1c61e5fc16 100644 --- a/packages/host/tests/integration/field-configuration-test.gts +++ b/packages/host/tests/integration/field-configuration-test.gts @@ -4,7 +4,7 @@ import { settled } from '@ember/test-helpers'; import { getService } from '@universal-ember/test-support'; import { module, test } from 'qunit'; -import { baseRealm, Deferred } from '@cardstack/runtime-common'; +import { baseRealm, Deferred, VirtualNetwork } from '@cardstack/runtime-common'; import type { SingleCardDocument, SingleFileMetaDocument, @@ -44,6 +44,7 @@ import { setupRenderingTest } from '../helpers/setup'; let loader: Loader; class DeferredLinkStore implements CardStore { + virtualNetwork: VirtualNetwork = new VirtualNetwork(); private cardInstances = new Map(); private fileMetaInstances = new Map(); private readyCardDocs = new Map(); diff --git a/packages/host/tests/unit/code-ref-test.ts b/packages/host/tests/unit/code-ref-test.ts index b8376da0b22..926a31bd8d4 100644 --- a/packages/host/tests/unit/code-ref-test.ts +++ b/packages/host/tests/unit/code-ref-test.ts @@ -6,6 +6,7 @@ import { module, test } from 'qunit'; import type { Loader, LooseCardResource } from '@cardstack/runtime-common'; import { + VirtualNetwork, baseRealm, loadCardDef, rri, @@ -194,6 +195,7 @@ module('code-ref', function (hooks) { let doc = { data: { id: base.href } }; let serialized = CodeRefSerializer.serialize(ref, doc, undefined, { relativeTo: base, + virtualNetwork: new VirtualNetwork(), }) as any; assert.strictEqual( serialized.module, diff --git a/packages/runtime-common/serializers/code-ref.ts b/packages/runtime-common/serializers/code-ref.ts index 3263df1e9d9..7d45f79f2a1 100644 --- a/packages/runtime-common/serializers/code-ref.ts +++ b/packages/runtime-common/serializers/code-ref.ts @@ -1,6 +1,7 @@ import type { BaseDefConstructor, BaseInstanceType, + CardStore, } from 'https://cardstack.com/base/card-api'; import { type ResolvedCodeRef, @@ -26,7 +27,7 @@ export function serialize( codeRef: ResolvedCodeRef | {}, doc: any, _visited?: Set, - opts?: SerializeOpts & { + opts?: Omit & { relativeTo?: RealmResourceIdentifier | URL; trimExecutableExtension?: true; maybeRelativeReference?: (reference: string) => string; @@ -34,14 +35,37 @@ export function serialize( virtualNetwork?: VirtualNetwork; }, ): ResolvedCodeRef | {} { - let vn = opts?.virtualNetwork; + // The recursive serialize path through a non-primitive `Contains` field + // intentionally isolates the inner card's serialization from the outer + // card's opts (see `Contains.serialize` in card-api.gts), so opts can + // arrive here as `undefined` or as a synthesized `{ overrides }` object + // with no `virtualNetwork`. URL-form refs can still be resolved with + // plain URL math; prefix-form refs need a VN and are left alone. + if (!opts) { + return { ...codeRef }; + } + let vn = opts.virtualNetwork; let baseURL: URL | undefined; - if (opts?.relativeTo instanceof URL) { + if (opts.relativeTo instanceof URL) { baseURL = opts.relativeTo; - } else if (typeof opts?.relativeTo === 'string') { - baseURL = vn ? vn.toURL(opts.relativeTo) : undefined; + } else if (typeof opts.relativeTo === 'string') { + if (vn) { + baseURL = vn.toURL(opts.relativeTo); + } else if ( + opts.relativeTo.startsWith('http://') || + opts.relativeTo.startsWith('https://') + ) { + baseURL = new URL(opts.relativeTo); + } } else if (doc?.data?.id && typeof doc.data.id === 'string') { - baseURL = vn ? vn.toURL(doc.data.id) : undefined; + if (vn) { + baseURL = vn.toURL(doc.data.id); + } else if ( + doc.data.id.startsWith('http://') || + doc.data.id.startsWith('https://') + ) { + baseURL = new URL(doc.data.id); + } } return { ...codeRef, @@ -65,17 +89,29 @@ export async function deserializeAbsolute( this: T, codeRef: ResolvedCodeRef | {}, relativeTo: RealmResourceIdentifier | URL | undefined, + _doc?: unknown, + store?: CardStore, ): Promise> { + if (!store) { + // Reached only by direct test callers that bypass the framework + // protocol; the framework's field-deserialize path always supplies + // a store. Without a VN we can't resolve prefix-form refs or + // round-trip URL-form refs through registered mappings, so leave + // the codeRef untouched. + return { ...codeRef } as BaseInstanceType; + } return { ...codeRef, - ...codeRefAdjustments(codeRef, relativeTo), + ...codeRefAdjustments(codeRef, relativeTo, { + virtualNetwork: store.virtualNetwork, + }), } as BaseInstanceType; } function codeRefAdjustments( codeRef: any, - relativeTo?: RealmResourceIdentifier | URL, - opts?: SerializeOpts & { + relativeTo: RealmResourceIdentifier | URL | undefined, + opts?: Omit & { trimExecutableExtension?: true; maybeRelativeReference?: (reference: string) => string; allowRelative?: true; @@ -88,13 +124,11 @@ function codeRefAdjustments( if (!isResolvedCodeRef(codeRef)) { return {}; } - // The `deserializeAbsolute` field-deserialize path reaches this without - // opts (no VN, no `allowRelative`, no `maybeRelativeReference`). For - // URL-like refs we can still do a plain URL-join against `relativeTo` - // and apply `trimExecutableExtension`. Bare specifiers (e.g. - // `@cardstack/boxel-host/…`) throw — `resolve` is wrapped in try/catch - // below, so the original ref stays intact for the loader's importMap - // shim. + // opts may arrive without a VN — the recursive non-primitive-Contains + // serialize path isolates inner cards from the outer card's opts, and + // `deserializeAbsolute` may also be called without a store. URL-like + // refs still resolve through plain URL math; bare specifiers fall + // through to the loader's importMap shim via the surrounding try/catch. let vn = opts?.virtualNetwork; let resolve = (ref: string) => { if (vn) {