From fc28b73723be4f03b11d4d8c45b38cd8be0fb45d Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 15 Jun 2026 09:09:46 -0500 Subject: [PATCH 01/12] Resolve and persist base-realm refs in RRI form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core of the persistence-format migration to RealmResourceIdentifier (RRI) prefix form. The `@cardstack/base/` realm prefix is already registered in the VirtualNetwork; this makes the runtime canonicalize base-realm module identifiers to that prefix form everywhere they are resolved, compared, or persisted. - virtual-network.ts `unresolveURL`: chase a URL-shaped input through any registered virtual → real URL mapping and retry the realm-prefix match, so a virtual-alias URL canonicalizes to its RRI prefix. This is what flips `internalKeyFor` (and therefore index keys, `adoptsFrom`, `types`, and `deps`) to RRI form for base modules. - definition-lookup.ts / host realm.ts: normalize both sides of the local-realm membership check via `unresolveURL` so an RRI-form (or resolved-URL) input still matches a realm keyed by its virtual alias. - loader.ts: collapse dependency-tracker keys onto a single canonical URL form so a base module imported via the virtual alias and the same module imported via the RRI prefix don't fragment into two tracker entries. - constants.ts: base code refs (baseRef/baseCardRef/specRef/etc.) move to the `@cardstack/base/` prefix form via the new `baseRealmRRI`. - module-syntax.ts: emit RRI-form imports for new fields, reusing an existing equivalent import (URL or RRI) instead of duplicating. - realm.ts / render routes / index-query engines: canonicalize emitted deps, the module-not-found message, and the realm-config adoptsFrom check to RRI form. - realm-server main.ts/worker.ts/create-realm.ts: register the scoped `@cardstack/X/` prefix alongside the cardstack.com URL alias and emit RRI refs in bootstrapped realm config. Co-Authored-By: Claude Opus 4.7 --- packages/host/app/routes/render/html.ts | 2 +- packages/host/app/routes/render/meta.ts | 2 +- packages/host/app/services/realm.ts | 14 ++++ .../realm-server/handlers/create-realm.ts | 4 +- packages/realm-server/main.ts | 11 +++ packages/realm-server/worker.ts | 7 ++ packages/runtime-common/constants.ts | 33 +++++--- packages/runtime-common/definition-lookup.ts | 23 +++++- packages/runtime-common/index-query-engine.ts | 4 +- packages/runtime-common/index.ts | 4 +- packages/runtime-common/loader.ts | 42 +++++++++-- packages/runtime-common/module-syntax.ts | 75 ++++++++++++++++++- .../realm-index-query-engine.ts | 4 +- packages/runtime-common/realm.ts | 11 ++- .../runtime-common/schema-analysis-plugin.ts | 3 +- packages/runtime-common/virtual-network.ts | 30 +++++++- 16 files changed, 231 insertions(+), 38 deletions(-) diff --git a/packages/host/app/routes/render/html.ts b/packages/host/app/routes/render/html.ts index 6e9baf0b69d..31434bad589 100644 --- a/packages/host/app/routes/render/html.ts +++ b/packages/host/app/routes/render/html.ts @@ -30,7 +30,7 @@ import type { Model as ParentModel } from '../render'; // the check tolerates whatever resolved form the host's identify path // produces (e.g. realm-aliased base URLs). const CARDS_GRID_REF = { - module: 'https://cardstack.com/base/cards-grid', + module: '@cardstack/base/cards-grid', name: 'CardsGrid', } as ResolvedCodeRef; diff --git a/packages/host/app/routes/render/meta.ts b/packages/host/app/routes/render/meta.ts index 2b94a0d75e8..be98949f833 100644 --- a/packages/host/app/routes/render/meta.ts +++ b/packages/host/app/routes/render/meta.ts @@ -172,7 +172,7 @@ export default class RenderMetaRoute extends Route { internalKeyFor(t, undefined, this.network.virtualNetwork), ), searchDoc, - deps, + deps: deps.map((dep) => this.network.virtualNetwork.unresolveURL(dep)), diagnostics, }; } diff --git a/packages/host/app/services/realm.ts b/packages/host/app/services/realm.ts index 82a85be8795..c20f02c52b8 100644 --- a/packages/host/app/services/realm.ts +++ b/packages/host/app/services/realm.ts @@ -1255,10 +1255,24 @@ export default class RealmService extends Service { } return undefined; } + // `this.realms` is keyed by the user-facing realm URL (typically a + // virtual alias like `https://cardstack.com/base/`). Callers may + // pass the resolved real URL (e.g. `https://localhost:4201/base/`) + // or an RRI prefix (e.g. `@cardstack/base/`) — all three should + // match the same realm. Normalize both sides via + // `virtualNetwork.unresolveURL` (chases through any registered + // virtual → real URL mapping and unifies to the realm-prefix RRI + // form) so the comparison is form-agnostic. + let vn = this.network.virtualNetwork; + let normalizedUrl = vn.unresolveURL(url); for (let [key, value] of this.realms) { if (url.startsWith(key)) { return value; } + let normalizedKey = vn.unresolveURL(key); + if (normalizedUrl.startsWith(normalizedKey)) { + return value; + } } if (tracked) { this.currentKnownRealms.has(url); diff --git a/packages/realm-server/handlers/create-realm.ts b/packages/realm-server/handlers/create-realm.ts index ea3d7b3ee93..10e8f2f02ae 100644 --- a/packages/realm-server/handlers/create-realm.ts +++ b/packages/realm-server/handlers/create-realm.ts @@ -175,7 +175,7 @@ export async function createRealm( }, meta: { adoptsFrom: { - module: 'https://cardstack.com/base/realm-config', + module: '@cardstack/base/realm-config', name: 'RealmConfig', }, }, @@ -186,7 +186,7 @@ export async function createRealm( type: 'card', meta: { adoptsFrom: { - module: 'https://cardstack.com/base/cards-grid', + module: '@cardstack/base/cards-grid', name: 'CardsGrid', }, }, diff --git a/packages/realm-server/main.ts b/packages/realm-server/main.ts index b8db76c556a..91b9d608e6c 100644 --- a/packages/realm-server/main.ts +++ b/packages/realm-server/main.ts @@ -307,6 +307,17 @@ for (let i = 0; i < fromUrls.length; i++) { let fromURL = new URL(from); virtualNetwork.addURLMapping(fromURL, to); urlMappings.push([fromURL, to]); + // Convention: https://cardstack.com/X/ aliases @cardstack/X/. Register + // the realm-prefix mapping too so unresolveURL on either form + // canonicalises to the same RRI form. This matters for cross-process + // cache keys (e.g. host prerender writes definitions cache keyed by + // internalKeyFor; realm-server reads back with the same VN). Without + // this, the realm-server would see only the URL mapping for base and + // the host's RRI-form keys would never match. + let m = from.match(/^https:\/\/cardstack\.com\/([^/]+)\/$/); + if (m) { + virtualNetwork.addRealmMapping(`@cardstack/${m[1]}/`, to.href); + } } else { virtualNetwork.addRealmMapping(from, to.href); urlMappings.push([to, to]); // use toUrl for both in hrefs diff --git a/packages/realm-server/worker.ts b/packages/realm-server/worker.ts index 4741207b83e..917cdb69d6a 100644 --- a/packages/realm-server/worker.ts +++ b/packages/realm-server/worker.ts @@ -133,6 +133,13 @@ for (let i = 0; i < fromUrls.length; i++) { let to = new URL(String(toUrls[i])); if (isUrlLike(from)) { virtualNetwork.addURLMapping(new URL(from), to); + // Convention: https://cardstack.com/X/ aliases @cardstack/X/. Also + // register the realm-prefix mapping so unresolveURL on either form + // produces the same canonical RRI — same reasoning as main.ts. + let m = from.match(/^https:\/\/cardstack\.com\/([^/]+)\/$/); + if (m) { + virtualNetwork.addRealmMapping(`@cardstack/${m[1]}/`, to.href); + } } else { virtualNetwork.addRealmMapping(from, to.href); } diff --git a/packages/runtime-common/constants.ts b/packages/runtime-common/constants.ts index 2927bde6eba..b62124f6d21 100644 --- a/packages/runtime-common/constants.ts +++ b/packages/runtime-common/constants.ts @@ -1,43 +1,58 @@ import { RealmPaths } from './paths.ts'; import type { ResolvedCodeRef } from './code-ref.ts'; -import { rri, type RealmResourceIdentifier } from './realm-identifiers.ts'; +import { + ri, + rri, + type RealmIdentifier, + type RealmResourceIdentifier, +} from './realm-identifiers.ts'; import type { RealmPermissions } from './index.ts'; export const baseRealm = new RealmPaths(new URL('https://cardstack.com/base/')); +/** + * The base realm's canonical RRI prefix. Use this when building code + * refs that should match what `Loader.identify` / `identifyCard` emit + * for base-realm classes (which canonicalise via `vn.unresolveURL` + * to the registered `@cardstack/base/` prefix). + */ +export const baseRealmRRI: RealmIdentifier = ri('@cardstack/base/'); + /** * Build a `RealmResourceIdentifier` for a module inside the base realm. - * Equivalent to `` rri(`${baseRealm.url}${path}`) `` but shorter. + * Returns the prefix-form RRI (e.g. `@cardstack/base/card-api`) so the + * value matches what `Loader.identify` / `identifyCard` emit for + * base-realm classes after the runtime's `unresolveURL` chase. */ export function baseRRI(path: string): RealmResourceIdentifier { - return rri(`${baseRealm.url}${path}`); + return rri(`${baseRealmRRI}${path}`); } export const devSkillLocalPath = 'Skill/boxel-development'; export const envSkillLocalPath = 'Skill/boxel-environment'; export const baseRef: ResolvedCodeRef = { - module: `${baseRealm.url}card-api` as RealmResourceIdentifier, + module: `${baseRealmRRI}card-api` as RealmResourceIdentifier, name: 'BaseDef', }; export const specRef: ResolvedCodeRef = { - module: `${baseRealm.url}spec` as RealmResourceIdentifier, + module: `${baseRealmRRI}spec` as RealmResourceIdentifier, name: 'Spec', }; export const baseCardRef: ResolvedCodeRef = { - module: `${baseRealm.url}card-api` as RealmResourceIdentifier, + module: `${baseRealmRRI}card-api` as RealmResourceIdentifier, name: 'CardDef', }; export const baseFieldRef: ResolvedCodeRef = { - module: `${baseRealm.url}card-api` as RealmResourceIdentifier, + module: `${baseRealmRRI}card-api` as RealmResourceIdentifier, name: 'FieldDef', }; export const skillCardRef: ResolvedCodeRef = { - module: `${baseRealm.url}skill` as RealmResourceIdentifier, + module: `${baseRealmRRI}skill` as RealmResourceIdentifier, name: 'Skill', }; export const baseFileRef: ResolvedCodeRef = { - module: `${baseRealm.url}card-api` as RealmResourceIdentifier, + module: `${baseRealmRRI}card-api` as RealmResourceIdentifier, name: 'FileDef', }; diff --git a/packages/runtime-common/definition-lookup.ts b/packages/runtime-common/definition-lookup.ts index 367e4f4a164..b3929ce4266 100644 --- a/packages/runtime-common/definition-lookup.ts +++ b/packages/runtime-common/definition-lookup.ts @@ -1155,8 +1155,29 @@ export class CachingDefinitionLookup implements DefinitionLookup { cacheUserId: string; prerenderUserId: string; } | null> { + // `canonicalURL` of an RRI-prefix input resolves to the realm's + // RESOLVED real URL via `vn.toURL` (e.g. `@cardstack/base/foo` → + // `https://localhost:4201/base/foo`), while `#realms` is keyed by + // the user-facing realm URL — typically the virtual alias like + // `https://cardstack.com/base/`. The direct startsWith check + // therefore misses local realms whenever the input was RRI form + // (or whenever realm.url is the virtual alias and the input + // canonicalised to the resolved URL). + // + // Normalize both sides to RRI form via `unresolveURL` (which + // chases through any registered virtual → real URL mapping and + // matches against realm-prefix targets) so the comparison is + // form-agnostic. After normalization, `https://localhost:4201/base/foo`, + // `https://cardstack.com/base/foo`, and `@cardstack/base/foo` all + // become `@cardstack/base/foo`. + let vn = this.#virtualNetwork; + let normalizedModuleURL = vn.unresolveURL(moduleURL); let localRealm = this.#realms.find((realm) => { - return moduleURL.startsWith(realm.url); + if (moduleURL.startsWith(realm.url)) { + return true; + } + let normalizedRealmURL = vn.unresolveURL(realm.url); + return normalizedModuleURL.startsWith(normalizedRealmURL); }); if (localRealm) { diff --git a/packages/runtime-common/index-query-engine.ts b/packages/runtime-common/index-query-engine.ts index ff5bb4a7ed7..b79b10bfbc1 100644 --- a/packages/runtime-common/index-query-engine.ts +++ b/packages/runtime-common/index-query-engine.ts @@ -9,7 +9,7 @@ import { baseCardRef, internalKeyFor, isResolvedCodeRef, - baseRealm, + baseRealmRRI, getSerializer, } from './index.ts'; import { @@ -1828,7 +1828,7 @@ async function getField( isPrimitive: true, isComputed: false, fieldOrCard: { - module: `${baseRealm.url}card-api`, + module: `${baseRealmRRI}card-api`, name: 'StringField', }, } as FieldDefinition; diff --git a/packages/runtime-common/index.ts b/packages/runtime-common/index.ts index 319a8ef54da..ef5822c4cc3 100644 --- a/packages/runtime-common/index.ts +++ b/packages/runtime-common/index.ts @@ -1245,9 +1245,7 @@ export async function apiFor( let loader = Loader.getLoaderFor(cardOrFieldOrClass) ?? loaderFor(cardOrFieldOrClass as CardDef | FieldDef | BaseDef); - let api = await loader.import( - 'https://cardstack.com/base/card-api', - ); + let api = await loader.import('@cardstack/base/card-api'); if (!api) { throw new Error(`could not load card API`); } diff --git a/packages/runtime-common/loader.ts b/packages/runtime-common/loader.ts index 9cd0e3ac816..088283f3e42 100644 --- a/packages/runtime-common/loader.ts +++ b/packages/runtime-common/loader.ts @@ -395,10 +395,17 @@ export class Loader { let resolvedModule = new URL(moduleIdentifier); let resolvedModuleIdentifier = resolvedModule.href; if (!this.moduleShims.has(resolvedModuleIdentifier)) { - trackRuntimeModuleDependency( - resolvedModuleIdentifier, - dependencyTrackingContext, - ); + // 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); + trackRuntimeModuleDependency(trackingKey, dependencyTrackingContext); } await this.advanceToState(resolvedModule, 'evaluated'); @@ -466,10 +473,11 @@ export class Loader { rootModuleIdentifier, )) { if (!this.moduleShims.has(moduleIdentifier)) { - trackRuntimeModuleDependency( - moduleIdentifier, - dependencyTrackingContext, - ); + // 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); + trackRuntimeModuleDependency(trackingKey, dependencyTrackingContext); } } } @@ -792,6 +800,24 @@ export class Loader { return this.moduleCanonicalURLs.get(trimModuleIdentifier(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, diff --git a/packages/runtime-common/module-syntax.ts b/packages/runtime-common/module-syntax.ts index 42d6f273d95..5366b3b084c 100644 --- a/packages/runtime-common/module-syntax.ts +++ b/packages/runtime-common/module-syntax.ts @@ -450,15 +450,41 @@ function makeNewField({ return `@${fieldDecorator.name} ${fieldName} = ${fieldTypeIdentifier.name}(() => ${fieldRef.name});`; } + // Canonicalize `fieldRef.module` to the registered RRI prefix form + // (e.g. `@cardstack/base/X`) so generated import lines use the stable + // identifier rather than a deployment-specific real URL. + let canonicalFieldModule = virtualNetwork.unresolveURL(fieldRef.module); + let relativeFieldModuleRef; if (incomingRelativeTo && outgoingRelativeTo) { + let resolved = virtualNetwork.resolveURL( + canonicalFieldModule, + incomingRelativeTo, + ); + let canonical = virtualNetwork.unresolveURL(resolved.href); relativeFieldModuleRef = maybeRelativeReference( - virtualNetwork.resolveURL(fieldRef.module, incomingRelativeTo), + canonical, outgoingRelativeTo, outgoingRealmURL, ); } else { - relativeFieldModuleRef = fieldRef.module; + relativeFieldModuleRef = canonicalFieldModule; + } + + // `ImportUtil` matches existing imports by exact source string. The + // file may still use the virtual-alias URL form + // (`https://cardstack.com/base/X`) for an equivalent module while the + // canonical form is the RRI prefix (`@cardstack/base/X`). Reuse the + // existing import specifier when one matches under VN canonicalization + // so we merge instead of emitting a duplicate import line. + let existingEquivalentSource = findEquivalentImportSource( + programPath, + relativeFieldModuleRef, + incomingRelativeTo, + virtualNetwork, + ); + if (existingEquivalentSource) { + relativeFieldModuleRef = existingEquivalentSource; } let fieldCardIdentifier = importUtil.import( @@ -489,6 +515,51 @@ function makeNewField({ return `@${fieldDecorator.name} ${fieldName} = ${fieldTypeIdentifier.name}(${fieldCardIdentifier.name});`; } +// Returns the source string of an existing `ImportDeclaration` that +// resolves to the same module as `targetSpecifier` under VN +// canonicalization (RRI prefix vs. registered URL alias), so callers +// can merge new identifiers into the existing import line rather than +// emitting a duplicate. Returns undefined when no existing import +// matches or when the file is being authored against the canonical +// specifier directly. +function findEquivalentImportSource( + programPath: NodePath, + targetSpecifier: string, + relativeTo: RealmResourceIdentifier | URL | undefined, + virtualNetwork: VirtualNetwork, +): string | undefined { + let targetCanonical: string; + try { + targetCanonical = virtualNetwork.unresolveURL( + virtualNetwork.resolveURL(targetSpecifier, relativeTo).href, + ); + } catch { + return undefined; + } + for (let node of programPath.node.body) { + if (node.type !== 'ImportDeclaration') { + continue; + } + let source = node.source.value; + if (source === targetSpecifier) { + // already exact-match — importUtil will merge without help + return undefined; + } + let sourceCanonical: string; + try { + sourceCanonical = virtualNetwork.unresolveURL( + virtualNetwork.resolveURL(source, relativeTo).href, + ); + } catch { + continue; + } + if (sourceCanonical === targetCanonical) { + return source; + } + } + return undefined; +} + function getProgramPath(path: NodePath): NodePath { let currentPath: NodePath | null = path; while (currentPath && currentPath.type !== 'Program') { diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index 8db24b8e5be..0239fd2e65d 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep'; import { SupportedMimeType, isJsonContentType, - baseRealm, + baseRealmRRI, inferContentType, unixTime, maxLinkDepth, @@ -2492,7 +2492,7 @@ function fileResourceFromIndex( (isCodeRef(fileEntry.resource?.meta?.adoptsFrom) ? fileEntry.resource?.meta?.adoptsFrom : { - module: `${baseRealm.url}card-api`, + module: `${baseRealmRRI}card-api`, name: 'FileDef', }); let resourceAttributes = fileEntry.resource?.attributes ?? {}; diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index 6f9f321ec5f..26a3859f4e0 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -3008,7 +3008,11 @@ export class Realm { if (!maybeFileRef) { return { kind: 'not-found', - response: notFound(request, requestContext, `${request.url} not found`), + response: notFound( + request, + requestContext, + `${this.#virtualNetwork.unresolveURL(request.url)} not found`, + ), }; } @@ -6887,7 +6891,7 @@ export class Realm { attributes: {}, meta: { adoptsFrom: { - module: 'https://cardstack.com/base/realm-config', + module: '@cardstack/base/realm-config', name: 'RealmConfig', }, }, @@ -6921,7 +6925,8 @@ export class Realm { let adoptsFrom = (cardDoc!.data as any).meta?.adoptsFrom; if ( !isPlainObject(adoptsFrom) || - adoptsFrom.module !== 'https://cardstack.com/base/realm-config' || + this.#virtualNetwork.unresolveURL(adoptsFrom.module) !== + '@cardstack/base/realm-config' || adoptsFrom.name !== 'RealmConfig' ) { return systemError({ diff --git a/packages/runtime-common/schema-analysis-plugin.ts b/packages/runtime-common/schema-analysis-plugin.ts index 83ddd8fbd84..bfc0c5b7bea 100644 --- a/packages/runtime-common/schema-analysis-plugin.ts +++ b/packages/runtime-common/schema-analysis-plugin.ts @@ -193,7 +193,8 @@ const coreVisitor = { let isCardApiFile = path.node.loc && 'filename' in path.node.loc && - path.node.loc.filename === 'https://cardstack.com/base/card-api'; + (path.node.loc.filename === '@cardstack/base/card-api' || + path.node.loc.filename === 'https://cardstack.com/base/card-api'); let decoratorInfo = getNamedImportInfo(path.scope, expression.node.name); if (!decoratorInfo && !isCardApiFile) { return; // our @field decorator must originate from a named import diff --git a/packages/runtime-common/virtual-network.ts b/packages/runtime-common/virtual-network.ts index 6efad49372d..aa75a185ccd 100644 --- a/packages/runtime-common/virtual-network.ts +++ b/packages/runtime-common/virtual-network.ts @@ -121,9 +121,17 @@ export class VirtualNetwork { } /** - * Convert a resolved URL back to its registered prefix form when one - * matches, e.g. `http://localhost:4201/catalog/foo` → `@cardstack/catalog/foo`. - * URLs that don't match any registered prefix are returned as-is. + * Convert a URL back to its registered prefix form when one matches, + * e.g. `http://localhost:4201/catalog/foo` → `@cardstack/catalog/foo`. + * + * If the input doesn't directly match any realm-prefix target, and the + * input is URL-shaped, chase through any virtual→real URL mapping (e.g. + * `https://cardstack.com/base/X` → `http://localhost:4201/base/X`) and + * retry the realm-prefix match. This bridges the gap when a realm + * prefix is registered against the resolved URL but the caller hands + * us the unresolved virtual URL. + * + * Inputs that match no prefix and no URL mapping are returned as-is. */ unresolveURL(url: string): RealmResourceIdentifier { for (let [prefix, target] of this.realmMappings) { @@ -131,6 +139,22 @@ export class VirtualNetwork { return (prefix + url.slice(target.length)) as RealmResourceIdentifier; } } + if (url.startsWith('http://') || url.startsWith('https://')) { + let resolved: string | undefined; + try { + resolved = this.resolveURLMapping(url, 'virtual-to-real'); + } catch { + resolved = undefined; + } + if (resolved && resolved !== url) { + for (let [prefix, target] of this.realmMappings) { + if (resolved.startsWith(target)) { + return (prefix + + resolved.slice(target.length)) as RealmResourceIdentifier; + } + } + } + } return url as RealmResourceIdentifier; } From a8dd9f21c6a002e18500473e6fd6aa079db5dbb8 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 15 Jun 2026 09:10:38 -0500 Subject: [PATCH 02/12] Emit RRI form at code-generation and config surfaces Surfaces that generate source code, hold lint config, or are realm content now use the `@cardstack/base/` prefix form, matching what the runtime resolves and persists. - eslint-plugin-boxel import-utils + missing-card-api-import-config: the auto-import rule's configured target modules move to RRI form, and the rule treats URL-form and RRI-form base imports as equivalent so it merges into an existing import of either form rather than emitting a duplicate. - create-file-modal / edit-field-modal / item-button: generated import lines and default code refs emit RRI form. - base/cards-grid + cards-grid-layout, experiments-realm cards: realm content card refs move to RRI form. - boxel-cli parse, boxel-ui spec generator, software-factory smoke scripts, vscode-boxel-tools skills: tooling that emits or asserts base code refs moves to RRI form. Co-Authored-By: Claude Opus 4.7 --- packages/base/cards-grid.gts | 8 +-- .../base/components/cards-grid-layout.gts | 4 +- packages/boxel-cli/src/commands/parse.ts | 2 +- .../addon/bin/generate-component-specs.mjs | 26 ++++--- .../lib/utils/import-utils.js | 33 ++++++++- packages/experiments-realm/app-card.gts | 4 +- .../file-search-playground.gts | 4 +- packages/experiments-realm/garden-design.gts | 4 +- .../components/card-search/item-button.gts | 2 +- .../operator-mode/create-file-modal.gts | 12 +++- .../operator-mode/edit-field-modal.gts | 2 +- .../eslint/missing-card-api-import-config.js | 67 +++++++++---------- .../smoke-test-factory-scenarios.ts | 2 +- .../scripts/smoke-tests/smoke-test-realm.ts | 2 +- packages/vscode-boxel-tools/src/skills.ts | 4 +- 15 files changed, 106 insertions(+), 70 deletions(-) diff --git a/packages/base/cards-grid.gts b/packages/base/cards-grid.gts index e914781d83d..6355621e49b 100644 --- a/packages/base/cards-grid.gts +++ b/packages/base/cards-grid.gts @@ -18,7 +18,7 @@ import FileIcon from '@cardstack/boxel-icons/file'; import { chooseCard, specRef, - baseRealm, + baseRealmRRI, baseFileRef, isCardInstance, SupportedMimeType, @@ -361,12 +361,12 @@ class Isolated extends Component { }; }[]; let excludedCardTypeIds = [ - `${baseRealm.url}card-api/CardDef`, - `${baseRealm.url}cards-grid/CardsGrid`, + `${baseRealmRRI}card-api/CardDef`, + `${baseRealmRRI}cards-grid/CardsGrid`, ]; // The "All Files" group already represents the bare FileDef root — listing // it again as a leaf would just be a duplicate row. - let excludedFileTypeIds = [`${baseRealm.url}card-api/FileDef`]; + let excludedFileTypeIds = [`${baseRealmRRI}card-api/FileDef`]; this.cardTypeFilters.splice(0, this.cardTypeFilters.length); this.fileTypeFilters.splice(0, this.fileTypeFilters.length); diff --git a/packages/base/components/cards-grid-layout.gts b/packages/base/components/cards-grid-layout.gts index 0fdef243c12..2bde84aa6eb 100644 --- a/packages/base/components/cards-grid-layout.gts +++ b/packages/base/components/cards-grid-layout.gts @@ -14,7 +14,7 @@ import { } from '@cardstack/boxel-ui/icons'; import { - baseRealm, + baseRealmRRI, type Format, type Query, type RealmResourceIdentifier, @@ -51,7 +51,7 @@ export const SORT_OPTIONS: SortOption[] = [ sort: [ { on: { - module: `${baseRealm.url}card-api` as RealmResourceIdentifier, + module: `${baseRealmRRI}card-api` as RealmResourceIdentifier, name: 'CardDef', }, by: 'cardTitle', diff --git a/packages/boxel-cli/src/commands/parse.ts b/packages/boxel-cli/src/commands/parse.ts index 39f85f61653..76884f92a8f 100644 --- a/packages/boxel-cli/src/commands/parse.ts +++ b/packages/boxel-cli/src/commands/parse.ts @@ -36,7 +36,7 @@ import { search } from './search.ts'; * `@cardstack/runtime-common/constants`. */ const SPEC_TYPE = { - module: 'https://cardstack.com/base/spec', + module: '@cardstack/base/spec', name: 'Spec', } as const; diff --git a/packages/boxel-ui/addon/bin/generate-component-specs.mjs b/packages/boxel-ui/addon/bin/generate-component-specs.mjs index eec097b344a..319ef66aae5 100644 --- a/packages/boxel-ui/addon/bin/generate-component-specs.mjs +++ b/packages/boxel-ui/addon/bin/generate-component-specs.mjs @@ -21,7 +21,13 @@ const ADDON_DIR = path.resolve(SCRIPT_DIR, '..'); const REPO_ROOT = path.resolve(ADDON_DIR, '..', '..', '..'); const COMPONENTS_DIR = path.join(ADDON_DIR, 'src', 'components'); -const CATALOG_DIR = path.join(REPO_ROOT, 'packages', 'catalog', 'contents', 'Spec'); +const CATALOG_DIR = path.join( + REPO_ROOT, + 'packages', + 'catalog', + 'contents', + 'Spec', +); const BARREL_FILE = path.join(ADDON_DIR, 'src', 'components.ts'); const SPEC_MODULE = '@cardstack/boxel-ui/components'; @@ -37,7 +43,8 @@ const SPEC_FILE_PREFIX = 'boxel-ui-'; // that doesn't actually exist. function buildBarrelExportMap() { const source = fs.readFileSync(BARREL_FILE, 'utf8'); - const re = /^import\s+([A-Za-z0-9_$]+)[^;]*\s+from\s+['"]\.\/components\/([a-z0-9-]+)\/index\.gts['"]/gm; + const re = + /^import\s+([A-Za-z0-9_$]+)[^;]*\s+from\s+['"]\.\/components\/([a-z0-9-]+)\/index\.gts['"]/gm; const candidates = new Map(); let m; while ((m = re.exec(source)) !== null) { @@ -104,7 +111,9 @@ function extractPrimaryUsageBlock(source) { function extractStringAttr(text, name) { // @name='value' or @name="value" - const re = new RegExp(`@${name}=(?:'((?:[^'\\\\]|\\\\.)*)'|"((?:[^"\\\\]|\\\\.)*)")`); + const re = new RegExp( + `@${name}=(?:'((?:[^'\\\\]|\\\\.)*)'|"((?:[^"\\\\]|\\\\.)*)")`, + ); const m = text.match(re); if (!m) return null; return (m[1] ?? m[2]).replace(/\\'/g, "'").replace(/\\"/g, '"'); @@ -152,10 +161,7 @@ function extractOptions(text, source) { // const (`const validBottomTreatments = [...]`) shapes when they're array // literals — which is most of the enum cases in usage.gts files. if (source) { - const re = new RegExp( - `(?:^|\\b)${ref}\\s*=\\s*\\[([^\\]]*)\\]`, - 'm', - ); + const re = new RegExp(`(?:^|\\b)${ref}\\s*=\\s*\\[([^\\]]*)\\]`, 'm'); const m = source.match(re); if (m) { const opts = []; @@ -399,9 +405,7 @@ function buildReadme({ } sections.push('## Import'); sections.push( - '```ts\n' + - `import { ${componentName} } from '${SPEC_MODULE}';\n` + - '```', + '```ts\n' + `import { ${componentName} } from '${SPEC_MODULE}';\n` + '```', ); sections.push('## API'); sections.push(buildApiTable(args)); @@ -438,7 +442,7 @@ function buildSpecJson({ componentName, cardDescription, readMe }) { }, meta: { adoptsFrom: { - module: 'https://cardstack.com/base/spec', + module: '@cardstack/base/spec', name: 'Spec', }, }, diff --git a/packages/eslint-plugin-boxel/lib/utils/import-utils.js b/packages/eslint-plugin-boxel/lib/utils/import-utils.js index 2e21ce6f858..b1a4bdc78b1 100644 --- a/packages/eslint-plugin-boxel/lib/utils/import-utils.js +++ b/packages/eslint-plugin-boxel/lib/utils/import-utils.js @@ -1,5 +1,30 @@ /** @type {import('eslint').Rule.RuleModule} */ +// URL-form alias for each registered RRI prefix. Lets the rule treat +// `https://cardstack.com/base/X` and `@cardstack/base/X` as the same +// module so a missing import configured in one form merges into an +// existing import that uses the other form. Add new realms here as the +// runtime registers their aliases. +const REALM_PREFIX_ALIASES = { + '@cardstack/base/': 'https://cardstack.com/base/', +}; + +function canonicalizeModuleSpecifier(specifier) { + if (typeof specifier !== 'string') { + return specifier; + } + for (const [rriPrefix, urlPrefix] of Object.entries(REALM_PREFIX_ALIASES)) { + if (specifier.startsWith(rriPrefix)) { + return urlPrefix + specifier.slice(rriPrefix.length); + } + } + return specifier; +} + +function modulesAreEquivalent(a, b) { + return canonicalizeModuleSpecifier(a) === canonicalizeModuleSpecifier(b); +} + /** * Adds an import statement for a missing import, or augments an existing import statement * @param {import('eslint').Rule.RuleFixer} fixer The fixer instance @@ -16,11 +41,13 @@ function fixMissingImport( exportedName, module, ) { - // Check if an import from this module already exists + // Check if an import from this module already exists. + // URL-form and RRI-form imports of the same registered realm module + // are treated as equivalent — see `REALM_PREFIX_ALIASES`. const importDeclarations = sourceCode.ast.body.filter( (node) => node.type === 'ImportDeclaration' && - node.source.value === module && + modulesAreEquivalent(node.source.value, module) && // Skip type-only imports node.importKind !== 'type', ); @@ -174,4 +201,6 @@ module.exports = { fixMissingImport, isBound, buildImportStatement, + modulesAreEquivalent, + canonicalizeModuleSpecifier, }; diff --git a/packages/experiments-realm/app-card.gts b/packages/experiments-realm/app-card.gts index bacfa80556a..b4c4b6e27d5 100644 --- a/packages/experiments-realm/app-card.gts +++ b/packages/experiments-realm/app-card.gts @@ -19,7 +19,7 @@ import { CardContainer } from '@cardstack/boxel-ui/components'; import { and, bool, cn } from '@cardstack/boxel-ui/helpers'; import { type Query, - baseRealm, + baseRealmRRI, type PrerenderedCardLike, } from '@cardstack/runtime-common'; import { hash } from '@ember/helper'; @@ -321,7 +321,7 @@ class DefaultTabTemplate extends GlimmerComponent { sort: [ { on: { - module: `${baseRealm.url}card-api`, + module: `${baseRealmRRI}card-api`, name: 'CardDef', }, by: 'cardTitle', diff --git a/packages/experiments-realm/file-search-playground.gts b/packages/experiments-realm/file-search-playground.gts index 58181a3220f..3564c7a18d6 100644 --- a/packages/experiments-realm/file-search-playground.gts +++ b/packages/experiments-realm/file-search-playground.gts @@ -17,13 +17,13 @@ const fileSearchQuery = { every: [ { type: { - module: rri('https://cardstack.com/base/card-api'), + module: rri('@cardstack/base/card-api'), name: 'FileDef', }, }, { on: { - module: rri('https://cardstack.com/base/card-api'), + module: rri('@cardstack/base/card-api'), name: 'FileDef', }, contains: { diff --git a/packages/experiments-realm/garden-design.gts b/packages/experiments-realm/garden-design.gts index d7b2068e194..4bba2b4da7e 100644 --- a/packages/experiments-realm/garden-design.gts +++ b/packages/experiments-realm/garden-design.gts @@ -16,7 +16,7 @@ import type Owner from '@ember/owner'; import { htmlSafe } from '@ember/template'; import { tracked } from '@glimmer/tracking'; import { TrackedMap } from 'tracked-built-ins'; -import { baseRealm, type getCards } from '@cardstack/runtime-common'; +import { baseRealmRRI, type getCards } from '@cardstack/runtime-common'; import LayoutBoardSplitIcon from '@cardstack/boxel-icons/layout-board-split'; import PlantIcon from '@cardstack/boxel-icons/plant'; @@ -221,7 +221,7 @@ class Isolated extends Component { sort: [ { on: { - module: `${baseRealm.url}card-api`, + module: `${baseRealmRRI}card-api`, name: 'CardDef', }, by: 'cardTitle', diff --git a/packages/host/app/components/card-search/item-button.gts b/packages/host/app/components/card-search/item-button.gts index 50139b9cd5c..de31395a8e9 100644 --- a/packages/host/app/components/card-search/item-button.gts +++ b/packages/host/app/components/card-search/item-button.gts @@ -131,7 +131,7 @@ interface Signature { // template — used when the caller doesn't thread a resolved render type. let defaultResultsCardRef: ResolvedCodeRef = { name: 'CardDef', - module: rri('https://cardstack.com/base/card-api'), + module: rri('@cardstack/base/card-api'), }; function isNewCardArgs(item: ItemType): item is NewCardArgs { diff --git a/packages/host/app/components/operator-mode/create-file-modal.gts b/packages/host/app/components/operator-mode/create-file-modal.gts index 9825df6a81e..4592f4b1a56 100644 --- a/packages/host/app/components/operator-mode/create-file-modal.gts +++ b/packages/host/app/components/operator-mode/create-file-modal.gts @@ -844,9 +844,15 @@ export default class CreateFileModal extends Component { this.network.virtualNetwork, ) as ResolvedCodeRef ).module; - const absoluteModule = new URL(absoluteModuleHref); + // `codeRefWithAbsoluteIdentifier` returns the resolved real URL for + // RRI-prefix inputs (`@cardstack/base/X` → `https://localhost:4201/base/X`). + // Canonicalize to the registered RRI prefix form when one matches so + // generated import lines use the stable identifier (`@cardstack/base/X`) + // instead of a deployment-specific real URL. + const canonicalModule = + this.network.virtualNetwork.unresolveURL(absoluteModuleHref); let moduleURL = maybeRelativeReference( - absoluteModule, + canonicalModule, url, new URL(this.selectedRealmURL), ); @@ -854,7 +860,7 @@ export default class CreateFileModal extends Component { let componentImport = isFileDef ? '' - : `\nimport { Component } from 'https://cardstack.com/base/card-api';`; + : `\nimport { Component } from '@cardstack/base/card-api';`; // There is actually only one possible declaration collision: `className` and `parent`, // reconcile that particular collision as necessary. diff --git a/packages/host/app/components/operator-mode/edit-field-modal.gts b/packages/host/app/components/operator-mode/edit-field-modal.gts index 342fdfa58ef..a036f1fd757 100644 --- a/packages/host/app/components/operator-mode/edit-field-modal.gts +++ b/packages/host/app/components/operator-mode/edit-field-modal.gts @@ -130,7 +130,7 @@ export default class EditFieldModal extends Component { // When adding a new field, we want to default to the base string card if (!field) { let ref = { - module: rri('https://cardstack.com/base/card-api'), // This seems fundamental enough to be hardcoded + module: rri('@cardstack/base/card-api'), // This seems fundamental enough to be hardcoded name: 'StringField', }; this.isFieldDef = true; diff --git a/packages/runtime-common/etc/eslint/missing-card-api-import-config.js b/packages/runtime-common/etc/eslint/missing-card-api-import-config.js index 60d2cc942cf..13f99c5ced7 100644 --- a/packages/runtime-common/etc/eslint/missing-card-api-import-config.js +++ b/packages/runtime-common/etc/eslint/missing-card-api-import-config.js @@ -4,46 +4,43 @@ module.exports = { importMappings: { // card-api methods, classes and decorators - Component: ['Component', 'https://cardstack.com/base/card-api'], - CardDef: ['CardDef', 'https://cardstack.com/base/card-api'], - FieldDef: ['FieldDef', 'https://cardstack.com/base/card-api'], - field: ['field', 'https://cardstack.com/base/card-api'], - contains: ['contains', 'https://cardstack.com/base/card-api'], - linksTo: ['linksTo', 'https://cardstack.com/base/card-api'], - containsMany: ['containsMany', 'https://cardstack.com/base/card-api'], - linksToMany: ['linksToMany', 'https://cardstack.com/base/card-api'], + Component: ['Component', '@cardstack/base/card-api'], + CardDef: ['CardDef', '@cardstack/base/card-api'], + FieldDef: ['FieldDef', '@cardstack/base/card-api'], + field: ['field', '@cardstack/base/card-api'], + contains: ['contains', '@cardstack/base/card-api'], + linksTo: ['linksTo', '@cardstack/base/card-api'], + containsMany: ['containsMany', '@cardstack/base/card-api'], + linksToMany: ['linksToMany', '@cardstack/base/card-api'], // Base realm field defs - AddressField: ['default', 'https://cardstack.com/base/address'], - Base64ImageField: ['default', 'https://cardstack.com/base/base64-image'], - BigIntegerField: ['default', 'https://cardstack.com/base/big-integer'], - BooleanField: ['default', 'https://cardstack.com/base/boolean'], - CodeRefField: ['default', 'https://cardstack.com/base/code-ref'], - ColorField: ['default', 'https://cardstack.com/base/color'], - CoordinateField: ['default', 'https://cardstack.com/base/coordinate'], - CountryField: ['default', 'https://cardstack.com/base/country'], - DateField: ['default', 'https://cardstack.com/base/date'], - DateRangeField: ['default', 'https://cardstack.com/base/date-range-field'], - DateTimeField: ['default', 'https://cardstack.com/base/datetime'], - EmailField: ['default', 'https://cardstack.com/base/email'], - EthereumAddressField: [ - 'default', - 'https://cardstack.com/base/ethereum-address', - ], - MarkdownField: ['default', 'https://cardstack.com/base/markdown'], - NumberField: ['default', 'https://cardstack.com/base/number'], - PercentageField: ['default', 'https://cardstack.com/base/percentage'], - PhoneNumberField: ['default', 'https://cardstack.com/base/phone-number'], - StringField: ['default', 'https://cardstack.com/base/string'], - TextAreaField: ['default', 'https://cardstack.com/base/text-area'], - URLField: ['default', 'https://cardstack.com/base/url'], - WebsiteField: ['default', 'https://cardstack.com/base/website'], + AddressField: ['default', '@cardstack/base/address'], + Base64ImageField: ['default', '@cardstack/base/base64-image'], + BigIntegerField: ['default', '@cardstack/base/big-integer'], + BooleanField: ['default', '@cardstack/base/boolean'], + CodeRefField: ['default', '@cardstack/base/code-ref'], + ColorField: ['default', '@cardstack/base/color'], + CoordinateField: ['default', '@cardstack/base/coordinate'], + CountryField: ['default', '@cardstack/base/country'], + DateField: ['default', '@cardstack/base/date'], + DateRangeField: ['default', '@cardstack/base/date-range-field'], + DateTimeField: ['default', '@cardstack/base/datetime'], + EmailField: ['default', '@cardstack/base/email'], + EthereumAddressField: ['default', '@cardstack/base/ethereum-address'], + MarkdownField: ['default', '@cardstack/base/markdown'], + NumberField: ['default', '@cardstack/base/number'], + PercentageField: ['default', '@cardstack/base/percentage'], + PhoneNumberField: ['default', '@cardstack/base/phone-number'], + StringField: ['default', '@cardstack/base/string'], + TextAreaField: ['default', '@cardstack/base/text-area'], + URLField: ['default', '@cardstack/base/url'], + WebsiteField: ['default', '@cardstack/base/website'], // Enumerations - enumField: ['default', 'https://cardstack.com/base/enum'], - enumConfig: ['enumConfig', 'https://cardstack.com/base/enum'], + enumField: ['default', '@cardstack/base/enum'], + enumConfig: ['enumConfig', '@cardstack/base/enum'], // More - Skill: ['default', 'https://cardstack.com/base/skill'], + Skill: ['default', '@cardstack/base/skill'], }, }; diff --git a/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts b/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts index b15b1aa375e..0503a1274ea 100644 --- a/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts +++ b/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts @@ -178,7 +178,7 @@ function buildSpecDocument() { }, meta: { adoptsFrom: { - module: 'https://cardstack.com/base/spec', + module: '@cardstack/base/spec', name: 'Spec', }, }, diff --git a/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts b/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts index a03c6158b61..6468ba65d21 100644 --- a/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts +++ b/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts @@ -67,7 +67,7 @@ const HELLO_SPEC_CARD = { }, meta: { adoptsFrom: { - module: 'https://cardstack.com/base/spec', + module: '@cardstack/base/spec', name: 'Spec', }, }, diff --git a/packages/vscode-boxel-tools/src/skills.ts b/packages/vscode-boxel-tools/src/skills.ts index 1ea2d627e28..590c29c812b 100644 --- a/packages/vscode-boxel-tools/src/skills.ts +++ b/packages/vscode-boxel-tools/src/skills.ts @@ -384,14 +384,14 @@ export class SkillList extends vscode.TreeItem { { by: 'title', on: { - module: 'https://cardstack.com/base/card-api', + module: '@cardstack/base/card-api', name: 'CardDef', }, }, ], filter: { type: { - module: 'https://cardstack.com/base/skill', + module: '@cardstack/base/skill', name: 'Skill', }, }, From 448bc0e3619bb8d307802a9c06579232ac257035 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 15 Jun 2026 09:30:02 -0500 Subject: [PATCH 03/12] Flip test fixtures and assertions to RRI form Mechanical sweep across the test suites: fixtures and assertions that referenced base-realm modules in virtual-alias URL form (`https://cardstack.com/base/X`) now use the RRI prefix form (`@cardstack/base/X`), matching what the runtime resolves, serializes, and indexes after the preceding two commits. Covers card-source fixtures, `meta.adoptsFrom.module` assertions, `internalKeyFor`/types/deps expectations, deps-endpoint and search assertions, DOM `data-test-card` selectors, and prompt/error-message expectations across host, realm-server, ai-bot, matrix, boxel-cli, eslint-plugin-boxel, software-factory, and runtime-common tests. TypeScript module specifiers (`from '...'`, `typeof import('...')`) are intentionally left in URL form: they resolve through pnpm/node and flipping them breaks the dual-type-identity guarantee. Co-Authored-By: Claude Opus 4.7 --- .../ai-bot/tests/prompt-construction-test.ts | 28 +-- .../tests/integration/file-delete.test.ts | 4 +- .../tests/integration/file-lint.test.ts | 12 +- .../tests/integration/file-read.test.ts | 2 +- .../tests/integration/file-touch.test.ts | 2 +- .../tests/integration/file-write.test.ts | 2 +- .../tests/integration/search.test.ts | 4 +- .../lib/rules/missing-card-api-import-test.js | 60 +++--- .../lib/rules/no-literal-realm-urls-test.js | 4 +- .../tests/acceptance/ai-assistant-test.gts | 2 +- .../audio-def/flac-audio-def-test.gts | 4 +- .../audio-def/m4a-audio-def-test.gts | 4 +- .../audio-def/mp3-audio-def-test.gts | 4 +- .../audio-def/ogg-audio-def-test.gts | 4 +- .../audio-def/wav-audio-def-test.gts | 4 +- .../tests/acceptance/code-submode-test.ts | 8 +- .../code-submode/card-playground-test.gts | 2 +- .../code-submode/create-file-test.gts | 72 ++++--- .../acceptance/code-submode/editor-test.ts | 4 +- .../code-submode/field-playground-test.gts | 14 +- .../acceptance/code-submode/file-tree-test.ts | 4 +- .../acceptance/code-submode/inspector-test.ts | 64 +++--- .../code-submode/recent-files-test.ts | 4 +- .../code-submode/schema-editor-test.ts | 14 +- .../acceptance/code-submode/spec-test.gts | 26 +-- .../tests/acceptance/csv-file-def-test.gts | 4 +- .../tests/acceptance/gts-file-def-test.gts | 4 +- .../host/tests/acceptance/host-mode-test.gts | 2 +- .../image-def/avif-image-def-test.gts | 4 +- .../image-def/gif-image-def-test.gts | 4 +- .../image-def/jpg-image-def-test.gts | 4 +- .../image-def/png-image-def-test.gts | 4 +- .../image-def/svg-image-def-test.gts | 4 +- .../image-def/webp-image-def-test.gts | 4 +- ...-submode-creation-and-permissions-test.gts | 14 +- .../interact-submode/create-file-test.gts | 12 +- .../tests/acceptance/json-file-def-test.gts | 4 +- .../acceptance/markdown-file-def-test.gts | 4 +- .../operator-mode-acceptance-test.gts | 6 +- .../tests/acceptance/prerender-meta-test.gts | 3 +- .../acceptance/prerender-module-test.gts | 3 +- .../tests/acceptance/text-file-def-test.gts | 4 +- .../host/tests/acceptance/theme-card-test.gts | 12 +- .../tests/acceptance/ts-file-def-test.gts | 4 +- .../workspace-chooser-delete-test.gts | 8 +- .../acceptance/workspace-chooser-test.gts | 4 +- packages/host/tests/helpers/index.gts | 12 +- ...-field-to-card-definition-command-test.gts | 4 +- .../commands/copy-and-edit-test.gts | 2 +- .../integration/commands/copy-card-test.gts | 2 +- .../integration/commands/store-add-test.gts | 2 +- .../commands/use-ai-assistant-test.gts | 4 +- .../fallback-banner-test.gts | 2 +- .../integration/components/card-copy-test.gts | 8 +- .../components/card-delete-test.gts | 2 +- .../field-markdown-specialized-test.gts | 4 +- .../components/overlay-menu-items-test.gts | 2 +- .../serialization-rri-form-audit-test.gts | 193 ++++++++++++++++++ .../tests/integration/realm-indexing-test.gts | 35 ++-- .../host/tests/integration/realm-test.gts | 26 +-- .../host/tests/integration/store-test.gts | 6 +- .../tests/unit/index-query-engine-test.ts | 8 +- .../unit/instance-filter-matcher-test.ts | 1 + packages/host/tests/unit/loader-test.ts | 5 + packages/matrix/tests/commands.spec.ts | 6 +- .../matrix/tests/correctness-checks.spec.ts | 14 +- packages/matrix/tests/host-mode.spec.ts | 2 +- packages/matrix/tests/live-cards.spec.ts | 6 +- .../tests/card-dependencies-endpoint-test.ts | 4 +- .../realm-server/tests/card-endpoints-test.ts | 8 +- .../tests/definition-lookup-test.ts | 2 +- .../tests/eq-containment-integration-test.ts | 6 +- packages/realm-server/tests/helpers/index.ts | 12 +- packages/realm-server/tests/indexing-test.ts | 6 +- .../realm-server/tests/lazy-mount-test.ts | 4 +- ...module-cache-invalidation-listener-test.ts | 10 +- .../tests/module-cache-race-test.ts | 4 +- .../realm-server/tests/module-syntax-test.ts | 38 ++-- .../realm-server/tests/prerendering-test.ts | 14 +- .../tests/publish-unpublish-realm-test.ts | 6 +- .../tests/realm-endpoints-test.ts | 7 +- .../realm-endpoints/dependencies-test.ts | 4 +- .../tests/realm-endpoints/lint-test.ts | 4 +- .../tests/realm-endpoints/search-test.ts | 11 +- .../tests/realm-file-changes-listener-test.ts | 11 +- .../tests/realm-identifiers-test.ts | 31 ++- .../tests/realm-registry-backfill-test.ts | 20 +- .../tests/realm-registry-reconciler-test.ts | 18 +- .../tests/realm-registry-writes-test.ts | 4 +- .../realm-server/tests/realm-routing-test.ts | 2 +- .../tests/search-prerendered-test.ts | 14 +- .../server-endpoints/download-realm-test.ts | 2 +- .../server-endpoints/federated-types-test.ts | 2 +- .../server-endpoints/index-responses-test.ts | 8 +- .../server-endpoints/realm-lifecycle-test.ts | 8 +- .../run-command-endpoint-test.ts | 2 +- .../search-prerendered-test.ts | 4 +- .../tests/server-endpoints/search-test.ts | 4 +- .../realm-server/tests/types-endpoint-test.ts | 2 +- .../tests/virtual-network-test.ts | 4 +- .../tests/consuming-realm-header-test.ts | 4 +- .../tests/normalize-realm-meta-value-test.ts | 2 +- .../tests/package-shim-handler-test.ts | 4 +- .../realm/kanban-board.test.gts | 2 +- .../helpers/instantiate-test-fixtures.ts | 4 +- .../tests/parse-validation.spec.ts | 2 +- .../tests/run-parse-in-memory.spec.ts | 2 +- .../contents/file-query-card.gts | 2 +- 108 files changed, 671 insertions(+), 437 deletions(-) create mode 100644 packages/host/tests/integration/components/serialization-rri-form-audit-test.gts diff --git a/packages/ai-bot/tests/prompt-construction-test.ts b/packages/ai-bot/tests/prompt-construction-test.ts index 59b084a9229..41c680cb26e 100644 --- a/packages/ai-bot/tests/prompt-construction-test.ts +++ b/packages/ai-bot/tests/prompt-construction-test.ts @@ -233,7 +233,7 @@ Current date and time: 2025-06-11T11:43:00.533Z }, { codeRef: { - module: rri('https://cardstack.com/base/card-api'), + module: rri('@cardstack/base/card-api'), name: 'CardDef', }, fields: [], @@ -292,7 +292,7 @@ File open in code editor: http://localhost:4201/experiments/author.gts Inheritance chain: 1. Address from http://localhost:4201/experiments/author Fields: street, city, state - 2. CardDef from https://cardstack.com/base/card-api + 2. CardDef from @cardstack/base/card-api Selected text: lines 10-12 (1-based), columns 5-20 (1-based) Note: Line numbers in selection refer to the original file. Attached file contents below show line numbers for reference. Module inspector panel: preview @@ -1945,7 +1945,7 @@ Attached Files (files with newer versions don't show their content): text: JSON.stringify({ data: { type: 'card', - id: 'https://cardstack.com/base/Skill/card-editing', + id: '@cardstack/base/Skill/card-editing', attributes: { instructions: '- If the user wants the data they see edited, AND the patchCardInstance function is available, you MUST use the "patchCardInstance" function to make the change.\n- If the user wants the data they see edited, AND the patchCardInstance function is NOT available, you MUST ask the user to open the card and share it with you.\n- If you do not call patchCardInstance, the user will not see the change.\n- You can ONLY modify cards shared with you. If there is no patchCardInstance function or tool, then the user hasn\'t given you access.\n- NEVER tell the user to use patchCardInstance; you should always do it for them.\n- If the user wants to search for a card instance, AND the "searchCard" function is available, you MUST use the "searchCard" function to find the card instance.\nOnly recommend one searchCard function at a time.\nIf the user wants to edit a field of a card, you can optionally use "searchCard" to help find a card instance that is compatible with the field being edited before using "patchCardInstance" to make the change of the field.\n You MUST confirm with the user the correct choice of card instance that he intends to use based upon the results of the search.', @@ -1991,7 +1991,7 @@ Attached Files (files with newer versions don't show their content): assert.true(systemPromptText.includes(SKILL_INSTRUCTIONS_MESSAGE)); assert.true( systemPromptText.includes( - 'Skill (id: https://cardstack.com/base/Skill/card-editing, title: Card Editing):', + 'Skill (id: @cardstack/base/Skill/card-editing, title: Card Editing):', ), 'includes skill title metadata when present', ); @@ -2024,7 +2024,7 @@ Attached Files (files with newer versions don't show their content): text: JSON.stringify({ data: { type: 'card', - id: 'https://cardstack.com/base/Skill/card-editing', + id: '@cardstack/base/Skill/card-editing', attributes: { instructions: '- If the user wants the data they see edited, AND the patchCardInstance function is available, you MUST use the "patchCardInstance" function to make the change.\n- If the user wants the data they see edited, AND the patchCardInstance function is NOT available, you MUST ask the user to open the card and share it with you.\n- If you do not call patchCardInstance, the user will not see the change.\n- You can ONLY modify cards shared with you. If there is no patchCardInstance function or tool, then the user hasn\'t given you access.\n- NEVER tell the user to use patchCardInstance; you should always do it for them.\n- If the user wants to search for a card instance, AND the "searchCard" function is available, you MUST use the "searchCard" function to find the card instance.\nOnly recommend one searchCard function at a time.\nIf the user wants to edit a field of a card, you can optionally use "searchCard" to help find a card instance that is compatible with the field being edited before using "patchCardInstance" to make the change of the field.\n You MUST confirm with the user the correct choice of card instance that he intends to use based upon the results of the search.', @@ -2243,7 +2243,7 @@ Attached Files (files with newer versions don't show their content): text: JSON.stringify({ data: { type: 'card', - id: 'https://cardstack.com/base/Skill/card-editing', + id: '@cardstack/base/Skill/card-editing', attributes: { instructions: '- If the user wants the data they see edited, AND the patchCardInstance function is available, you MUST use the "patchCardInstance" function to make the change.\n- If the user wants the data they see edited, AND the patchCardInstance function is NOT available, you MUST ask the user to open the card and share it with you.\n- If you do not call patchCardInstance, the user will not see the change.\n- You can ONLY modify cards shared with you. If there is no patchCardInstance function or tool, then the user hasn\'t given you access.\n- NEVER tell the user to use patchCardInstance; you should always do it for them.\n- If the user wants to search for a card instance, AND the "searchCard" function is available, you MUST use the "searchCard" function to find the card instance.\nOnly recommend one searchCard function at a time.\nIf the user wants to edit a field of a card, you can optionally use "searchCard" to help find a card instance that is compatible with the field being edited before using "patchCardInstance" to make the change of the field.\n You MUST confirm with the user the correct choice of card instance that he intends to use based upon the results of the search.', @@ -2668,7 +2668,7 @@ Attached Files (files with newer versions don't show their content): }, meta: { adoptsFrom: { - module: 'https://cardstack.com/base/search-results', + module: '@cardstack/base/search-results', name: 'SearchResults', }, }, @@ -2708,7 +2708,7 @@ Attached Files (files with newer versions don't show their content): ); assert.equal(result[5].role, 'tool'); assert.equal(result[5].tool_call_id, 'tool-call-id-1'); - const expected = `Tool call executed, with result card: {"data":{"type":"card","attributes":{"title":"Search Results","description":"Here are the search results","results":[{"data":{"type":"card","id":"http://localhost:4201/drafts/Author/1","attributes":{"firstName":"Alice","lastName":"Enwunder","photo":null,"body":"Alice is a software engineer at Google.","description":null,"thumbnailURL":null},"meta":{"adoptsFrom":{"module":"../author","name":"Author"}}}}]},"meta":{"adoptsFrom":{"module":"https://cardstack.com/base/search-results","name":"SearchResults"}}}}.`; + const expected = `Tool call executed, with result card: {"data":{"type":"card","attributes":{"title":"Search Results","description":"Here are the search results","results":[{"data":{"type":"card","id":"http://localhost:4201/drafts/Author/1","attributes":{"firstName":"Alice","lastName":"Enwunder","photo":null,"body":"Alice is a software engineer at Google.","description":null,"thumbnailURL":null},"meta":{"adoptsFrom":{"module":"../author","name":"Author"}}}}]},"meta":{"adoptsFrom":{"module":"@cardstack/base/search-results","name":"SearchResults"}}}}.`; assert.equal((result[5].content as string).trim(), expected.trim()); }); @@ -2849,7 +2849,7 @@ Attached Files (files with newer versions don't show their content): text: JSON.stringify({ data: { type: 'card', - id: 'https://cardstack.com/base/Skill/skill_card_v1', + id: '@cardstack/base/Skill/skill_card_v1', attributes: { instructions: 'Test skill instructions', title: 'Test Skill', @@ -2868,7 +2868,7 @@ Attached Files (files with newer versions don't show their content): text: JSON.stringify({ data: { type: 'card', - id: 'https://cardstack.com/base/Skill/skill_card_v2', + id: '@cardstack/base/Skill/skill_card_v2', attributes: { instructions: 'Test skill instructions with updated commands', commands: [ @@ -3093,7 +3093,7 @@ Attached Files (files with newer versions don't show their content): text: JSON.stringify({ data: { type: 'card', - id: 'https://cardstack.com/base/Skill/card-editing', + id: '@cardstack/base/Skill/card-editing', attributes: { instructions: '- If the user wants the data they see edited, AND the patchCardInstance function is available, you MUST use the "patchCardInstance" function to make the change.\n- If the user wants the data they see edited, AND the patchCardInstance function is NOT available, you MUST ask the user to open the card and share it with you.\n- If you do not call patchCardInstance, the user will not see the change.\n- You can ONLY modify cards shared with you. If there is no patchCardInstance function or tool, then the user hasn\'t given you access.\n- NEVER tell the user to use patchCardInstance; you should always do it for them.\n- If the user wants to search for a card instance, AND the "searchCard" function is available, you MUST use the "searchCard" function to find the card instance.\nOnly recommend one searchCard function at a time.\nIf the user wants to edit a field of a card, you can optionally use "searchCard" to help find a card instance that is compatible with the field being edited before using "patchCardInstance" to make the change of the field.\n You MUST confirm with the user the correct choice of card instance that he intends to use based upon the results of the search.', @@ -3269,7 +3269,7 @@ Current date and time: 2025-06-11T11:43:00.533Z text: JSON.stringify({ data: { type: 'card', - id: 'https://cardstack.com/base/Skill/skill_card_v1', + id: '@cardstack/base/Skill/skill_card_v1', attributes: { instructions: 'Test skill instructions', title: 'Test Skill', @@ -3288,7 +3288,7 @@ Current date and time: 2025-06-11T11:43:00.533Z text: JSON.stringify({ data: { type: 'card', - id: 'https://cardstack.com/base/Skill/skill_card_v2', + id: '@cardstack/base/Skill/skill_card_v2', attributes: { instructions: 'Test skill instructions with updated commands', commands: [ @@ -3439,7 +3439,7 @@ Current date and time: 2025-06-11T11:43:00.533Z text: JSON.stringify({ data: { type: 'card', - id: 'https://cardstack.com/base/Skill/skill_card_v1', + id: '@cardstack/base/Skill/skill_card_v1', attributes: { instructions: 'Test skill instructions', title: 'Test Skill', diff --git a/packages/boxel-cli/tests/integration/file-delete.test.ts b/packages/boxel-cli/tests/integration/file-delete.test.ts index 7f81a6abdb8..7a8eeedff48 100644 --- a/packages/boxel-cli/tests/integration/file-delete.test.ts +++ b/packages/boxel-cli/tests/integration/file-delete.test.ts @@ -28,7 +28,7 @@ beforeAll(async () => { attributes: { title: 'Keep' }, meta: { adoptsFrom: { - module: 'https://cardstack.com/base/card-api', + module: '@cardstack/base/card-api', name: 'CardDef', }, }, @@ -40,7 +40,7 @@ beforeAll(async () => { attributes: { title: 'Delete' }, meta: { adoptsFrom: { - module: 'https://cardstack.com/base/card-api', + module: '@cardstack/base/card-api', name: 'CardDef', }, }, diff --git a/packages/boxel-cli/tests/integration/file-lint.test.ts b/packages/boxel-cli/tests/integration/file-lint.test.ts index 5219fbf8df4..788875b9280 100644 --- a/packages/boxel-cli/tests/integration/file-lint.test.ts +++ b/packages/boxel-cli/tests/integration/file-lint.test.ts @@ -60,7 +60,7 @@ describe('file lint (integration)', () => { }); it('returns fixed output for source with formatting issues', async () => { - let source = `import{CardDef}from 'https://cardstack.com/base/card-api'; + let source = `import{CardDef}from '@cardstack/base/card-api'; export class MyCard extends CardDef { @field name = contains(StringField); } @@ -77,7 +77,7 @@ export class MyCard extends CardDef { }); it('returns fixed output with proper single-quote formatting', async () => { - let source = `import { CardDef } from "https://cardstack.com/base/card-api"; + let source = `import { CardDef } from "@cardstack/base/card-api"; export class MyCard extends CardDef { @field name = contains(StringField); } @@ -88,11 +88,11 @@ export class MyCard extends CardDef { expect(result.fixed).toBe(true); expect(result.output).toBeDefined(); // Prettier should convert double quotes to single quotes - expect(result.output).toContain("'https://cardstack.com/base/card-api'"); + expect(result.output).toContain("'@cardstack/base/card-api'"); }); it('reports lint messages for unfixable issues', async () => { - let source = `import { CardDef } from 'https://cardstack.com/base/card-api'; + let source = `import { CardDef } from '@cardstack/base/card-api'; export class MyCard extends CardDef { }