Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions packages/base/card-api.gts
Original file line number Diff line number Diff line change
Expand Up @@ -3958,15 +3958,24 @@ function trackRuntimeRelationshipModuleDependencies(
return;
}

trackRuntimeModuleDependency(identity.module, dependencyTrackingContext);

// Loader identities and dependency lists are in canonical RRI form, while
// the dependency tracker keys module nodes by http(s) URL and drops
// anything else — so convert at this boundary via the loader's
// tracking-key form.
let loader = Loader.getLoaderFor(ctor);
trackRuntimeModuleDependency(
loader ? loader.dependencyTrackingKey(identity.module) : identity.module,
dependencyTrackingContext,
);
if (!loader) {
return;
}

for (let dep of loader.getKnownConsumedModules(identity.module)) {
trackRuntimeModuleDependency(dep, dependencyTrackingContext);
trackRuntimeModuleDependency(
loader.dependencyTrackingKey(dep),
dependencyTrackingContext,
);
}
}

Expand Down
23 changes: 14 additions & 9 deletions packages/host/tests/unit/loader-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,10 @@ 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.
// Module identifiers can be in registered prefix form (e.g.
// @cardstack/catalog/...); the loader accepts either spelling as input and
// emits dependency lists in canonical form — the prefix spelling wherever a
// realm-prefix mapping is registered.
test('can determine consumed modules using prefix-form module identifier', async function (assert) {
// Realm-prefix mappings live on the per-app VirtualNetwork — without
// the finally clause this registration leaks into later tests,
Expand All @@ -247,14 +247,19 @@ module('Unit | loader', function (hooks) {
// 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 VN-aware resolution this throws TypeError: Invalid URL
// because new URL('@test-loader/f') is not a valid URL.
// Call getConsumedModules with the prefix-form identifier. This
// requires VN-aware resolution — 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-loader/b`, `@test-loader/c`, `@test-loader/g`],
'consumed modules come out in canonical prefix form',
);
assert.deepEqual(
await loader.getConsumedModules(`${testRealmURL}f`),
consumed,
'URL-form input produces the same canonical output',
);
} finally {
virtualNetwork.removeRealmMapping('@test-loader/');
Expand Down
85 changes: 77 additions & 8 deletions packages/runtime-common/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,12 @@ export class Loader {
});
}

// Returns the transitive consumed modules of `moduleIdentifier` in
// canonical identifier form: the registered realm-prefix (RRI) spelling
// (e.g. `@cardstack/base/card-api`) when the virtual network has a matching
// prefix mapping, otherwise the module URL. Accepts either spelling as
// input. Callers that need a fetchable URL resolve via the virtual network
// at the network boundary.
async getConsumedModules(moduleIdentifier: string): Promise<string[]> {
// Normalize to resolved URL href so that prefix-form identifiers
// (e.g. @cardstack/catalog/...) and their resolved URL equivalents
Expand Down Expand Up @@ -355,7 +361,10 @@ export class Loader {
await walk(moduleIdentifier, initialHref);
// you can't consume yourself
visited.delete(initialHref);
return [...visited];
return this.canonicalizeIdentifiers(
visited,
this.canonicalIdentifier(initialHref),
);
}

static identify(
Expand Down Expand Up @@ -440,17 +449,67 @@ export class Loader {
}
}

// Synchronous sibling of `getConsumedModules` limited to modules already
// known to this loader. Output is in the same canonical identifier form:
// realm-prefix (RRI) spelling where a prefix mapping is registered,
// module URL otherwise.
getKnownConsumedModules(moduleIdentifier: string): string[] {
let resolvedModuleIdentifier = this.resolveImport(moduleIdentifier);
let knownDependencies = this.collectKnownModuleDependencies(
resolvedModuleIdentifier,
);
// Filter rather than delete to avoid mutating the cached Set
return [...knownDependencies].filter(
(dep) => dep !== resolvedModuleIdentifier,
// Copy rather than delete from the cached Set to avoid mutating it
return this.canonicalizeIdentifiers(
knownDependencies,
this.canonicalIdentifier(resolvedModuleIdentifier),
);
}

// Canonical form for module identifiers that flow out of the loader
// (dependency lists, identities): the registered realm-prefix (RRI)
// spelling when the virtual network has a matching mapping, otherwise the
// identifier unchanged. Resolving back to a fetchable URL is the virtual
// network's job at the network boundary.
private canonicalIdentifier(moduleIdentifier: string): string {
return this.virtualNetwork
? this.virtualNetwork.unresolveURL(moduleIdentifier)
: moduleIdentifier;
}

// Map a set of module identifiers to canonical form, deduped (distinct
// spellings of one module — a real URL and its virtual alias — collapse to
// one canonical identifier) and with the module itself excluded: a module
// doesn't consume itself, and the canonical comparison catches self
// references under any spelling.
private canonicalizeIdentifiers(
identifiers: Iterable<string>,
selfCanonical: string,
): string[] {
let seen = new Set<string>();
let result: string[] = [];
for (let identifier of identifiers) {
let canonical = this.canonicalIdentifier(identifier);
if (canonical === selfCanonical || seen.has(canonical)) {
continue;
}
seen.add(canonical);
result.push(canonical);
}
return result;
}

// The runtime dependency tracker keys module nodes by http(s) URL — its
// canonicalURL guard drops realm-prefix identifiers (see
// dependency-tracker.ts) — and this loader collapses each tracked module
// onto its virtual-alias URL when one is registered (see
// canonicalizeTrackingKey). Converts any module identifier, canonical RRI
// form included, into that tracking-key form. Callers recording
// loader-derived module identifiers with the tracker must cross this
// boundary; identifiers stay in canonical RRI form everywhere else.
dependencyTrackingKey(moduleIdentifier: string): string {
return this.canonicalizeTrackingKey(this.resolveImport(moduleIdentifier));
}

private trackKnownModuleDependencies(
rootModuleIdentifier: string,
dependencyTrackingContext?: RuntimeDependencyTrackingContext,
Expand Down Expand Up @@ -782,6 +841,15 @@ export class Loader {
// and RRI forms evaluate as two distinct modules and `instanceof` /
// polymorphic-field identity checks across them diverge. Mirrors the
// real→virtual convention used by `canonicalizeTrackingKey`.
//
// The key deliberately does NOT use the realm-prefix (RRI) form even
// though that's the canonical form for identifiers flowing out of the
// loader: realm-prefix mappings can be registered and removed while a
// loader is live (tests scope temporary prefixes via
// `addRealmMapping`/`removeRealmMapping`), and a mapping-sensitive key
// would orphan already-cached entries whenever a mapping changes —
// re-evaluating the same module under a new key and splitting class
// identities across the old and new copies.
private moduleCacheKey(moduleIdentifier: string): string {
let trimmed = trimModuleIdentifier(moduleIdentifier);
if (this.virtualNetwork) {
Expand Down Expand Up @@ -841,10 +909,11 @@ export class Loader {
module: any,
moduleIdentifier: string,
) {
let trimmed = trimModuleIdentifier(moduleIdentifier);
let moduleId = this.virtualNetwork
? this.virtualNetwork.unresolveURL(trimmed)
: trimmed;
// Identities are recorded in canonical identifier form so that
// `identify()` output matches the form persisted in code refs.
let moduleId = this.canonicalIdentifier(
trimModuleIdentifier(moduleIdentifier),
);
Comment thread
backspace marked this conversation as resolved.
for (let propName of Object.keys(module)) {
let exportedEntity = module[propName];
if (
Expand Down
Loading