diff --git a/packages/host/tests/unit/loader-test.ts b/packages/host/tests/unit/loader-test.ts index 6c14669b5d..36decb917d 100644 --- a/packages/host/tests/unit/loader-test.ts +++ b/packages/host/tests/unit/loader-test.ts @@ -4,7 +4,11 @@ import { getService } from '@universal-ember/test-support'; import { module, test } from 'qunit'; -import { baseRealm, Loader } from '@cardstack/runtime-common'; +import { + baseRealm, + Loader, + registerCardReferencePrefix, +} from '@cardstack/runtime-common'; import { testRealmURL, @@ -229,6 +233,27 @@ module('Unit | loader', function (hooks) { assert.strictEqual(myLoader(), loader, 'the loader instance is correct'); }); + // Regression test for CS-10498: after the import-maps change, module + // identifiers can be in registered prefix form (e.g. @cardstack/catalog/...). + // getConsumedModules passed these directly to new URL() which throws + // TypeError: Invalid URL. The fix uses resolveCardReference() first. + test('can determine consumed modules using prefix-form module identifier', async function (assert) { + registerCardReferencePrefix('@test-loader/', testRealmURL); + + // Import the module using its regular URL so it's in the loader cache + await loader.import(`${testRealmURL}f`); + + // Now call getConsumedModules with the prefix-form identifier. + // Without the fix, this throws TypeError: Invalid URL because + // new URL('@test-loader/f') is not a valid URL. + let consumed = await loader.getConsumedModules(`@test-loader/f`); + assert.deepEqual( + consumed, + [`${testRealmURL}b`, `${testRealmURL}c`, `${testRealmURL}g`], + 'consumed modules resolved correctly from prefix-form identifier', + ); + }); + test('identify preserves original module for reexports', function (assert) { let throwIfFetch = new Loader(async () => { throw new Error( diff --git a/packages/runtime-common/loader.ts b/packages/runtime-common/loader.ts index 9724e8b6c2..fab0338d91 100644 --- a/packages/runtime-common/loader.ts +++ b/packages/runtime-common/loader.ts @@ -10,7 +10,10 @@ import { trackRuntimeModuleDependency, type RuntimeDependencyTrackingContext, } from './dependency-tracker'; -import { unresolveCardReference } from './card-reference-resolver'; +import { + unresolveCardReference, + resolveCardReference, +} from './card-reference-resolver'; type FetchingModule = { state: 'fetching'; @@ -158,16 +161,25 @@ export class Loader { consumed: string[] = [], initialIdentifier = moduleIdentifier, ): Promise { - if (consumed.includes(moduleIdentifier)) { + // Normalize to resolved URL href so that prefix-form identifiers + // (e.g. @cardstack/catalog/...) and their resolved URL equivalents + // are treated as the same module for cycle detection and self-exclusion. + let resolvedHref = new URL( + resolveCardReference(moduleIdentifier, undefined), + ).href; + let resolvedInitial = new URL( + resolveCardReference(initialIdentifier, undefined), + ).href; + + if (consumed.includes(resolvedHref)) { return []; } // you can't consume yourself - if (moduleIdentifier !== initialIdentifier) { - consumed.push(moduleIdentifier); + if (resolvedHref !== resolvedInitial) { + consumed.push(resolvedHref); } - let resolvedModuleIdentifier = new URL(moduleIdentifier); - let module = this.getModule(resolvedModuleIdentifier.href); + let module = this.getModule(resolvedHref); if (!module || module.state === 'fetching') { // we haven't yet tried importing the module or we are still in the process of importing the module