From 7f4fcf6135c82ceb813e4e8a2fa412d1ae3de059 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 10 Sep 2026 10:37:02 -0400 Subject: [PATCH 1/2] Resolve a code ref's module without a virtual network MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `loadCardDef` pulled the network off the loader and refused to run without one, but used it for a single call: `resolveModuleHref`, which wanted only `isRegisteredPrefix` and `resolveURL`. A code ref's module is canonical RRI, so resolving it is path math — `@scope/name/...` and anything carrying a URL scheme are already absolute, and a relative reference joins against `relativeTo`. That is what `resolveRRIReference` does, with no mappings involved, and `codeRefWithAbsoluteIdentifier` already took that path when handed no network. No call site changes. All 67 `loadCardDef` callers pass a loader rather than a network, so this is confined to code-ref.ts. The bare-specifier rejection survives without a registry, because a bare specifier is recognisable by shape: neither URL-like — relative, rooted, or schemed — nor scoped. What it stops rejecting is a scoped reference whose prefix this process has not registered, deliberately: such a reference is absolute and cross-realm by construction, which is the rule the read path already applies, and an unresolvable one now fails at fetch naming the module the caller wrote rather than at a prefix check. `codeRefWithAbsoluteIdentifier` keeps its optional network parameter, now accepted and ignored, so its ~50 call sites stay untouched. Removing it cascades into three more signatures and belongs with the sweep that takes the network off the Loader's remaining consumers. --- .../tests/resolve-module-href-test.ts | 62 +++++++++++++++++++ packages/runtime-common/code-ref.ts | 61 ++++++++---------- 2 files changed, 87 insertions(+), 36 deletions(-) create mode 100644 packages/realm-server/tests/resolve-module-href-test.ts diff --git a/packages/realm-server/tests/resolve-module-href-test.ts b/packages/realm-server/tests/resolve-module-href-test.ts new file mode 100644 index 00000000000..b8d14fb3c8b --- /dev/null +++ b/packages/realm-server/tests/resolve-module-href-test.ts @@ -0,0 +1,62 @@ +import QUnit from 'qunit'; +const { module, test } = QUnit; +import { basename } from 'path'; +import { resolveModuleHref } from '@cardstack/runtime-common/code-ref'; + +const relativeTo = new URL('http://test/realm/consumer.gts'); + +module(basename(import.meta.filename), function () { + module('resolveModuleHref', function () { + test('passes a scoped reference through unchanged', function (assert) { + assert.strictEqual( + resolveModuleHref('@cardstack/base/card-api', relativeTo), + '@cardstack/base/card-api', + ); + }); + + test('passes a scoped reference through even with no registered prefix', function (assert) { + // A scoped reference is absolute and cross-realm by construction, so it + // resolves without asking whether this process knows the realm. An + // unresolvable one fails at fetch, naming what the caller wrote. + assert.strictEqual( + resolveModuleHref('@nobody/knows-this/thing', relativeTo), + '@nobody/knows-this/thing', + ); + }); + + test('joins a relative reference against the consumer', function (assert) { + assert.strictEqual( + resolveModuleHref('./person', relativeTo), + 'http://test/realm/person', + ); + assert.strictEqual( + resolveModuleHref('../shared/person', relativeTo), + 'http://test/shared/person', + ); + }); + + test('passes an absolute URL through unchanged', function (assert) { + assert.strictEqual( + resolveModuleHref('http://elsewhere/other/person', relativeTo), + 'http://elsewhere/other/person', + ); + }); + + test('rejects a bare package specifier', function (assert) { + // Recognised by shape rather than by a registry lookup: neither + // URL-like nor scoped. Without this a bare name would silently join + // against the consumer and fetch a URL nobody wrote. + assert.throws( + () => resolveModuleHref('lodash', relativeTo), + /bare package specifier "lodash"/, + ); + }); + + test('needs no relativeTo for an already-absolute reference', function (assert) { + assert.strictEqual( + resolveModuleHref('@cardstack/base/string', undefined), + '@cardstack/base/string', + ); + }); + }); +}); diff --git a/packages/runtime-common/code-ref.ts b/packages/runtime-common/code-ref.ts index 78a8ff1e665..174ad736e77 100644 --- a/packages/runtime-common/code-ref.ts +++ b/packages/runtime-common/code-ref.ts @@ -173,36 +173,44 @@ export function isSpecCard(def: any) { // path. Throw on that exact case so callers' surrounding try/catch // leaves the original ref alone for the loader's importMap shim to // resolve. (URL-like refs and registered prefixes resolve normally.) +// A code ref's module is canonical RRI, so resolving it is path math rather +// than a naming question: `@scope/name/...` and anything carrying a URL scheme +// are already absolute, and a relative reference joins against `relativeTo`. +// +// The rejection below no longer consults a prefix registry, because a bare +// specifier is recognisable by shape: it is neither URL-like (relative, rooted, +// or schemed) nor scoped. What it stops rejecting is a scoped reference whose +// prefix this process has not registered — deliberately, since such a reference +// is absolute and cross-realm by construction, which is the same rule the read +// path applies. An unresolvable one fails at fetch, naming the module the +// caller actually wrote. export function resolveModuleHref( module: string, relativeTo: RealmResourceIdentifier | URL | undefined, - virtualNetwork: VirtualNetwork, ): string { - if (!isUrlLike(module) && !virtualNetwork.isRegisteredPrefix(module)) { + if (!isUrlLike(module) && !module.startsWith('@')) { throw new Error( - `Cannot resolve bare package specifier "${module}" — no matching prefix mapping registered`, + `Cannot resolve bare package specifier "${module}" — a module reference must be scoped, URL-like, or relative`, ); } - return virtualNetwork.resolveURL(module, relativeTo).href; + return resolveRRIReference(module, relativeTo); } export function codeRefWithAbsoluteIdentifier( ref: CodeRef, relativeTo: RealmResourceIdentifier | URL | undefined, opts: { trimExecutableExtension?: true } | undefined, - // Optional: when a VirtualNetwork is supplied the module is resolved through - // it (legacy callers). When omitted, the module is resolved in RRI space via - // `resolveRRIReference` — no VirtualNetwork — since code refs are canonical - // RRI; relative modules join against `relativeTo`, absolute/prefix modules - // pass through unchanged. - virtualNetwork?: VirtualNetwork, + // Accepted and ignored. Resolution is the same either way now: a code ref's + // module is canonical RRI, so `resolveModuleHref` does path math and consults + // no mappings. The parameter stays until the wider sweep that removes the + // network from the Loader's consumers, so ~50 call sites need not change here. + _virtualNetwork?: VirtualNetwork, ): CodeRef { if (!('type' in ref)) { try { - let moduleHref = ( - virtualNetwork - ? resolveModuleHref(ref.module, relativeTo, virtualNetwork) - : resolveRRIReference(ref.module, relativeTo) + let moduleHref = resolveModuleHref( + ref.module, + relativeTo, ) as RealmResourceIdentifier; if (opts?.trimExecutableExtension) { moduleHref = trimExecutableExtension(moduleHref); @@ -214,12 +222,7 @@ export function codeRefWithAbsoluteIdentifier( } return { ...ref, - card: codeRefWithAbsoluteIdentifier( - ref.card, - relativeTo, - undefined, - virtualNetwork, - ), + card: codeRefWithAbsoluteIdentifier(ref.card, relativeTo, undefined), }; } @@ -238,18 +241,8 @@ export async function loadCardDef( ): Promise { let maybeCard: unknown; let loader = opts.loader; - let virtualNetwork = loader.getVirtualNetwork(); - if (!virtualNetwork) { - throw new Error( - `loadCardDef requires a Loader configured with a VirtualNetwork`, - ); - } if (!('type' in ref)) { - let resolvedModuleURL = resolveModuleHref( - ref.module, - opts?.relativeTo, - virtualNetwork, - ); + let resolvedModuleURL = resolveModuleHref(ref.module, opts?.relativeTo); let module = await loader.import>( resolvedModuleURL, opts.dependencyTrackingContext, @@ -270,11 +263,7 @@ export async function loadCardDef( return maybeCard; } - let resolvedFromRef = resolveModuleHref( - moduleFrom(ref), - opts?.relativeTo, - virtualNetwork, - ); + let resolvedFromRef = resolveModuleHref(moduleFrom(ref), opts?.relativeTo); let err = new CardError( `Cannot find card ${humanReadable(ref)}. Make sure ${resolvedFromRef} exports ${exportFrom(ref)}`, { From 0a86a2eabf566fe8f49c1ed83100d6c44f45143b Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 10 Sep 2026 11:37:03 -0400 Subject: [PATCH 2/2] Resolve a code ref's module by the RRI contract, with no guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bare-specifier rejection this added was stricter than the contract around it and rejected identifiers the resolver would have handled. `isRelativePath` draws the line: `@scope/name/...` is absolute, anything a URL parser accepts is absolute, and everything else is a relative reference. `resolveRRIReference` draws it identically. So `data:` and `blob:` modules were being rejected before the resolver could preserve them, and a same-realm module written without `./` — `garden-design`, as the checked-in Garden tab refs have it — was rejected too, after which `codeRefWithAbsoluteIdentifier`'s catch returned the ref unresolved and callers went on to look up the wrong identifier. Silent, and in a path that had no guard before this branch. The premise behind that guard was wrong. A bare specifier cannot be recognised by shape: `garden-design` naming a module in this realm and `date-fns` naming one of the ~30 packages the loader shims are indistinguishable, and only the prefix registry ever told them apart. Losing the registry loses that, so the resolution rule is now simply the contract's. Relative wins for a bare name. A code ref names a card definition, so the checked-in refs of that shape are same-realm modules while the shimmed packages are imported by module source rather than referenced as code refs. A scoped specifier that matches no realm prefix — `@cardstack/boxel-host/commands/foo`, the case the removed comment named — still passes through unchanged, which is what the loader's import map needs. What it costs is a bare shimmed specifier used as a code ref, which would resolve into the realm and fail at fetch. Tests follow the contract rather than the guard: a bare name joins against the consumer, and non-http schemes pass through. --- .../tests/resolve-module-href-test.ts | 28 +++++++++--- packages/runtime-common/code-ref.ts | 43 ++++++++----------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/packages/realm-server/tests/resolve-module-href-test.ts b/packages/realm-server/tests/resolve-module-href-test.ts index b8d14fb3c8b..3f1f7cad06b 100644 --- a/packages/realm-server/tests/resolve-module-href-test.ts +++ b/packages/realm-server/tests/resolve-module-href-test.ts @@ -42,16 +42,30 @@ module(basename(import.meta.filename), function () { ); }); - test('rejects a bare package specifier', function (assert) { - // Recognised by shape rather than by a registry lookup: neither - // URL-like nor scoped. Without this a bare name would silently join - // against the consumer and fetch a URL nobody wrote. - assert.throws( - () => resolveModuleHref('lodash', relativeTo), - /bare package specifier "lodash"/, + // There is no bare-specifier category to reject: `isRelativePath` treats + // any non-scoped, non-URL identifier as relative, so a bare name is + // indistinguishable in shape from a module in this realm. One that names + // nothing resolves here and fails at fetch. + test('joins a bare name against the consumer, like any relative reference', function (assert) { + assert.strictEqual( + resolveModuleHref('garden-design', relativeTo), + 'http://test/realm/garden-design', + ); + assert.strictEqual( + resolveModuleHref('lodash', relativeTo), + 'http://test/realm/lodash', ); }); + test('passes a non-http absolute scheme through unchanged', function (assert) { + for (let ref of [ + 'data:text/javascript,export default 1', + 'blob:http://test/8f2c', + ]) { + assert.strictEqual(resolveModuleHref(ref, relativeTo), ref); + } + }); + test('needs no relativeTo for an already-absolute reference', function (assert) { assert.strictEqual( resolveModuleHref('@cardstack/base/string', undefined), diff --git a/packages/runtime-common/code-ref.ts b/packages/runtime-common/code-ref.ts index 174ad736e77..c4987287488 100644 --- a/packages/runtime-common/code-ref.ts +++ b/packages/runtime-common/code-ref.ts @@ -21,11 +21,7 @@ import { CardError } from './error.ts'; import type { VirtualNetwork } from './virtual-network.ts'; import type { RealmResourceIdentifier } from './realm-identifiers.ts'; import type { LooseCardResource, FileMetaResource } from './index.ts'; -import { - isUrlLike, - trimExecutableExtension, - resolveRRIReference, -} from './index.ts'; +import { trimExecutableExtension, resolveRRIReference } from './index.ts'; import type { RuntimeDependencyTrackingContext } from './dependency-tracker.ts'; export type ResolvedCodeRef = { @@ -167,32 +163,29 @@ export function isSpecCard(def: any) { return isBaseDef(def) && isSpec in def; } -// Loader-only bare specifiers (e.g. `@cardstack/boxel-host/commands/foo`) -// have no registered realm-prefix mapping — `VirtualNetwork.resolveURL` -// would URL-join them to `relativeTo` and produce a nonexistent realm -// path. Throw on that exact case so callers' surrounding try/catch -// leaves the original ref alone for the loader's importMap shim to -// resolve. (URL-like refs and registered prefixes resolve normally.) // A code ref's module is canonical RRI, so resolving it is path math rather -// than a naming question: `@scope/name/...` and anything carrying a URL scheme -// are already absolute, and a relative reference joins against `relativeTo`. +// than a naming question: `@scope/name/...` and anything a URL parser accepts +// are already absolute, and everything else is a relative reference that joins +// against `relativeTo`. That is the line `isRelativePath` draws, and +// `resolveRRIReference` draws it the same way. +// +// A scoped specifier the loader shims rather than serves — say +// `@cardstack/boxel-host/commands/foo`, which matches no realm prefix — passes +// through unchanged, which is what the loader's import map needs in order to +// resolve it. // -// The rejection below no longer consults a prefix registry, because a bare -// specifier is recognisable by shape: it is neither URL-like (relative, rooted, -// or schemed) nor scoped. What it stops rejecting is a scoped reference whose -// prefix this process has not registered — deliberately, since such a reference -// is absolute and cross-realm by construction, which is the same rule the read -// path applies. An unresolvable one fails at fetch, naming the module the -// caller actually wrote. +// A bare specifier cannot be given that treatment, and no rule here can fix +// that: `garden-design` naming a module in this realm and `date-fns` naming a +// shimmed package are the same shape, and only a prefix registry told them +// apart. Relative wins, because a code ref names a card definition — the +// checked-in refs that look like this are same-realm modules, and the shimmed +// packages are imported by module source rather than referenced as code refs. +// The cost is that a bare shimmed specifier used *as* a code ref would resolve +// into the realm and fail at fetch instead of reaching the import map. export function resolveModuleHref( module: string, relativeTo: RealmResourceIdentifier | URL | undefined, ): string { - if (!isUrlLike(module) && !module.startsWith('@')) { - throw new Error( - `Cannot resolve bare package specifier "${module}" — a module reference must be scoped, URL-like, or relative`, - ); - } return resolveRRIReference(module, relativeTo); }