diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index 5ddca01807..8e5f94786c 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -3958,24 +3958,18 @@ function trackRuntimeRelationshipModuleDependencies( return; } - // 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. + // `identity.module` and `getKnownConsumedModules` deps are already in the + // loader's canonical identifier form — an RRI prefix for a mapped realm, the + // resolved real URL for a plain realm, never a virtual alias — which is + // exactly the form the tracker keys nodes by. So track them directly. + trackRuntimeModuleDependency(identity.module, dependencyTrackingContext); 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( - loader.dependencyTrackingKey(dep), - dependencyTrackingContext, - ); + trackRuntimeModuleDependency(dep, dependencyTrackingContext); } } diff --git a/packages/realm-server/tests/runtime-dependency-tracker-test.ts b/packages/realm-server/tests/runtime-dependency-tracker-test.ts index 8975ed0712..486f2a0b3c 100644 --- a/packages/realm-server/tests/runtime-dependency-tracker-test.ts +++ b/packages/realm-server/tests/runtime-dependency-tracker-test.ts @@ -62,6 +62,35 @@ module(basename(import.meta.filename), function (hooks) { ); }); + test('tracks module deps in canonical RRI (prefix) form', async function (assert) { + beginRuntimeDependencyTrackingSession({ + sessionKey: 'rri-module', + rootURL: 'https://example.com/root.json', + rootKind: 'instance', + }); + + await withRuntimeDependencyTrackingContext( + { + mode: 'non-query', + source: 'test:rri-module', + consumer: 'https://example.com/root.json', + consumerKind: 'instance', + }, + async () => { + // Loader-derived module ids arrive in canonical RRI form for mapped + // realms; the tracker must retain them (not drop them as non-URL + // junk) and strip the executable extension like it does for URLs. + trackRuntimeModuleDependency('@cardstack/base/example-module.gts'); + }, + ); + + let snapshot = snapshotRuntimeDependencies({ excludeQueryOnly: true }); + assert.true( + snapshot.deps.includes('@cardstack/base/example-module'), + 'an RRI-form module dependency is retained in canonical form with its extension stripped', + ); + }); + test('classifies async query-context deps as query-only', async function (assert) { beginRuntimeDependencyTrackingSession({ sessionKey: 'async-query', diff --git a/packages/runtime-common/dependency-tracker.ts b/packages/runtime-common/dependency-tracker.ts index 39502be81f..1ba8e5b2b9 100644 --- a/packages/runtime-common/dependency-tracker.ts +++ b/packages/runtime-common/dependency-tracker.ts @@ -67,7 +67,12 @@ function hasPathExtension(url: string): boolean { } function normalizeModuleURL(url: string): string | undefined { - let canonical = canonicalURL(url); + // The loader emits module ids in canonical form: an http(s) URL (user + // realms) or an RRI prefix like `@cardstack/base/...` (mapped realms). + // canonicalURL handles the URL case (and its query/hash strip); an RRI is + // already canonical and passes through. Anything else (relative/junk) is + // dropped, as before. + let canonical = url.startsWith('@') ? url : canonicalURL(url); if (!canonical) { return undefined; } diff --git a/packages/runtime-common/loader.ts b/packages/runtime-common/loader.ts index d98042bf6d..06ae34adeb 100644 --- a/packages/runtime-common/loader.ts +++ b/packages/runtime-common/loader.ts @@ -413,16 +413,10 @@ export class Loader { let resolvedModule = new URL(moduleIdentifier); let resolvedModuleIdentifier = resolvedModule.href; if (!this.moduleShims.has(resolvedModuleIdentifier)) { - // Normalize tracker keys to the virtual-alias URL form when one - // exists (the dependency tracker requires `http://`/`https://` - // URLs — see `canonicalURL` in dependency-tracker.ts — so RRI - // prefix forms can't be used as keys). Without this, a base - // module imported via the virtual alias - // (`https://cardstack.com/base/X`) and the same module imported - // via the RRI prefix (`@cardstack/base/X` → resolveImport → - // resolved real URL `https://localhost:4201/base/X`) get tracked - // as two separate entries. - let trackingKey = this.canonicalizeTrackingKey(resolvedModuleIdentifier); + // Track the module under its canonical form so every spelling of it — + // virtual alias, resolved real URL, RRI prefix — collapses to one + // tracker node instead of fragmenting. + let trackingKey = this.canonicalIdentifier(resolvedModuleIdentifier); trackRuntimeModuleDependency(trackingKey, dependencyTrackingContext); } @@ -476,13 +470,30 @@ export class Loader { // 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. + // spelling for a prefix-mapped realm (e.g. `@cardstack/base/card-api`), + // otherwise the resolved (real) module URL. + // + // `unresolveURL` folds a prefix-mapped realm onto its RRI, but the virtual + // alias of a plain, prefix-less realm (e.g. a realm served at + // `localhost:4202` but reached via a virtual host) has no prefix to fold + // onto, so it passes straight through — and would leak a virtual URL into + // dependency lists the index persists as real URLs. Collapse any such + // surviving alias to its real URL: `mapURL('virtual-to-real')` rewrites a + // registered virtual→real URL mapping and returns undefined for a real URL + // (or an RRI, which isn't a URL at all), so real URLs and RRIs pass through + // untouched. private canonicalIdentifier(moduleIdentifier: string): string { - return this.virtualNetwork - ? this.virtualNetwork.unresolveURL(moduleIdentifier) - : moduleIdentifier; + if (!this.virtualNetwork) { + return moduleIdentifier; + } + let canonical = this.virtualNetwork.unresolveURL(moduleIdentifier); + if (canonical.startsWith('http://') || canonical.startsWith('https://')) { + let real = this.virtualNetwork.mapURL(canonical, 'virtual-to-real'); + if (real) { + return real.href; + } + } + return canonical; } // Map a set of module identifiers to canonical form, deduped (distinct @@ -507,18 +518,6 @@ export class Loader { 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, @@ -541,10 +540,11 @@ export class Loader { rootModuleIdentifier, )) { if (!this.moduleShims.has(moduleIdentifier)) { - // Same canonicalization as the top-level import-time tracking - // call — collapse virtual-alias / resolved real URL forms onto - // the virtual-alias URL. - let trackingKey = this.canonicalizeTrackingKey(moduleIdentifier); + // Same canonicalization as the top-level import-time tracking call. + // `collectKnownModuleDependencies` yields raw recorded spellings, so + // canonicalIdentifier resolves each before folding to its canonical + // form — otherwise a virtual alias of a plain realm would leak through. + let trackingKey = this.canonicalIdentifier(moduleIdentifier); trackRuntimeModuleDependency(trackingKey, dependencyTrackingContext); } } @@ -883,24 +883,6 @@ export class Loader { return this.moduleCanonicalURLs.get(this.moduleCacheKey(moduleIdentifier)); } - // Collapse a module identifier to its virtual-alias URL form when one - // exists, so the dependency tracker keys aren't fragmented across the - // virtual-alias (`https://cardstack.com/base/X`) and resolved real URL - // (`https://localhost:4201/base/X`) for the same module. Returns the - // input unchanged when no virtual alias is registered. - private canonicalizeTrackingKey(moduleIdentifier: string): string { - if (!this.virtualNetwork) { - return moduleIdentifier; - } - try { - let parsed = new URL(moduleIdentifier); - let virtual = this.virtualNetwork.mapURL(parsed, 'real-to-virtual'); - return virtual ? virtual.href : moduleIdentifier; - } catch { - return moduleIdentifier; - } - } - private captureIdentitiesOfModuleExports( module: any, moduleIdentifier: string,