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..3f1f7cad06b --- /dev/null +++ b/packages/realm-server/tests/resolve-module-href-test.ts @@ -0,0 +1,76 @@ +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', + ); + }); + + // 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), + '@cardstack/base/string', + ); + }); + }); +}); diff --git a/packages/runtime-common/code-ref.ts b/packages/runtime-common/code-ref.ts index 78a8ff1e665..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,42 +163,47 @@ 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 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. +// +// 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, - virtualNetwork: VirtualNetwork, ): string { - if (!isUrlLike(module) && !virtualNetwork.isRegisteredPrefix(module)) { - throw new Error( - `Cannot resolve bare package specifier "${module}" — no matching prefix mapping registered`, - ); - } - 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 +215,7 @@ export function codeRefWithAbsoluteIdentifier( } return { ...ref, - card: codeRefWithAbsoluteIdentifier( - ref.card, - relativeTo, - undefined, - virtualNetwork, - ), + card: codeRefWithAbsoluteIdentifier(ref.card, relativeTo, undefined), }; } @@ -238,18 +234,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 +256,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)}`, {