diff --git a/packages/base/card-serialization.ts b/packages/base/card-serialization.ts index becd3af72c..167717366a 100644 --- a/packages/base/card-serialization.ts +++ b/packages/base/card-serialization.ts @@ -28,6 +28,7 @@ import { getSerializer, humanReadable, identifyCard, + isRegisteredPrefix, isSingleCardDocument, isSingleFileMetaDocument, loadCardDef, @@ -221,6 +222,11 @@ export function serializeCard( ...opts, ...{ maybeRelativeURL(possibleURL: string) { + // Registered prefix refs (e.g. @cardstack/catalog/foo) are already + // in their canonical portable form — return as-is + if (isRegisteredPrefix(possibleURL)) { + return possibleURL; + } let url = maybeURL(possibleURL, modelRelativeTo); if (!url) { throw new Error( @@ -316,6 +322,11 @@ export function serializeFileDef( ...opts, ...{ maybeRelativeURL(possibleURL: string) { + // Registered prefix refs (e.g. @cardstack/catalog/foo) are already + // in their canonical portable form — return as-is + if (isRegisteredPrefix(possibleURL)) { + return possibleURL; + } let url = maybeURL(possibleURL, modelRelativeTo); if (!url) { throw new Error( diff --git a/packages/runtime-common/card-reference-resolver.ts b/packages/runtime-common/card-reference-resolver.ts index ac1ae601f2..b6ed6cad73 100644 --- a/packages/runtime-common/card-reference-resolver.ts +++ b/packages/runtime-common/card-reference-resolver.ts @@ -7,6 +7,15 @@ export function registerCardReferencePrefix( prefixMappings.set(prefix, targetURL); } +export function isRegisteredPrefix(reference: string): boolean { + for (let [prefix] of prefixMappings) { + if (reference.startsWith(prefix)) { + return true; + } + } + return false; +} + function isUrlLikeReference(ref: string): boolean { return ( ref.startsWith('.') || @@ -32,3 +41,15 @@ export function resolveCardReference( } return new URL(reference, relativeTo).href; } + +// Reverse of resolveCardReference: converts a resolved URL back to +// its registered prefix form if one matches. +// e.g. "http://localhost:4201/catalog/foo" → "@cardstack/catalog/foo" +export function unresolveCardReference(resolvedURL: string): string { + for (let [prefix, target] of prefixMappings) { + if (resolvedURL.startsWith(target)) { + return prefix + resolvedURL.slice(target.length); + } + } + return resolvedURL; +} diff --git a/packages/runtime-common/definition-lookup.ts b/packages/runtime-common/definition-lookup.ts index 3a5d3a975c..ef3f9c1b91 100644 --- a/packages/runtime-common/definition-lookup.ts +++ b/packages/runtime-common/definition-lookup.ts @@ -25,7 +25,9 @@ import { hasExecutableExtension, trimExecutableExtension, } from './index'; +import { isRegisteredPrefix } from './card-reference-resolver'; import type { VirtualNetwork } from './virtual-network'; +import { unresolveCardReference } from './card-reference-resolver'; const MODULES_TABLE = 'modules'; const PREFERRED_EXECUTABLE_EXTENSIONS = ['.gts', '.ts', '.gjs', '.js']; @@ -40,7 +42,7 @@ function canonicalURL(url: string, relativeTo?: string): string { let parsed = new URL(url, relativeTo); parsed.search = ''; parsed.hash = ''; - return parsed.href; + return unresolveCardReference(parsed.href); } catch (_e) { let stripped = url.split('#')[0] ?? url; return stripped.split('?')[0] ?? stripped; @@ -54,7 +56,8 @@ function normalizeExecutableURL(url: string): string { try { return trimExecutableExtension(new URL(url)).href; } catch (_e) { - return url; + // Handle non-URL identifiers like @cardstack/catalog/foo.gts + return url.replace(/\.(gts|ts|js|gjs)$/, ''); } } @@ -769,6 +772,11 @@ export class CachingDefinitionLookup implements DefinitionLookup { private normalizeDependencyForLookup(dep: string, relativeTo: URL): string { let canonical = canonicalURL(dep, relativeTo.href); + // For registered prefix deps (e.g. @cardstack/catalog/foo.gts), + // trim executable extensions without URL parsing + if (isRegisteredPrefix(canonical)) { + return canonical.replace(/\.(gts|ts|js|gjs)$/, ''); + } try { let url = new URL(canonical); if (hasExecutableExtension(url.href)) { diff --git a/packages/runtime-common/index-runner/dependency-normalization.ts b/packages/runtime-common/index-runner/dependency-normalization.ts index 840f673eb0..ff7a340065 100644 --- a/packages/runtime-common/index-runner/dependency-normalization.ts +++ b/packages/runtime-common/index-runner/dependency-normalization.ts @@ -1,4 +1,5 @@ import { trimExecutableExtension } from '../index'; +import { isRegisteredPrefix } from '../card-reference-resolver'; import { canonicalURL } from './dependency-url'; export function isExtensionlessPath(url: URL): boolean { @@ -53,6 +54,11 @@ export function normalizeDependencyForLookup( relativeTo: URL, ): string { let canonical = canonicalURL(dep, relativeTo.href); + // For registered prefix deps (e.g. @cardstack/catalog/foo.gts), + // trim executable extensions without URL parsing + if (isRegisteredPrefix(canonical)) { + return canonical.replace(/\.(gts|ts|js|gjs)$/, ''); + } try { return trimExecutableExtension(new URL(canonical)).href; } catch (_err) { diff --git a/packages/runtime-common/index-runner/dependency-url.ts b/packages/runtime-common/index-runner/dependency-url.ts index 1ba232786e..813d76e4e4 100644 --- a/packages/runtime-common/index-runner/dependency-url.ts +++ b/packages/runtime-common/index-runner/dependency-url.ts @@ -1,12 +1,23 @@ -import { resolveCardReference } from '../card-reference-resolver'; +import { + resolveCardReference, + unresolveCardReference, + isRegisteredPrefix, +} from '../card-reference-resolver'; export function canonicalURL(url: string, relativeTo?: string): string { try { + // If the URL is already a registered prefix (e.g. @cardstack/catalog/foo), + // keep it in that form — it's already canonical. + if (isRegisteredPrefix(url)) { + let stripped = url.split('#')[0] ?? url; + return stripped.split('?')[0] ?? stripped; + } let resolved = resolveCardReference(url, relativeTo); let parsed = new URL(resolved); parsed.search = ''; parsed.hash = ''; - return parsed.href; + // Convert resolved URLs back to prefix form if possible + return unresolveCardReference(parsed.href); } catch (_e) { let stripped = url.split('#')[0] ?? url; return stripped.split('?')[0] ?? stripped; diff --git a/packages/runtime-common/index.ts b/packages/runtime-common/index.ts index a1df4bef82..2b9b472293 100644 --- a/packages/runtime-common/index.ts +++ b/packages/runtime-common/index.ts @@ -222,7 +222,10 @@ export { v4 as uuidv4 } from '@lukeed/uuid'; // isomorphic UUID's using Math.ran import type { LocalPath } from './paths'; import type { CardTypeFilter, Query, DataQuery, EveryFilter } from './query'; import { Loader } from './loader'; -import { resolveCardReference } from './card-reference-resolver'; +import { + resolveCardReference, + unresolveCardReference, +} from './card-reference-resolver'; export * from './paths'; export * from './cached-fetch'; export * from './definition-lookup'; @@ -664,9 +667,11 @@ export function internalKeyFor( relativeTo: URL | undefined, ): string { if (!('type' in ref)) { - let module = trimExecutableExtension( - new URL(resolveCardReference(ref.module, relativeTo)), - ).href; + let resolved = resolveCardReference(ref.module, relativeTo); + let module = trimExecutableExtension(new URL(resolved)).href; + // Use the prefix form (e.g. @cardstack/catalog/foo) as the canonical + // internal key when a registered prefix mapping matches + module = unresolveCardReference(module); return `${module}/${ref.name}`; } switch (ref.type) { diff --git a/packages/runtime-common/loader.ts b/packages/runtime-common/loader.ts index 2f73200628..217ac1d8cb 100644 --- a/packages/runtime-common/loader.ts +++ b/packages/runtime-common/loader.ts @@ -10,6 +10,7 @@ import { trackRuntimeModuleDependency, type RuntimeDependencyTrackingContext, } from './dependency-tracker'; +import { unresolveCardReference } from './card-reference-resolver'; type FetchingModule = { state: 'fetching'; @@ -562,7 +563,9 @@ export class Loader { module: any, moduleIdentifier: string, ) { - let moduleId = trimExecutableExtension(new URL(moduleIdentifier)).href; + let moduleId = unresolveCardReference( + trimExecutableExtension(new URL(moduleIdentifier)).href, + ); for (let propName of Object.keys(module)) { let exportedEntity = module[propName]; if ( diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index 23bbd8f6c7..fc61c1d811 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -9,6 +9,7 @@ import { maxLinkDepth, maybeURL, resolveCardReference, + isRegisteredPrefix, IndexQueryEngine, codeRefWithAbsoluteURL, logger, @@ -1038,6 +1039,11 @@ function relativizeResource( setURL(maybeRelativeURL(urlObj, primaryURL, realmURL)); }); visitModuleDeps(resource, (moduleURL, setModuleURL) => { + // Registered prefix references (e.g. @cardstack/catalog/foo) are already + // in their canonical portable form — don't resolve or relativize them. + if (isRegisteredPrefix(moduleURL)) { + return; + } let absoluteModuleURL = new URL( resolveCardReference(moduleURL, resource.id ?? primaryURL), );