From b308f94055b304b305b5f8daf9bfb09cb2339c01 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 3 Jul 2026 16:31:31 -0500 Subject: [PATCH] refactor: replace virtualNetworkFor export with resolveInstanceURL (CS-11778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Card definitions (spec.gts, experiments-realm/asset.gts) reached the store's VirtualNetwork via the `virtualNetworkFor` export purely to turn an RRI/relative reference into a real fetchable URL for a boundary that can't consume RRI (an ``, the AI source-file reader's `new URL(...)`). Replace that export with a narrow `resolveInstanceURL(instance, reference)` that returns a URL, not the VirtualNetwork itself — closing the raw-VN leak to card definitions. It resolves through the active Loader's VirtualNetwork (the network boundary). Scope note: the CardStore.virtualNetwork interface member stays for now. Beyond the two loadCardDocument fetch boundaries it is still read by query-field-support (resolveQueryAndRealm) and host search (isFileDefCodeRef), which need the raw VirtualNetwork for operations resolveInstanceURL doesn't cover. Removing the interface member is deferred to a follow-up that migrates those consumers. Co-Authored-By: Claude Opus 4.8 --- packages/base/card-api.gts | 29 +++++++++++++++------ packages/base/spec.gts | 34 ++++++++++-------------- packages/experiments-realm/asset.gts | 39 ++++++++++------------------ 3 files changed, 47 insertions(+), 55 deletions(-) diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index 2371e809fca..811efbcf59b 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -4787,18 +4787,31 @@ function getStore(instance: BaseDef): CardStore { return stores.get(instance as BaseDef) ?? new FallbackCardStore(); } -// The VirtualNetwork associated with an instance's store, for prefix/RRI -// 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 { +// Resolve a (possibly relative or RRI) reference to a real, fetchable URL, +// relative to the instance's own location. Card definitions that must hand a +// real URL to a boundary that can't consume canonical RRI (an ``, the +// AI source-file reader's `new URL(...)`) use this rather than reaching for the +// VirtualNetwork object directly — they get back a URL, not the network itself. +// Resolves through the active Loader's VirtualNetwork (the network boundary); +// returns undefined when none is available (detached / static-parse contexts) +// so callers can degrade to URL math. +export function resolveInstanceURL( + instance: CardDef, + reference: string, +): URL | undefined { + let virtualNetwork: VirtualNetwork | undefined; try { - return getStore(instance).virtualNetwork; + virtualNetwork = myLoader().getVirtualNetwork(); } catch { return undefined; } + if (!virtualNetwork) { + return undefined; + } + return virtualNetwork.resolveURL( + reference, + instance.id ?? instance[relativeTo], + ); } // Resolve a (possibly relative) reference to its absolute canonical RRI, diff --git a/packages/base/spec.gts b/packages/base/spec.gts index c890852b96f..3e1d8dafb89 100644 --- a/packages/base/spec.gts +++ b/packages/base/spec.gts @@ -3,12 +3,11 @@ import { field, Component, CardDef, - relativeTo, linksToMany, FieldDef, containsMany, getCardMeta, - virtualNetworkFor, + resolveInstanceURL, type CardOrFieldTypeIcon, BaseDef, type CardContext, @@ -81,23 +80,20 @@ class PopulateFieldSpecExampleCommand extends PopulateWithSampleDataCommand { if (!codeRef) { return []; } - // The attached-file identifiers are read as fetchable source URLs (the AI - // source-file reader does `new URL(...)`), so this must resolve to a real - // URL — keep the VirtualNetwork here (a scoped RRI can't be fetched). - let vn = virtualNetworkFor(card); - if (!vn) { - return []; - } + // The attached-file identifier is read as a fetchable source URL (the AI + // source-file reader does `new URL(...)`), so the card's type module must + // resolve to a real URL — a scoped RRI can't be fetched. codeRef = codeRefWithAbsoluteIdentifier( codeRef, - vn.toURL(card.id!), + card.id, undefined, - vn, )! as ResolvedCodeRef; - let cardOrFieldModuleURL = codeRef.module - ? ensureExtension(codeRef.module, { default: '.gts' }) + let moduleURL = codeRef.module + ? resolveInstanceURL(card, codeRef.module) : undefined; - return cardOrFieldModuleURL ? [cardOrFieldModuleURL] : []; + return moduleURL + ? [ensureExtension(moduleURL.href, { default: '.gts' })] + : []; } } @@ -941,13 +937,9 @@ export class Spec extends CardDef { } // `moduleHref` is consumed as a fetchable / absolute URL (source reader's // `new URL(...)`, and URL-form comparisons in the code submode), so it - // must resolve to a real URL — keep the VirtualNetwork here (RRI space - // would leave a scoped prefix that those readers can't use). - let vn = virtualNetworkFor(this); - if (!vn) { - return undefined; - } - return vn.resolveURL(this.ref.module, this.id ?? this[relativeTo]).href; + // must resolve to a real URL — RRI space would leave a scoped prefix that + // those readers can't use. + return resolveInstanceURL(this, this.ref.module)?.href; }, }); @field linkedExamples = linksToMany(CardDef); diff --git a/packages/experiments-realm/asset.gts b/packages/experiments-realm/asset.gts index e44521a8c87..ac0650c16d4 100644 --- a/packages/experiments-realm/asset.gts +++ b/packages/experiments-realm/asset.gts @@ -4,8 +4,7 @@ import { CardDef, FieldDef, Component, - relativeTo, - virtualNetworkFor, + resolveInstanceURL, } from 'https://cardstack.com/base/card-api'; import StringField from 'https://cardstack.com/base/string'; import CurrencyIcon from '@cardstack/boxel-icons/currency'; @@ -21,19 +20,13 @@ export class Asset extends CardDef { if (!this.logoURL) { return null; } - let rel = this[relativeTo] || this.id; - // The instance may have no store-attached VirtualNetwork (detached - // / static-parse contexts), and `rel` may be a prefix-form RRI - // (e.g. `@cardstack/…/Asset/foo`) that `new URL()` can't parse on - // its own. If we can't resolve a base, return `logoURL` raw so the - // binding still has a string to render rather than - // letting the compute throw. + // `logoURL` may be relative to the instance, which lives at a prefix-form + // RRI (e.g. `@cardstack/…/Asset/foo`) that `new URL()` can't parse. + // Resolve it against the instance's real URL for the ; if no + // VirtualNetwork is available (detached / static-parse), fall back to the + // raw value. try { - let base = - typeof rel === 'string' - ? (virtualNetworkFor(this)?.toURL(rel) ?? new URL(rel)) - : rel; - return new URL(this.logoURL, base).href; + return resolveInstanceURL(this, this.logoURL)?.href ?? this.logoURL; } catch { return this.logoURL; } @@ -105,19 +98,13 @@ class AssetField extends FieldDef { if (!this.logoURL) { return null; } - let rel = this[relativeTo] || this.id; - // The instance may have no store-attached VirtualNetwork (detached - // / static-parse contexts), and `rel` may be a prefix-form RRI - // (e.g. `@cardstack/…/Asset/foo`) that `new URL()` can't parse on - // its own. If we can't resolve a base, return `logoURL` raw so the - // binding still has a string to render rather than - // letting the compute throw. + // `logoURL` may be relative to the instance, which lives at a prefix-form + // RRI (e.g. `@cardstack/…/Asset/foo`) that `new URL()` can't parse. + // Resolve it against the instance's real URL for the ; if no + // VirtualNetwork is available (detached / static-parse), fall back to the + // raw value. try { - let base = - typeof rel === 'string' - ? (virtualNetworkFor(this)?.toURL(rel) ?? new URL(rel)) - : rel; - return new URL(this.logoURL, base).href; + return resolveInstanceURL(this, this.logoURL)?.href ?? this.logoURL; } catch { return this.logoURL; }