Skip to content
Draft
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
18 changes: 6 additions & 12 deletions packages/base/card-api.gts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
29 changes: 29 additions & 0 deletions packages/realm-server/tests/runtime-dependency-tracker-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 6 additions & 1 deletion packages/runtime-common/dependency-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
82 changes: 32 additions & 50 deletions packages/runtime-common/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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,
Expand Down
Loading