From 33f4a6d70aa74124ab20288601df0dbf8c6de2c9 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 17 Jul 2026 13:14:11 -0500 Subject: [PATCH] Make the runtime dependency tracker RRI-native MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the dependencyTrackingKey / canonicalizeTrackingKey shim and let the loader's own canonical identifier form feed the dependency tracker directly. The tracker previously keyed module nodes exclusively by http(s) URL: its canonicalURL guard dropped any non-URL identifier. To bridge that, the loader carried a dependencyTrackingKey shim that callers crossed before tracking. That form is no longer needed — the tracker keys nodes by the loader's canonical identifier: an RRI prefix (`@cardstack/base/X`) for a mapped realm, the resolved real URL for a plain realm. Two coordinated changes make direct tracking correct: - canonicalIdentifier never emits a virtual alias. unresolveURL folds a prefix-mapped realm onto its RRI, but the virtual alias of a plain, prefix-less realm has no prefix to fold onto and would pass through — leaking a virtual URL into dependency lists the index persists as real URLs. Any such surviving alias is collapsed to its real URL via mapURL('virtual-to-real'), which no-ops on real URLs and RRIs. The result is an RRI for prefixed realms and the real URL for plain ones. - normalizeModuleURL accepts an RRI prefix alongside http(s) URLs, stripping the executable extension either way. card-api tracks identity.module and getKnownConsumedModules deps directly; the two loader-internal tracking sites use canonicalIdentifier. Instance and file dependency normalization stay URL-keyed, unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/base/card-api.gts | 18 ++-- .../tests/runtime-dependency-tracker-test.ts | 29 +++++++ packages/runtime-common/dependency-tracker.ts | 7 +- packages/runtime-common/loader.ts | 82 ++++++++----------- 4 files changed, 73 insertions(+), 63 deletions(-) diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index 5ddca01807c..8e5f94786cf 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 8975ed0712e..486f2a0b3c2 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 39502be81ff..1ba8e5b2b92 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 d98042bf6de..06ae34adeb4 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,