From 8cb0e3ab36e08e2d05dbdc0b93f6616f4494eec9 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 24 Mar 2026 16:41:31 -0400 Subject: [PATCH 1/3] Fix Invalid URL in Loader.getConsumedModules for prefix-form module IDs Same root cause as the relativizeResource fix: after the import maps change, module identifiers can be in registered prefix form (e.g. @cardstack/catalog/...). Loader.getConsumedModules passed these directly to new URL() which throws TypeError: Invalid URL. Use resolveCardReference() to resolve the prefix to a real URL first. This fixes the client-side error seen when prerendering catalog cards: "Failed to render live search result: TypeError: Failed to construct 'URL': Invalid URL at Loader.getConsumedModules" Fixes CS-10498 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/runtime-common/loader.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/runtime-common/loader.ts b/packages/runtime-common/loader.ts index 9724e8b6c2..19effb1d37 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'; @@ -166,7 +169,9 @@ export class Loader { consumed.push(moduleIdentifier); } - let resolvedModuleIdentifier = new URL(moduleIdentifier); + let resolvedModuleIdentifier = new URL( + resolveCardReference(moduleIdentifier, undefined), + ); let module = this.getModule(resolvedModuleIdentifier.href); if (!module || module.state === 'fetching') { From 7eaa5f2afec3d3188319ec0ed5469201e9ee8175 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 24 Mar 2026 17:00:35 -0400 Subject: [PATCH 2/3] Add regression test for prefix-form module ID in Loader.getConsumedModules Verifies that getConsumedModules works when called with a registered prefix-form module identifier (e.g. @cardstack/catalog/...). Without the fix, new URL('@test-loader/f') throws TypeError: Invalid URL. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/host/tests/unit/loader-test.ts | 27 ++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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( From 099cf4d2855da09390aef7410ef762e032a9e190 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 24 Mar 2026 17:14:29 -0400 Subject: [PATCH 3/3] Normalize module identifiers in getConsumedModules for consistent cycle detection Resolve both moduleIdentifier and initialIdentifier to their canonical URL href before cycle detection and self-exclusion checks. This ensures prefix-form identifiers (e.g. @cardstack/catalog/...) and their resolved URL equivalents are treated as the same module, preventing incorrect self-inclusion or missed de-duplication in dependency cycles. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/runtime-common/loader.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/runtime-common/loader.ts b/packages/runtime-common/loader.ts index 19effb1d37..fab0338d91 100644 --- a/packages/runtime-common/loader.ts +++ b/packages/runtime-common/loader.ts @@ -161,18 +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( - resolveCardReference(moduleIdentifier, undefined), - ); - 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