Skip to content

Add support for @cardstack/catalog references#4058

Merged
backspace merged 12 commits intomainfrom
catalog-virtual-network-cs-10211
Feb 26, 2026
Merged

Add support for @cardstack/catalog references#4058
backspace merged 12 commits intomainfrom
catalog-virtual-network-cs-10211

Conversation

@backspace
Copy link
Contributor

@backspace backspace commented Feb 24, 2026

This lets the catalog be referenced with @cardstack/catalog instead of relying on relative references that break in published realms, or absolute references that break when working across environments.

Concretely, this means that boxel-home can use an import like import { SkillPlus } from '@cardstack/catalog/skill-plus'; instead of needing import { SkillPlus } from 'https://realms-staging.stack.cards/catalog/skill-plus'; that differs across environments, and the theme can be referenced like this:

 "cardInfo.theme": {
        "links": {
          "self": "@cardstack/catalog/Theme/boxel-brand-guide"
        }
      }

This removes the need for hacks like this.

Implementation

The major work here is a resolveCardReference function that wraps what were previously bare URL constructors, it converts @cardstack/catalog to whatever realm URL is correct for the catalog in the current environment, as specified in the startup script.

Open question

I left --fromUrl alone despite it now accepting @cardstack/catalog. Should it have a different name?

Migrating references in existing realms

The migration script dry run feature lets you see what changes are needed, and it saves a patch if you need to roll back (as I have when switching off this branch):

❯ scripts/migrate-realm-references.sh --dry-run -e development -r catalog realms
Resolved: http://localhost:4201/catalog/ -> @cardstack/catalog/
Scanning realms ...

  Would update: realms/localhost_4201/_published/48f92bba-edb8-461f-a5a9-ba4e05ea500e/boxel-ai-website/sections/FooterSection/footer.json
    -              "linkUrl": "/catalog/index"
    +              "linkUrl": "@cardstack/catalog/index"

  Would update: realms/localhost_4201/_published/48f92bba-edb8-461f-a5a9-ba4e05ea500e/boxel-ai-website/examples/brand-voice-skill.gts
    -import { SkillPlus } from 'http://localhost:4201/catalog/skill-plus';
    +import { SkillPlus } from '@cardstack/catalog/skill-plus';

  Would update: realms/localhost_4201/_published/48f92bba-edb8-461f-a5a9-ba4e05ea500e/boxel-ai-website/DocsLayout/docs-page.json
    -          "self": "http://localhost:4201/catalog/Theme/boxel-brand-guide"
    +          "self": "@cardstack/catalog/Theme/boxel-brand-guide"

  Would update: realms/localhost_4201/_published/48f92bba-edb8-461f-a5a9-ba4e05ea500e/boxel-ai-website/Site/boxel-site.json
    -          "self": "http://localhost:4201/catalog/Theme/boxel-brand-guide"
    +          "self": "@cardstack/catalog/Theme/boxel-brand-guide"
    -          "self": "http://localhost:4201/catalog/Theme/boxel-brand-guide"
    +          "self": "@cardstack/catalog/Theme/boxel-brand-guide"

  Would update: realms/localhost_4201/_published/48f92bba-edb8-461f-a5a9-ba4e05ea500e/index.json
    -          "self": "http://localhost:4201/catalog/Theme/boxel-brand-guide"
    +          "self": "@cardstack/catalog/Theme/boxel-brand-guide"

  Would update: realms/localhost_4201/user/homepage20260225/boxel-ai-website/sections/FooterSection/footer.json
    -              "linkUrl": "/catalog/index"
    +              "linkUrl": "@cardstack/catalog/index"

  Would update: realms/localhost_4201/user/homepage20260225/boxel-ai-website/examples/brand-voice-skill.gts
    -import { SkillPlus } from 'http://localhost:4201/catalog/skill-plus';
    +import { SkillPlus } from '@cardstack/catalog/skill-plus';

  Would update: realms/localhost_4201/user/homepage20260225/boxel-ai-website/DocsLayout/docs-page.json
    -          "self": "http://localhost:4201/catalog/Theme/boxel-brand-guide"
    +          "self": "@cardstack/catalog/Theme/boxel-brand-guide"

  Would update: realms/localhost_4201/user/homepage20260225/boxel-ai-website/Site/boxel-site.json
    -          "self": "http://localhost:4201/catalog/Theme/boxel-brand-guide"
    +          "self": "@cardstack/catalog/Theme/boxel-brand-guide"
    -          "self": "http://localhost:4201/catalog/Theme/boxel-brand-guide"
    +          "self": "@cardstack/catalog/Theme/boxel-brand-guide"

  Would update: realms/localhost_4201/user/homepage20260225/index.json
    -          "self": "http://localhost:4201/catalog/Theme/boxel-brand-guide"
    +          "self": "@cardstack/catalog/Theme/boxel-brand-guide"

Dry run complete. 10 file(s) would be updated.
Patch preview saved to: catalog-to-catalog.patch
  (this is a preview — no files were modified)

❯ ../../../boxel-motion/packages/realm-server/scripts/migrate-realm-references.sh --dry-run -e production -r catalog ./
Resolved: https://app.boxel.ai/catalog/ -> @cardstack/catalog/
Scanning ./ ...

  Would update: ./IkeaProduct/solglim-modular-sofa.json
    -          "self": "https://app.boxel.ai/catalog/Theme/cardstack"
    +          "self": "@cardstack/catalog/Theme/cardstack"

  Would update: ./IkeaProduct/ljus-arc-floor-lamp.json
    -          "self": "https://app.boxel.ai/catalog/Theme/cardstack"
    +          "self": "@cardstack/catalog/Theme/cardstack"

  Would update: ./IkeaProduct/norra-oak-dining-table.json
    -          "self": "https://app.boxel.ai/catalog/Theme/cardstack"
    +          "self": "@cardstack/catalog/Theme/cardstack"

  Would update: ./IkeaProduct/loom-tall-bookshelf.json
    -          "self": "https://app.boxel.ai/catalog/Theme/cardstack"
    +          "self": "@cardstack/catalog/Theme/cardstack"

  Would update: ./garden-app.gts
    -// @ts-expect-error: Module '/catalog/app-card' may not be available during compilation
    +// @ts-expect-error: Module '@cardstack/catalog/app-card' may not be available during compilation

  Would update: ./ProductCatalog/main.json
    -          "self": "https://app.boxel.ai/catalog/Theme/cardstack"
    +          "self": "@cardstack/catalog/Theme/cardstack"

Dry run complete. 6 file(s) would be updated.
Patch preview saved to: catalog-to-catalog.patch
  (this is a preview — no files were modified)

@github-actions
Copy link

Preview deployments

@github-actions
Copy link

github-actions bot commented Feb 24, 2026

Host Test Results

    1 files  ± 0      1 suites  ±0   1h 47m 59s ⏱️ + 18m 0s
1 896 tests +33  1 881 ✅ +34  15 💤 ±0  0 ❌ ±0 
1 911 runs  +33  1 896 ✅ +35  15 💤 ±0  0 ❌  - 1 

Results for commit 3237e93. ± Comparison against base commit 13b3155.

This pull request removes 1 and adds 34 tests. Note that renamed tests count towards both.
Chrome ‑ Acceptance | code submode | create-file tests > when user has permissions to both test realms: new file button has options to create card def, field def, card instance, and text files
Chrome ‑ Acceptance | code submode tests > single realm: clicking "metadata" format for a non-card file shows metadata panel with JSON-API content
Chrome ‑ Acceptance | code submode tests > single realm: non-card file preview shows "metadata" format option and not "edit"
Chrome ‑ Acceptance | code submode tests > single realm: switching away from "metadata" format hides the metadata panel
Chrome ‑ Acceptance | code submode | create-file tests > when user has permissions to both test realms: can upload a file via the New menu
Chrome ‑ Acceptance | code submode | create-file tests > when user has permissions to both test realms: cancelling upload file picker does not cause errors
Chrome ‑ Acceptance | code submode | create-file tests > when user has permissions to both test realms: new file button has options to create card def, field def, card instance, text files, and upload file
Chrome ‑ Acceptance | code submode | editor tests: text file in writable realm is not read-only
Chrome ‑ Acceptance | code submode | file-tree tests > when the user lacks write permissions: file tree does not show context menu in read-only realm
Chrome ‑ Acceptance | code submode | file-tree tests: can cancel delete from file tree context menu
Chrome ‑ Acceptance | code submode | file-tree tests: can delete a file from file tree context menu
…

♻️ This comment has been updated with latest results.

@backspace backspace marked this pull request as ready for review February 26, 2026 18:43
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3237e93132

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@backspace backspace requested a review from a team February 26, 2026 18:58
return new URL(rest, withTrailingSlash(config.resolvedCatalogRealmURL))
.href;
});
if (config.resolvedCatalogRealmURL) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s missing when SKIP_CATALOG is present.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds environment-independent @cardstack/catalog/... references by introducing a central resolver and registering prefix→URL mappings at startup, so realms and imports no longer need hard-coded absolute URLs.

Changes:

  • Introduce resolveCardReference/registerCardReferencePrefix and apply it broadly where URLs were previously constructed directly.
  • Update realm-server startup scripts and runtime wiring to register @cardstack/catalog/ mappings.
  • Migrate existing realm content to use @cardstack/catalog/... references.

Reviewed changes

Copilot reviewed 45 out of 45 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/runtime-common/serializers/code-ref.ts Resolve prefixed/bare module refs via resolveCardReference during serialization.
packages/runtime-common/resource-types.ts Resolve relationship IDs using resolveCardReference.
packages/runtime-common/realm.ts Resolve module dependency URLs via resolveCardReference.
packages/runtime-common/realm-index-query-engine.ts Use resolveCardReference when absolutizing/relativizing links and module deps.
packages/runtime-common/module-syntax.ts Resolve module URLs via resolveCardReference when inspecting/updating modules.
packages/runtime-common/index.ts Export card reference resolver and use it in internalKeyFor.
packages/runtime-common/index-runner/dependency-url.ts Canonicalize dependency URLs after resolving card references.
packages/runtime-common/index-runner.ts Resolve adoptsFrom module dependency URLs via resolveCardReference.
packages/runtime-common/file-serializer.ts Resolve self links via resolveCardReference before relativizing.
packages/runtime-common/file-def-code-ref.ts Resolve file def code refs via resolveCardReference.
packages/runtime-common/code-ref.ts Use resolveCardReference when normalizing/loading code refs and in error messaging.
packages/runtime-common/card-reference-resolver.ts New prefix mapping registry + reference resolver for @cardstack/...-style references.
packages/realm-server/worker.ts Register prefix mappings (non-URL --fromUrl) and import maps for workers.
packages/realm-server/worker-manager.ts Allow non-URL --fromUrl values to flow through to workers.
packages/realm-server/scripts/start-worker-staging.sh Use @cardstack/catalog/ as the catalog --fromUrl.
packages/realm-server/scripts/start-worker-production.sh Use @cardstack/catalog/ as the catalog --fromUrl.
packages/realm-server/scripts/start-worker-development.sh Use @cardstack/catalog/ as the catalog --fromUrl.
packages/realm-server/scripts/start-staging.sh Use @cardstack/catalog/ as the catalog --fromUrl.
packages/realm-server/scripts/start-production.sh Use @cardstack/catalog/ as the catalog --fromUrl.
packages/realm-server/scripts/start-development.sh Use @cardstack/catalog/ as the catalog --fromUrl.
packages/realm-server/scripts/migrate-realm-references.sh New script to migrate realm references (URL ↔ prefix) with patch output.
packages/realm-server/main.ts Register prefix mappings/import maps for non-URL --fromUrl values at server startup.
packages/host/app/services/store.ts Resolve relationship self links via resolveCardReference when loading instances.
packages/host/app/services/network.ts Register @cardstack/catalog/ prefix mapping in the host virtual network.
packages/host/app/lib/prerender-util.ts Resolve module deps via resolveCardReference for prerender dependency discovery.
packages/host/app/components/operator-mode/edit-field-modal.gts Resolve module URLs via resolveCardReference before constructing URL.
packages/host/app/components/operator-mode/code-submode/module-inspector.gts Resolve adoptsFrom module URLs via resolveCardReference for file def creation.
packages/experiments-realm/SkillPlus/4e9f1f88-ef64-44e4-bcf0-78ab9fbeedf8.json Replace catalog absolute URLs with @cardstack/catalog/....
packages/experiments-realm/ProductCatalog/main.json Replace catalog absolute theme link with @cardstack/catalog/....
packages/experiments-realm/IkeaProduct/solglim-modular-sofa.json Replace catalog absolute theme link with @cardstack/catalog/....
packages/experiments-realm/IkeaProduct/norra-oak-dining-table.json Replace catalog absolute theme link with @cardstack/catalog/....
packages/experiments-realm/IkeaProduct/loom-tall-bookshelf.json Replace catalog absolute theme link with @cardstack/catalog/....
packages/experiments-realm/IkeaProduct/ljus-arc-floor-lamp.json Replace catalog absolute theme link with @cardstack/catalog/....
packages/catalog-realm/Spec/16320d37-fac6-417c-ae20-c583ea0c8a2e.json Replace catalog absolute module URL with @cardstack/catalog/....
packages/catalog-realm/GameResult/e7cfd2bd-8306-4b58-aba0-afc05c6d18ef.json Replace catalog absolute module URL with @cardstack/catalog/....
packages/catalog-realm/GameResult/d3e487ac-074f-404a-a63e-b41b83df24d7.json Replace catalog absolute module URL with @cardstack/catalog/....
packages/catalog-realm/GameResult/c6e7cfd2-bd83-465b-982b-a0afc05c6d18.json Replace catalog absolute module URL with @cardstack/catalog/....
packages/catalog-realm/GameResult/c05c6d18-ef0a-40d3-a487-ac074f004a66.json Replace catalog absolute module URL with @cardstack/catalog/....
packages/catalog-realm/GameResult/5c6d18ef-0a50-43e4-87ac-074f004a663e.json Replace catalog absolute module URL with @cardstack/catalog/....
packages/catalog-realm/GameResult/5b582ba0-afc0-4c6d-98ef-0a50d3e487ac.json Replace catalog absolute module URL with @cardstack/catalog/....
packages/catalog-realm/GameResult/50d3e487-ac07-4f00-8a66-3eb41b83df24.json Replace catalog absolute module URL with @cardstack/catalog/....
packages/catalog-realm/GameResult/14c25556-2c52-42ff-ab5f-cb790140a3d0.json Replace catalog absolute module URL with @cardstack/catalog/....
packages/catalog-realm/GameResult/065b582b-a0af-405c-ad18-ef0a50d3e487.json Replace catalog absolute module URL with @cardstack/catalog/....
packages/base/spec.gts Resolve spec module refs via resolveCardReference.
packages/base/card-api.gts Resolve link references via resolveCardReference throughout link field logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +3 to +7
export function registerCardReferencePrefix(
prefix: string,
targetURL: string,
): void {
prefixMappings.set(prefix, targetURL);
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registerCardReferencePrefix stores targetURL as-is, but resolveCardReference uses new URL(rest, target). If targetURL is missing a trailing slash (e.g. https://app.boxel.ai/catalog), resolving @cardstack/catalog/skill-plus will incorrectly become https://app.boxel.ai/skill-plus. Normalize targetURL (ensure trailing /) when registering prefixes, or enforce/validate this invariant with a clear error.

Copilot uses AI. Check for mistakes.
Comment on lines +208 to +210
registerCardReferencePrefix(from, to.href);
virtualNetwork.addImportMap(from, (rest) => new URL(rest, to).href);
urlMappings.push([to, to]); // use toUrl for both in hrefs
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When registering a non-URL prefix mapping, to.href must behave as a base URL for new URL(rest, to). If the provided --toUrl lacks a trailing slash, imports/links will resolve to the parent path (dropping the realm segment). Normalize to to a trailing-slash base before calling registerCardReferencePrefix and addImportMap.

Suggested change
registerCardReferencePrefix(from, to.href);
virtualNetwork.addImportMap(from, (rest) => new URL(rest, to).href);
urlMappings.push([to, to]); // use toUrl for both in hrefs
// Ensure `to` behaves as a base URL for `new URL(rest, to)` by normalizing it
// to have a trailing slash when used for non-URL prefix mappings.
const normalizedTo = to.href.endsWith('/') ? to : new URL(to.href + '/');
registerCardReferencePrefix(from, normalizedTo.href);
virtualNetwork.addImportMap(from, (rest) => new URL(rest, normalizedTo).href);
urlMappings.push([normalizedTo, normalizedTo]); // use normalized toUrl for both in hrefs

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +110
if (isUrlLike(from)) {
virtualNetwork.addURLMapping(new URL(from), to);
} else {
registerCardReferencePrefix(from, to.href);
virtualNetwork.addImportMap(from, (rest) => new URL(rest, to).href);
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new URL(rest, to) requires to to be a trailing-slash base URL. If --toUrl is configured without a trailing slash, resolving @cardstack/catalog/... will drop the realm path segment. Normalize to (ensure it ends with /) before registering the prefix mapping and import map.

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +170
matching_files=$(grep -rlE "${FIND_STR}|[\"']${REALM_PATH}" "$search_dir" --include='*.json' --include='*.gts' 2>/dev/null || true)
else
matching_files=$(grep -rl "${FIND_STR}" "$search_dir" --include='*.json' --include='*.gts' 2>/dev/null || true)
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

grep -rlE "${FIND_STR}|..." treats FIND_STR as a regex. For URL inputs this contains . and other metacharacters, so it can match unintended strings. Prefer fixed-string matching for the full URL (e.g. grep -F) and only use regex where necessary (like the quoted path-only form).

Suggested change
matching_files=$(grep -rlE "${FIND_STR}|[\"']${REALM_PATH}" "$search_dir" --include='*.json' --include='*.gts' 2>/dev/null || true)
else
matching_files=$(grep -rl "${FIND_STR}" "$search_dir" --include='*.json' --include='*.gts' 2>/dev/null || true)
matching_files_url=$(grep -rlF "${FIND_STR}" "$search_dir" --include='*.json' --include='*.gts' 2>/dev/null || true)
matching_files_path=$(grep -rlE "[\"']${REALM_PATH}" "$search_dir" --include='*.json' --include='*.gts' 2>/dev/null || true)
matching_files=$(printf '%s\n%s\n' "$matching_files_url" "$matching_files_path" | sed '/^$/d' | sort -u | tr '\n' ' ')
else
matching_files=$(grep -rlF "${FIND_STR}" "$search_dir" --include='*.json' --include='*.gts' 2>/dev/null || true)

Copilot uses AI. Check for mistakes.
Comment on lines +208 to +211
registerCardReferencePrefix(from, to.href);
virtualNetwork.addImportMap(from, (rest) => new URL(rest, to).href);
urlMappings.push([to, to]); // use toUrl for both in hrefs
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For non-URL prefix mappings you currently do urlMappings.push([to, to]), which makes the later ‘Additional URL mappings’ log output print as to => to (dropping the original prefix like @cardstack/catalog/). Consider tracking prefix mappings separately (or at least logging them) so operators can verify the effective mapping configuration.

Copilot uses AI. Check for mistakes.
Comment on lines +180 to +185
if [ "$IS_URL" = true ]; then
SED_ARGS=(-e "s|${FIND_STR}|${REPLACEMENT}|g"
-e "s|${DQ}${REALM_PATH}|${DQ}${REPLACEMENT}|g"
-e "s|'${REALM_PATH}|'${REPLACEMENT}|g")
else
SED_ARGS=(-e "s|${FIND_STR}|${REPLACEMENT}|g")
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sed substitutions use s|${FIND_STR}|...|g, which interprets FIND_STR as a regex (so . in URLs matches any char). Escape the find string before building the sed expressions, or use a replacement approach that performs literal string substitution to avoid unintended edits.

Suggested change
if [ "$IS_URL" = true ]; then
SED_ARGS=(-e "s|${FIND_STR}|${REPLACEMENT}|g"
-e "s|${DQ}${REALM_PATH}|${DQ}${REPLACEMENT}|g"
-e "s|'${REALM_PATH}|'${REPLACEMENT}|g")
else
SED_ARGS=(-e "s|${FIND_STR}|${REPLACEMENT}|g")
# Escape FIND_STR and REALM_PATH so sed treats them as literal strings.
ESCAPED_FIND_STR=$(printf '%s\n' "$FIND_STR" | sed -e 's/[.[\*^$\/&|(){}+?]/\\&/g')
if [ "$IS_URL" = true ]; then
ESCAPED_REALM_PATH=$(printf '%s\n' "$REALM_PATH" | sed -e 's/[.[\*^$\/&|(){}+?]/\\&/g')
SED_ARGS=(-e "s|${ESCAPED_FIND_STR}|${REPLACEMENT}|g"
-e "s|${DQ}${ESCAPED_REALM_PATH}|${DQ}${REPLACEMENT}|g"
-e "s|'${ESCAPED_REALM_PATH}|'${REPLACEMENT}|g")
else
SED_ARGS=(-e "s|${ESCAPED_FIND_STR}|${REPLACEMENT}|g")

Copilot uses AI. Check for mistakes.
@backspace backspace merged commit 778598b into main Feb 26, 2026
125 of 126 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants