diff --git a/packages/base/codemirror-editor.gts b/packages/base/codemirror-editor.gts index 461d5153f05..257063d3e4d 100644 --- a/packages/base/codemirror-editor.gts +++ b/packages/base/codemirror-editor.gts @@ -8,6 +8,7 @@ import { scheduleOnce } from '@ember/runloop'; import { eq } from '@cardstack/boxel-ui/helpers'; import { + cardIdToURL, resolveCardReference, trimJsonExtension, maybeRelativeURL, @@ -100,7 +101,11 @@ function makeCardRef( ): string { if (!baseUrl) return cardUrl; try { - return maybeRelativeURL(new URL(cardUrl), new URL(baseUrl), undefined); + return maybeRelativeURL( + cardIdToURL(cardUrl), + cardIdToURL(baseUrl), + undefined, + ); } catch { return cardUrl; } diff --git a/packages/base/components/card-list.gts b/packages/base/components/card-list.gts index 5c1980227e7..ef7b7bb9738 100644 --- a/packages/base/components/card-list.gts +++ b/packages/base/components/card-list.gts @@ -10,6 +10,7 @@ import { LoadingIndicator } from '@cardstack/boxel-ui/components'; import { cn, eq } from '@cardstack/boxel-ui/helpers'; import { + cardIdToURL, removeFileExtension, CardCrudFunctionsContextName, type Query, @@ -43,7 +44,7 @@ export default class CardList extends Component { handleCardClick(cardUrl: string, event?: Event) { if (this.cardCrudFunctions?.viewCard) { event?.preventDefault(); - this.cardCrudFunctions.viewCard(new URL(cardUrl)); + this.cardCrudFunctions.viewCard(cardIdToURL(cardUrl)); } } diff --git a/packages/base/file-menu-items.ts b/packages/base/file-menu-items.ts index 6ea72f10480..c1b2331256d 100644 --- a/packages/base/file-menu-items.ts +++ b/packages/base/file-menu-items.ts @@ -12,6 +12,7 @@ import CopyFileToRealmCommand from '@cardstack/boxel-host/commands/copy-file-to- import OpenInInteractModeCommand from '@cardstack/boxel-host/commands/open-in-interact-mode'; import ShowFileCommand from '@cardstack/boxel-host/commands/show-file'; import SwitchSubmodeCommand from '@cardstack/boxel-host/commands/switch-submode'; +import { cardIdToURL } from '@cardstack/runtime-common'; import type { FileDef } from './card-api'; import type { GetMenuItemParams } from './menu-items'; @@ -81,7 +82,7 @@ export function getDefaultFileMenuItems( await new SwitchSubmodeCommand(params.commandContext).execute({ submode: 'code', codePath: fileDefInstanceId - ? new URL(fileDefInstanceId).href + ? cardIdToURL(fileDefInstanceId).href : undefined, }); }, diff --git a/packages/base/menu-items.ts b/packages/base/menu-items.ts index cf5b41a800d..3a2dce934dd 100644 --- a/packages/base/menu-items.ts +++ b/packages/base/menu-items.ts @@ -17,6 +17,7 @@ import type { ResolvedCodeRef, } from '@cardstack/runtime-common'; import { + cardIdToURL, cardTypeIcon, identifyCard, isRealmIndexCard, @@ -148,7 +149,7 @@ export function getDefaultCardMenuItems( action: async () => { await new SwitchSubmodeCommand(params.commandContext).execute({ submode: 'code', - codePath: cardId ? new URL(cardId).href : undefined, + codePath: cardId ? cardIdToURL(cardId).href : undefined, }); }, icon: CodeIcon, diff --git a/packages/base/skill-set.gts b/packages/base/skill-set.gts index af9dbc40902..2d8bfed91fe 100644 --- a/packages/base/skill-set.gts +++ b/packages/base/skill-set.gts @@ -6,6 +6,7 @@ import EditIcon from '@cardstack/boxel-icons/edit'; import FileTextIcon from '@cardstack/boxel-icons/file-text'; import { gt } from '@cardstack/boxel-ui/helpers'; +import { cardIdToURL } from '@cardstack/runtime-common'; import { SkillPlus, @@ -444,7 +445,7 @@ export class SkillSet extends SkillPlus { if (this.args.viewCard) { event.preventDefault(); event.stopPropagation(); - this.args.viewCard(new URL(cardUrl), 'isolated'); + this.args.viewCard(cardIdToURL(cardUrl), 'isolated'); } }; diff --git a/packages/base/system-card.gts b/packages/base/system-card.gts index c4a245208bc..4305e9081ce 100644 --- a/packages/base/system-card.gts +++ b/packages/base/system-card.gts @@ -10,7 +10,7 @@ import { import BooleanField from './boolean'; import StringField from './string'; import enumField from './enum'; -import { getMenuItems } from '@cardstack/runtime-common'; +import { cardIdToURL, getMenuItems } from '@cardstack/runtime-common'; import { type GetMenuItemParams } from './menu-items'; import { type MenuItemOptions, MenuItem } from '@cardstack/boxel-ui/helpers'; import SetUserSystemCardCommand from '@cardstack/boxel-host/commands/set-user-system-card'; @@ -214,7 +214,7 @@ class SystemCardIsolated extends Component { navigateToActive = async () => { if (this.activeSystemCardId && this.args.viewCard) { - await this.args.viewCard(new URL(this.activeSystemCardId), 'isolated'); + await this.args.viewCard(cardIdToURL(this.activeSystemCardId), 'isolated'); } }; diff --git a/packages/host/app/commands/add-field-to-card-definition.ts b/packages/host/app/commands/add-field-to-card-definition.ts index 80b431f9b21..9bd0b562a11 100644 --- a/packages/host/app/commands/add-field-to-card-definition.ts +++ b/packages/host/app/commands/add-field-to-card-definition.ts @@ -1,5 +1,6 @@ import { service } from '@ember/service'; +import { cardIdToURL } from '@cardstack/runtime-common'; import { ModuleSyntax } from '@cardstack/runtime-common/module-syntax'; import type { FieldType } from 'https://cardstack.com/base/card-api'; @@ -36,13 +37,13 @@ export default class AddFieldToCardDefinitionCommand extends HostBaseCommand< ): Promise { let moduleSource = ( await this.cardService.getSource( - new URL(input.cardDefinitionToModify.module), + cardIdToURL(input.cardDefinitionToModify.module), ) ).content; let moduleSyntax = new ModuleSyntax( moduleSource, - new URL(input.cardDefinitionToModify.module), + cardIdToURL(input.cardDefinitionToModify.module), ); moduleSyntax.addField({ @@ -52,13 +53,13 @@ export default class AddFieldToCardDefinitionCommand extends HostBaseCommand< fieldType: input.fieldType as FieldType, fieldDefinitionType: input.fieldDefinitionType as 'field' | 'card', incomingRelativeTo: input.incomingRelativeTo - ? new URL(input.incomingRelativeTo) + ? cardIdToURL(input.incomingRelativeTo) : undefined, outgoingRelativeTo: input.outgoingRelativeTo - ? new URL(input.outgoingRelativeTo) + ? cardIdToURL(input.outgoingRelativeTo) : undefined, outgoingRealmURL: input.outgoingRealmURL - ? new URL(input.outgoingRealmURL) + ? cardIdToURL(input.outgoingRealmURL) : undefined, addFieldAtIndex: input.addFieldAtIndex, computedFieldFunctionSourceCode: input.computedFieldFunctionSourceCode, diff --git a/packages/host/app/commands/check-correctness.ts b/packages/host/app/commands/check-correctness.ts index 4d4f644c85e..2def739df13 100644 --- a/packages/host/app/commands/check-correctness.ts +++ b/packages/host/app/commands/check-correctness.ts @@ -1,6 +1,7 @@ import { service } from '@ember/service'; import { + cardIdToURL, isCardDocumentString, isCardErrorJSONAPI, type CardErrorJSONAPI, @@ -192,14 +193,14 @@ export default class CheckCorrectnessCommand extends HostBaseCommand< targetRef: string, ): { moduleURL: URL; realmURL: URL; fileURL: URL } | undefined { try { - let fileURL = new URL(targetRef); + let fileURL = cardIdToURL(targetRef); let realmURL = this.realm.realmOfURL(fileURL); if (!realmURL) { return undefined; } let moduleHref = fileURL.href.replace(/\.gts$/, ''); - let moduleURL = new URL(moduleHref); + let moduleURL = cardIdToURL(moduleHref); return { moduleURL, realmURL, fileURL }; } catch { return undefined; @@ -263,7 +264,7 @@ export default class CheckCorrectnessCommand extends HostBaseCommand< private async isEmptyFileContent(targetRef: string): Promise { try { - let fileUrl = new URL(targetRef); + let fileUrl = cardIdToURL(targetRef); let { status, content } = await this.cardService.getSource(fileUrl); return status === 200 && content.trim() === ''; } catch { @@ -289,7 +290,7 @@ export default class CheckCorrectnessCommand extends HostBaseCommand< ): Promise { try { let { status, content } = await this.cardService.getSource( - new URL(fileUrl), + cardIdToURL(fileUrl), ); if (status !== 200) { return undefined; diff --git a/packages/host/app/commands/copy-file-to-realm.ts b/packages/host/app/commands/copy-file-to-realm.ts index b4f8d526257..24b9328eaf6 100644 --- a/packages/host/app/commands/copy-file-to-realm.ts +++ b/packages/host/app/commands/copy-file-to-realm.ts @@ -1,5 +1,7 @@ import { service } from '@ember/service'; +import { cardIdToURL } from '@cardstack/runtime-common'; + import type * as BaseCommandModule from 'https://cardstack.com/base/command'; import HostBaseCommand from '../lib/host-base-command'; @@ -39,12 +41,12 @@ export default class CopyFileToRealmCommand extends HostBaseCommand< throw new Error(`Do not have write permissions to ${targetRealm}`); } - let sourceUrl = new URL(input.sourceFileUrl); + let sourceUrl = cardIdToURL(input.sourceFileUrl); let filename = decodeURIComponent( sourceUrl.pathname.split('/').pop() ?? sourceUrl.pathname, ); - let destinationUrl = new URL(filename, targetRealm); + let destinationUrl = new URL(filename, cardIdToURL(targetRealm)); let existing = await this.cardService.getSource(destinationUrl); if (existing.status === 200 || existing.status === 406) { @@ -72,7 +74,9 @@ export default class CopyFileToRealmCommand extends HostBaseCommand< } private async fileExists(fileUrl: string): Promise { - let getSourceResult = await this.cardService.getSource(new URL(fileUrl)); + let getSourceResult = await this.cardService.getSource( + cardIdToURL(fileUrl), + ); return getSourceResult.status !== 404; } } diff --git a/packages/host/app/commands/copy-source.ts b/packages/host/app/commands/copy-source.ts index 0ccb4156dfa..d5b7f3a20ab 100644 --- a/packages/host/app/commands/copy-source.ts +++ b/packages/host/app/commands/copy-source.ts @@ -1,5 +1,7 @@ import { service } from '@ember/service'; +import { cardIdToURL } from '@cardstack/runtime-common'; + import type * as BaseCommandModule from 'https://cardstack.com/base/command'; import HostBaseCommand from '../lib/host-base-command'; @@ -25,8 +27,8 @@ export default class CopySourceCommand extends HostBaseCommand< protected async run( input: BaseCommandModule.CopySourceInput, ): Promise { - const originSourceUrl = new URL(input.originSourceUrl); - const destinationSourceUrl = new URL(input.destinationSourceUrl); + const originSourceUrl = cardIdToURL(input.originSourceUrl); + const destinationSourceUrl = cardIdToURL(input.destinationSourceUrl); let r = await this.cardService.copySource( originSourceUrl, destinationSourceUrl, diff --git a/packages/host/app/commands/create-specs.ts b/packages/host/app/commands/create-specs.ts index 05d6568f575..640627f1b0e 100644 --- a/packages/host/app/commands/create-specs.ts +++ b/packages/host/app/commands/create-specs.ts @@ -1,6 +1,7 @@ import { service } from '@ember/service'; import { + cardIdToURL, loadCardDef, specRef, type ResolvedCodeRef, @@ -259,7 +260,7 @@ export default class CreateSpecCommand extends HostBaseCommand< let url: string; if (codeRef) { - let relativeTo = new URL(targetRealm); + let relativeTo = cardIdToURL(targetRealm); let maybeAbsoluteRef = codeRefWithAbsoluteURL(codeRef, relativeTo); if (isResolvedCodeRef(maybeAbsoluteRef)) { codeRef = maybeAbsoluteRef; @@ -382,7 +383,7 @@ async function getSpecClassFromDeclaration( }; const loadedSpec = await loadCardDef(specCodeRef, { loader, - relativeTo: new URL(targetRealm), + relativeTo: cardIdToURL(targetRealm), }); if ( diff --git a/packages/host/app/commands/create-submission-workflow.ts b/packages/host/app/commands/create-submission-workflow.ts index b49cbd36e17..20466382b7c 100644 --- a/packages/host/app/commands/create-submission-workflow.ts +++ b/packages/host/app/commands/create-submission-workflow.ts @@ -1,6 +1,6 @@ import { service } from '@ember/service'; -import { isCardInstance } from '@cardstack/runtime-common'; +import { cardIdToURL, isCardInstance } from '@cardstack/runtime-common'; import type { LooseSingleCardDocument } from '@cardstack/runtime-common'; import type * as BaseCommandModule from 'https://cardstack.com/base/command'; @@ -60,7 +60,7 @@ export default class CreateSubmissionWorkflowCommand extends HostBaseCommand< // Save the workflow card in the user's realm (where the listing lives) let workflowRealm = - this.realm.realmOfURL(new URL(listingId))?.href ?? realm; + this.realm.realmOfURL(cardIdToURL(listingId))?.href ?? realm; // Step 1: Create the SubmissionWorkflowCard with listing linked let catalogRealm = this.catalogRealm; diff --git a/packages/host/app/commands/evaluate-module.ts b/packages/host/app/commands/evaluate-module.ts index e85aa44ba11..b185db7ad89 100644 --- a/packages/host/app/commands/evaluate-module.ts +++ b/packages/host/app/commands/evaluate-module.ts @@ -10,6 +10,8 @@ * Used by the software-factory's EvalValidationStep via `_run-command`. */ +import { cardIdToURL } from '@cardstack/runtime-common'; + import type * as BaseCommandModule from 'https://cardstack.com/base/command'; import HostBaseCommand from '../lib/host-base-command'; @@ -70,6 +72,8 @@ export default class EvaluateModuleCommand extends HostBaseCommand< * Throws if validation fails. */ private validateModuleUrl(moduleUrl: string, realmUrl: string): void { + moduleUrl = cardIdToURL(moduleUrl).href; + realmUrl = cardIdToURL(realmUrl).href; this.assertHttpOrHttpsUrl(moduleUrl, 'moduleUrl'); this.assertHttpOrHttpsUrl(realmUrl, 'realmUrl'); diff --git a/packages/host/app/commands/generate-example-cards.ts b/packages/host/app/commands/generate-example-cards.ts index 6f253d0cae2..2181abdf930 100644 --- a/packages/host/app/commands/generate-example-cards.ts +++ b/packages/host/app/commands/generate-example-cards.ts @@ -1,6 +1,7 @@ import { service } from '@ember/service'; import { + cardIdToURL, isCardInstance, type LooseSingleCardDocument, } from '@cardstack/runtime-common'; @@ -284,7 +285,7 @@ function resolveExampleCodeRef( return codeRef; } try { - const relativeTo = realm ? new URL(realm) : undefined; + const relativeTo = realm ? cardIdToURL(realm) : undefined; const resolved = codeRefWithAbsoluteURL( codeRef, relativeTo, diff --git a/packages/host/app/commands/instantiate-card.ts b/packages/host/app/commands/instantiate-card.ts index c6e597da4dd..c667eaa3fc2 100644 --- a/packages/host/app/commands/instantiate-card.ts +++ b/packages/host/app/commands/instantiate-card.ts @@ -16,6 +16,8 @@ import { service } from '@ember/service'; +import { cardIdToURL } from '@cardstack/runtime-common'; + import type * as BaseCommandModule from 'https://cardstack.com/base/command'; import HostBaseCommand from '../lib/host-base-command'; @@ -110,7 +112,7 @@ export default class InstantiateCardCommand extends HostBaseCommand< await this.store.__dangerousCreateFromSerialized( doc.data, doc, - resolveFrom ? new URL(resolveFrom) : undefined, + resolveFrom ? cardIdToURL(resolveFrom) : undefined, ); return new commandModule.InstantiateCardResult({ passed: true }); @@ -135,6 +137,8 @@ export default class InstantiateCardCommand extends HostBaseCommand< * Throws if validation fails. */ private validateModuleUrl(moduleUrl: string, realmUrl: string): void { + moduleUrl = cardIdToURL(moduleUrl).href; + realmUrl = cardIdToURL(realmUrl).href; this.assertHttpOrHttpsUrl(moduleUrl, 'moduleUrl'); this.assertHttpOrHttpsUrl(realmUrl, 'realmUrl'); diff --git a/packages/host/app/commands/listing-create.ts b/packages/host/app/commands/listing-create.ts index 7610ca5a4a1..4ecde96ea8c 100644 --- a/packages/host/app/commands/listing-create.ts +++ b/packages/host/app/commands/listing-create.ts @@ -7,6 +7,7 @@ import type { ResolvedCodeRef, } from '@cardstack/runtime-common'; import { + cardIdToURL, isCardInstance, SupportedMimeType, isFieldDef, @@ -79,7 +80,7 @@ export default class ListingCreateCommand extends HostBaseCommand< // "https://…/foo.gts" and "https://…/foo" don't produce separate entries. const seen = new Map(); // normalized → original for (const m of modulesToCreate) { - const normalized = trimExecutableExtension(new URL(m)).href; + const normalized = trimExecutableExtension(cardIdToURL(m)).href; if (!seen.has(normalized)) { seen.set(normalized, m); } @@ -102,7 +103,7 @@ export default class ListingCreateCommand extends HostBaseCommand< } // Only allow modulesToCreate that belong to a realm we can read - const url = new URL(dep); + const url = cardIdToURL(dep); const realmURL = this.realm.realmOfURL(url); if (!realmURL) { return false; @@ -256,7 +257,7 @@ export default class ListingCreateCommand extends HostBaseCommand< codeRef: ResolvedCodeRef, // the specific export being listed ): Promise { const resourceRealm = - this.realm.realmOfURL(new URL(resourceUrl))?.href ?? targetRealm; + this.realm.realmOfURL(cardIdToURL(resourceUrl))?.href ?? targetRealm; const url = `${resourceRealm}_dependencies?url=${encodeURIComponent(resourceUrl)}`; const response = await this.network.authedFetch(url, { headers: { Accept: SupportedMimeType.JSONAPI }, @@ -297,7 +298,7 @@ export default class ListingCreateCommand extends HostBaseCommand< if (sanitizedModules.length > 0) { const createSpecCommand = new CreateSpecCommand(this.commandContext); const normalizedModuleUrl = trimExecutableExtension( - new URL(moduleUrl), + cardIdToURL(moduleUrl), ).href; const specResults = await Promise.all( sanitizedModules.map((module) => { @@ -308,7 +309,7 @@ export default class ListingCreateCommand extends HostBaseCommand< // is often extensionless, so a bare string comparison would create // duplicate specs for the same source file. const normalizedModule = trimExecutableExtension( - new URL(module), + cardIdToURL(module), ).href; const input = normalizedModule === normalizedModuleUrl diff --git a/packages/host/app/commands/listing-install.ts b/packages/host/app/commands/listing-install.ts index 907421bb5c1..c7dff31310b 100644 --- a/packages/host/app/commands/listing-install.ts +++ b/packages/host/app/commands/listing-install.ts @@ -6,6 +6,7 @@ import type { LooseCardResource, } from '@cardstack/runtime-common'; import { + cardIdToURL, type ResolvedCodeRef, RealmPaths, join, @@ -62,7 +63,7 @@ export default class ListingInstallCommand extends HostBaseCommand< let realmUrls = this.realmServer.availableRealmURLs; let { realm, listing: listingInput } = input; - let realmUrl = new RealmPaths(new URL(realm)).url; + let realmUrl = new RealmPaths(cardIdToURL(realm)).url; if (!realmUrls.includes(realmUrl)) { throw new Error(`Invalid realm: ${realmUrl}`); @@ -107,7 +108,7 @@ export default class ListingInstallCommand extends HostBaseCommand< let sourceOperations = await Promise.all( plan.modulesToInstall.map(async (moduleMeta: CopyModuleMeta) => { let { sourceModule, targetModule } = moduleMeta; - let res = await this.cardService.getSource(new URL(sourceModule)); + let res = await this.cardService.getSource(cardIdToURL(sourceModule)); let moduleResource: ModuleResource = { type: 'source', attributes: { content: res.content }, @@ -147,7 +148,7 @@ export default class ListingInstallCommand extends HostBaseCommand< let results = await this.cardService.executeAtomicOperations( operations, - new URL(realmUrl), + cardIdToURL(realmUrl), ); let atomicResults: AtomicOperationResult[] | undefined = diff --git a/packages/host/app/commands/listing-remix.ts b/packages/host/app/commands/listing-remix.ts index c8a15eb9dba..63e8a18f792 100644 --- a/packages/host/app/commands/listing-remix.ts +++ b/packages/host/app/commands/listing-remix.ts @@ -1,6 +1,7 @@ import { service } from '@ember/service'; import { + cardIdToURL, isResolvedCodeRef, RealmPaths, type ResolvedCodeRef, @@ -114,7 +115,7 @@ export default class RemixCommand extends HostBaseCommand< ): Promise { let realmUrls = this.realmServer.availableRealmURLs; let { realm, listing: listingInput } = input; - let realmUrl = new RealmPaths(new URL(realm)).url; + let realmUrl = new RealmPaths(cardIdToURL(realm)).url; // Make sure realm is valid if (!realmUrls.includes(realmUrl)) { diff --git a/packages/host/app/commands/listing-update-specs.ts b/packages/host/app/commands/listing-update-specs.ts index 7e284171a77..a6bcb6c5184 100644 --- a/packages/host/app/commands/listing-update-specs.ts +++ b/packages/host/app/commands/listing-update-specs.ts @@ -2,7 +2,11 @@ import { service } from '@ember/service'; import { isScopedCSSRequest } from 'glimmer-scoped-css'; -import { isCardInstance, SupportedMimeType } from '@cardstack/runtime-common'; +import { + cardIdToURL, + isCardInstance, + SupportedMimeType, +} from '@cardstack/runtime-common'; import { realmURL as realmURLSymbol } from '@cardstack/runtime-common'; @@ -48,7 +52,7 @@ export default class ListingUpdateSpecsCommand extends HostBaseCommand< return false; } try { - const url = new URL(dep); + const url = cardIdToURL(dep); const realmURL = this.realm.realmOfURL(url); if (!realmURL) { return false; diff --git a/packages/host/app/commands/listing-use.ts b/packages/host/app/commands/listing-use.ts index 8ed65b72c49..34cb64139d6 100644 --- a/packages/host/app/commands/listing-use.ts +++ b/packages/host/app/commands/listing-use.ts @@ -45,7 +45,7 @@ export default class ListingUseCommand extends HostBaseCommand< const listing = listingInput as Listing; - let realmUrl = new RealmPaths(new URL(realm)).url; + let realmUrl = new RealmPaths(cardIdToURL(realm)).url; // Make sure realm is valid if (!realmUrls.includes(realmUrl)) { diff --git a/packages/host/app/commands/patch-code.ts b/packages/host/app/commands/patch-code.ts index 8d8dd5e6505..80f7fa9d694 100644 --- a/packages/host/app/commands/patch-code.ts +++ b/packages/host/app/commands/patch-code.ts @@ -1,6 +1,6 @@ import { service } from '@ember/service'; -import { hasExecutableExtension } from '@cardstack/runtime-common'; +import { cardIdToURL, hasExecutableExtension } from '@cardstack/runtime-common'; import type * as BaseCommandModule from 'https://cardstack.com/base/command'; @@ -86,7 +86,7 @@ export default class PatchCodeCommand extends HostBaseCommand< ); if (!savedThroughOpenFile) { this.cardService - .saveSource(new URL(finalFileUrl), patchedCode, 'bot-patch', { + .saveSource(cardIdToURL(finalFileUrl), patchedCode, 'bot-patch', { resetLoader: hasExecutableExtension(finalFileUrl), clientRequestId, }) @@ -122,8 +122,8 @@ export default class PatchCodeCommand extends HostBaseCommand< if (!isReady(openFileResource)) { return false; } - let normalizedOpenUrl = new URL(openFileResource.url).href; - let normalizedTarget = new URL(targetFileUrl).href; + let normalizedOpenUrl = cardIdToURL(openFileResource.url).href; + let normalizedTarget = cardIdToURL(targetFileUrl).href; if (normalizedOpenUrl !== normalizedTarget) { return false; } @@ -150,7 +150,9 @@ export default class PatchCodeCommand extends HostBaseCommand< } private async getFileInfo(fileUrl: string): Promise { - let getSourceResult = await this.cardService.getSource(new URL(fileUrl)); + let getSourceResult = await this.cardService.getSource( + cardIdToURL(fileUrl), + ); let exists = getSourceResult.status !== 404; let content = exists ? getSourceResult.content : ''; let hasContent = exists && content.trim() !== ''; @@ -205,7 +207,8 @@ export default class PatchCodeCommand extends HostBaseCommand< ): Promise { let lintCommand = new LintAndFixCommand(this.commandContext); let realmURL = this.realm.url(fileUrl); - let filename = new URL(fileUrl).pathname.split('/').pop() || 'input.gts'; + let filename = + cardIdToURL(fileUrl).pathname.split('/').pop() || 'input.gts'; return await lintCommand.execute({ realm: realmURL, @@ -216,7 +219,7 @@ export default class PatchCodeCommand extends HostBaseCommand< private isLintableFile(fileUrl: string): boolean { try { - return /\.(gts|ts)$/.test(new URL(fileUrl).pathname); + return /\.(gts|ts)$/.test(cardIdToURL(fileUrl).pathname); } catch { return /\.(gts|ts)$/.test(fileUrl); } @@ -237,7 +240,9 @@ export default class PatchCodeCommand extends HostBaseCommand< } private async fileExists(fileUrl: string): Promise { - let getSourceResult = await this.cardService.getSource(new URL(fileUrl)); + let getSourceResult = await this.cardService.getSource( + cardIdToURL(fileUrl), + ); return getSourceResult.status !== 404; } } diff --git a/packages/host/app/commands/read-source.ts b/packages/host/app/commands/read-source.ts index b0e87d84f3d..3148a78c462 100644 --- a/packages/host/app/commands/read-source.ts +++ b/packages/host/app/commands/read-source.ts @@ -1,6 +1,6 @@ import { service } from '@ember/service'; -import { SupportedMimeType } from '@cardstack/runtime-common'; +import { cardIdToURL, SupportedMimeType } from '@cardstack/runtime-common'; import type * as BaseCommandModule from 'https://cardstack.com/base/command'; @@ -28,9 +28,17 @@ export default class ReadSourceCommand extends HostBaseCommand< protected async run( input: BaseCommandModule.ReadSourceInput, ): Promise { - let url = input.realm - ? new URL(input.path, input.realm) - : new URL(input.path); + let url: URL; + if (input.realm) { + let realmURL = cardIdToURL(input.realm); + try { + url = cardIdToURL(input.path); + } catch { + url = new URL(input.path, realmURL); + } + } else { + url = cardIdToURL(input.path); + } let response = await this.network.authedFetch(url, { headers: { Accept: SupportedMimeType.CardSource }, }); diff --git a/packages/host/app/commands/read-text-file.ts b/packages/host/app/commands/read-text-file.ts index 345f8239038..00bd4704f40 100644 --- a/packages/host/app/commands/read-text-file.ts +++ b/packages/host/app/commands/read-text-file.ts @@ -1,5 +1,7 @@ import { service } from '@ember/service'; +import { cardIdToURL } from '@cardstack/runtime-common'; + import type * as BaseCommandModule from 'https://cardstack.com/base/command'; import HostBaseCommand from '../lib/host-base-command'; @@ -26,9 +28,17 @@ export default class ReadTextFileCommand extends HostBaseCommand< protected async run( input: BaseCommandModule.ReadTextFileInput, ): Promise { - let url = input.realm - ? new URL(input.path, input.realm) - : new URL(input.path); + let url: URL; + if (input.realm) { + let realmURL = cardIdToURL(input.realm); + try { + url = cardIdToURL(input.path); + } catch { + url = new URL(input.path, realmURL); + } + } else { + url = cardIdToURL(input.path); + } let response = await this.network.authedFetch(url, { headers: { Accept: 'text/plain' }, }); diff --git a/packages/host/app/commands/show-card.ts b/packages/host/app/commands/show-card.ts index 6101aff695e..73cf9524fd1 100644 --- a/packages/host/app/commands/show-card.ts +++ b/packages/host/app/commands/show-card.ts @@ -2,6 +2,7 @@ import { service } from '@ember/service'; import type { ResolvedCodeRef } from '@cardstack/runtime-common'; import { + cardIdToURL, identifyCard, internalKeyFor, isCardErrorJSONAPI, @@ -71,7 +72,7 @@ export default class ShowCardCommand extends HostBaseCommand< operatorModeStateService.state.codeSelection !== cardDefRef.name ) { await operatorModeStateService.updateCodePath( - new URL(cardDefRef.module + '.gts'), + cardIdToURL(cardDefRef.module + '.gts'), 'preview', ); } diff --git a/packages/host/app/commands/show-file.ts b/packages/host/app/commands/show-file.ts index 9b28ef1de39..f559535b38c 100644 --- a/packages/host/app/commands/show-file.ts +++ b/packages/host/app/commands/show-file.ts @@ -1,5 +1,7 @@ import { service } from '@ember/service'; +import { cardIdToURL } from '@cardstack/runtime-common'; + import type * as BaseCommandModule from 'https://cardstack.com/base/command'; import HostBaseCommand from '../lib/host-base-command'; @@ -31,7 +33,7 @@ export default class ShowFileCommand extends HostBaseCommand< if (operatorModeStateService.workspaceChooserOpened) { operatorModeStateService.closeWorkspaceChooser(); } - await operatorModeStateService.updateCodePath(new URL(input.fileUrl)); + await operatorModeStateService.updateCodePath(cardIdToURL(input.fileUrl)); await operatorModeStateService.updateSubmode('code'); } } diff --git a/packages/host/app/commands/switch-submode.ts b/packages/host/app/commands/switch-submode.ts index 474e1b53f39..4b8a3d2dac6 100644 --- a/packages/host/app/commands/switch-submode.ts +++ b/packages/host/app/commands/switch-submode.ts @@ -1,5 +1,7 @@ import { service } from '@ember/service'; +import { cardIdToURL } from '@cardstack/runtime-common'; + import type * as BaseCommandModule from 'https://cardstack.com/base/command'; import { Submodes } from '../components/submode-switcher'; @@ -67,7 +69,7 @@ export default class SwitchSubmodeCommand extends HostBaseCommand< ? lastId : lastId + '.json' : null); - let codeUrl = codePath ? new URL(codePath) : null; + let codeUrl = codePath ? cardIdToURL(codePath) : null; let currentSubmode = this.operatorModeStateService.state.submode; let finalCodeUrl = codeUrl; if ( @@ -84,7 +86,7 @@ export default class SwitchSubmodeCommand extends HostBaseCommand< useNonConflictingFilename: true, }); if (writeResult.fileUrl !== codeUrl.href) { - let newCodeUrl = new URL(writeResult.fileUrl); + let newCodeUrl = cardIdToURL(writeResult.fileUrl); finalCodeUrl = newCodeUrl; let commandModule = await this.loadCommandModule(); diff --git a/packages/host/app/commands/write-text-file.ts b/packages/host/app/commands/write-text-file.ts index 09ace13b6c0..03304983a7f 100644 --- a/packages/host/app/commands/write-text-file.ts +++ b/packages/host/app/commands/write-text-file.ts @@ -1,5 +1,7 @@ import { service } from '@ember/service'; +import { cardIdToURL } from '@cardstack/runtime-common'; + import type * as BaseCommandModule from 'https://cardstack.com/base/command'; import HostBaseCommand from '../lib/host-base-command'; @@ -37,7 +39,7 @@ export default class WriteTextFileCommand extends HostBaseCommand< } let realm; if (input.realm) { - realm = this.realm.realmOfURL(new URL(input.realm)); + realm = this.realm.realmOfURL(cardIdToURL(input.realm)); if (!realm) { throw new Error(`Invalid or unknown realm provided: ${input.realm}`); } @@ -46,7 +48,12 @@ export default class WriteTextFileCommand extends HostBaseCommand< if (path.startsWith('/')) { path = path.slice(1); } - let url = new URL(path, realm?.href); + let url: URL; + try { + url = cardIdToURL(path); + } catch { + url = new URL(path, realm?.href); + } let finalUrl = url; let shouldWrite = true; if (!input.overwrite) { @@ -96,7 +103,9 @@ export default class WriteTextFileCommand extends HostBaseCommand< } private async fileExists(fileUrl: string): Promise { - let getSourceResult = await this.cardService.getSource(new URL(fileUrl)); + let getSourceResult = await this.cardService.getSource( + cardIdToURL(fileUrl), + ); return getSourceResult.status !== 404; } } diff --git a/packages/host/app/components/ai-assistant/message/attachments.gts b/packages/host/app/components/ai-assistant/message/attachments.gts index 3cca1a6a185..01e895c7c88 100644 --- a/packages/host/app/components/ai-assistant/message/attachments.gts +++ b/packages/host/app/components/ai-assistant/message/attachments.gts @@ -1,8 +1,9 @@ import type { TemplateOnlyComponent } from '@ember/component/template-only'; -import type { - CardErrorJSONAPI, - getCardCollection, +import { + cardIdToURL, + type CardErrorJSONAPI, + type getCardCollection, } from '@cardstack/runtime-common'; import CardPill from '@cardstack/host/components/card-pill'; @@ -40,7 +41,7 @@ function cardErrorRealm(cardError: CardErrorJSONAPI) { } try { - let url = new URL(id); + let url = cardIdToURL(id); let lastSlashIndex = url.pathname.lastIndexOf('/'); let pathname = lastSlashIndex >= 0 ? url.pathname.slice(0, lastSlashIndex + 1) : '/'; @@ -62,7 +63,7 @@ function cardErrorDisplayTitle(cardError: CardErrorJSONAPI) { let path = id; try { - path = new URL(id).pathname; + path = cardIdToURL(id).pathname; } catch { // ignore invalid urls } diff --git a/packages/host/app/components/card-search/search-content.gts b/packages/host/app/components/card-search/search-content.gts index de8b07e207e..4ddf6495388 100644 --- a/packages/host/app/components/card-search/search-content.gts +++ b/packages/host/app/components/card-search/search-content.gts @@ -13,6 +13,7 @@ import type { PickerOption } from '@cardstack/boxel-ui/components'; import { eq } from '@cardstack/boxel-ui/helpers'; import { + cardIdToURL, type CodeRef, type Filter, type getCard, @@ -201,7 +202,7 @@ export default class SearchContent extends Component { private get searchKeyIsURL() { try { - new URL(this.args.searchKey); + cardIdToURL(this.args.searchKey); return true; } catch (_e) { return false; @@ -390,7 +391,7 @@ export default class SearchContent extends Component { private realmNameFromUrl(realmUrl: string): string { try { - const pathname = new URL(realmUrl).pathname; + const pathname = cardIdToURL(realmUrl).pathname; const segments = pathname.split('/').filter(Boolean); return segments[segments.length - 1] ?? 'Workspace'; } catch { @@ -517,7 +518,7 @@ export default class SearchContent extends Component { } } try { - const url = new URL(cardIdOrUrl); + const url = cardIdToURL(cardIdOrUrl); return `${url.origin}${url.pathname.split('/').slice(0, -1)?.join('/') ?? ''}/`; } catch { return ''; diff --git a/packages/host/app/components/operator-mode/card-schema-editor.gts b/packages/host/app/components/operator-mode/card-schema-editor.gts index 27c5fd4d5d0..20179667b9c 100644 --- a/packages/host/app/components/operator-mode/card-schema-editor.gts +++ b/packages/host/app/components/operator-mode/card-schema-editor.gts @@ -18,7 +18,7 @@ import { and, bool, gt } from '@cardstack/boxel-ui/helpers'; import { ArrowTopLeft, IconLink, IconPlus } from '@cardstack/boxel-ui/icons'; -import { getPlural, isOwnField } from '@cardstack/runtime-common'; +import { cardIdToURL, getPlural, isOwnField } from '@cardstack/runtime-common'; import type { CodeRef } from '@cardstack/runtime-common/code-ref'; import type { ModuleSyntax } from '@cardstack/runtime-common/module-syntax'; @@ -500,7 +500,7 @@ export default class CardSchemaEditor extends Component { } @action async openCardDefinition(moduleURL: string) { - await this.operatorModeStateService.updateCodePath(new URL(moduleURL)); + await this.operatorModeStateService.updateCodePath(cardIdToURL(moduleURL)); } @action diff --git a/packages/host/app/components/operator-mode/card-url-bar.gts b/packages/host/app/components/operator-mode/card-url-bar.gts index 900fa6b0d2f..f3cddaa0fc2 100644 --- a/packages/host/app/components/operator-mode/card-url-bar.gts +++ b/packages/host/app/components/operator-mode/card-url-bar.gts @@ -8,6 +8,8 @@ import { and, bool, not } from '@cardstack/boxel-ui/helpers'; import { IconGlobe, Warning as IconWarning } from '@cardstack/boxel-ui/icons'; +import { cardIdToURL } from '@cardstack/runtime-common'; + import type URLBarResource from '@cardstack/host/resources/url-bar'; import { urlBarResource } from '@cardstack/host/resources/url-bar'; @@ -183,7 +185,7 @@ export default class CardURLBar extends Component { private urlBar: URLBarResource = urlBarResource(this, () => ({ getValue: () => (this.codePath ? decodeURI(this.codePath) : ''), setValue: async (url: string) => { - await this.operatorModeStateService.updateCodePath(new URL(url)); + await this.operatorModeStateService.updateCodePath(cardIdToURL(url)); }, setValueError: this.args.loadFileError, resetValueError: this.args.resetLoadFileError, diff --git a/packages/host/app/components/operator-mode/choose-file-modal.gts b/packages/host/app/components/operator-mode/choose-file-modal.gts index da95bdc4414..fc7901bf2f7 100644 --- a/packages/host/app/components/operator-mode/choose-file-modal.gts +++ b/packages/host/app/components/operator-mode/choose-file-modal.gts @@ -22,6 +22,7 @@ import { import { eq } from '@cardstack/boxel-ui/helpers'; import { + cardIdToURL, Deferred, RealmPaths, isCardErrorJSONAPI, @@ -257,7 +258,7 @@ export default class ChooseFileModal extends Component { private get knownRealms() { return Object.entries(this.realm.allRealmsInfo).map((entry) => ({ - url: new URL(entry[0]), + url: cardIdToURL(entry[0]), info: entry[1].info, })); } diff --git a/packages/host/app/components/operator-mode/code-editor.gts b/packages/host/app/components/operator-mode/code-editor.gts index 86beeb1c85e..f61655d81b7 100644 --- a/packages/host/app/components/operator-mode/code-editor.gts +++ b/packages/host/app/components/operator-mode/code-editor.gts @@ -23,6 +23,7 @@ import { Position } from 'monaco-editor'; import { LoadingIndicator } from '@cardstack/boxel-ui/components'; import { + cardIdToURL, hasExecutableExtension, logger, isSingleCardDocument, @@ -379,14 +380,14 @@ export default class CodeEditor extends Component { } adoptsFrom = codeRefWithAbsoluteURL( adoptsFrom, - new URL(this.args.file.url), + cardIdToURL(this.args.file.url), ); if ( !isEqual( adoptsFrom, codeRefWithAbsoluteURL( json.data.meta.adoptsFrom, - new URL(this.args.file.url), + cardIdToURL(this.args.file.url), ), ) ) { diff --git a/packages/host/app/components/operator-mode/code-submode.gts b/packages/host/app/components/operator-mode/code-submode.gts index 6e7f3bfd93b..ffec381b03d 100644 --- a/packages/host/app/components/operator-mode/code-submode.gts +++ b/packages/host/app/components/operator-mode/code-submode.gts @@ -33,6 +33,7 @@ import type { CodeRef } from '@cardstack/runtime-common'; import { isCardDocumentString, isCardErrorJSONAPI, + cardIdToURL, RealmPaths, PermissionsContextName, GetCardContextName, @@ -512,7 +513,7 @@ export default class CodeSubmode extends Component { // TODO: This is a side effect of the recent-file service making assumptions about // what realm we are in. we should refactor that so that callers have to tell // it the realm of the file in question - let realmURL = new URL(this.operatorModeStateService.realmURL); + let realmURL = cardIdToURL(this.operatorModeStateService.realmURL); if (realmURL) { let realmPaths = new RealmPaths(realmURL); @@ -533,7 +534,7 @@ export default class CodeSubmode extends Component { let recentFileUrl = `${recentFile.realmURL}${recentFile.filePath}`; await this.operatorModeStateService.updateCodePath( - new URL(recentFileUrl), + cardIdToURL(recentFileUrl), ); } else { await this.operatorModeStateService.updateCodePath(null); @@ -573,7 +574,7 @@ export default class CodeSubmode extends Component { this.isCreateModalOpen = true; let url = await this.createFileModal.createNewFile( fileType, - new URL(destinationRealm), + destinationRealm, definitionClass, sourceInstance, ); @@ -591,11 +592,13 @@ export default class CodeSubmode extends Component { if (!realmURL) { throw new Error('No realm available for upload'); } - let task = this.fileUpload.uploadFile({ realmURL: new URL(realmURL) }); + let task = this.fileUpload.uploadFile({ realmURL: cardIdToURL(realmURL) }); task.result .then((fileDef) => { if (fileDef?.url) { - this.operatorModeStateService.updateCodePath(new URL(fileDef.url)); + this.operatorModeStateService.updateCodePath( + cardIdToURL(fileDef.url), + ); } }) .catch((error) => { @@ -629,9 +632,9 @@ export default class CodeSubmode extends Component { }; @action private async openSearchResultInEditor(cardId: string) { - let codePath = cardId.endsWith('.json') - ? new URL(cardId) - : new URL(cardId + '.json'); + let codePath = cardIdToURL( + cardId.endsWith('.json') ? cardId : `${cardId}.json`, + ); await this.operatorModeStateService.updateCodePath(codePath); } diff --git a/packages/host/app/components/operator-mode/code-submode/left-panel-toggle.gts b/packages/host/app/components/operator-mode/code-submode/left-panel-toggle.gts index 67d84525cf7..97a6e05d744 100644 --- a/packages/host/app/components/operator-mode/code-submode/left-panel-toggle.gts +++ b/packages/host/app/components/operator-mode/code-submode/left-panel-toggle.gts @@ -10,6 +10,7 @@ import { Button as BoxelButton } from '@cardstack/boxel-ui/components'; import { cn, not } from '@cardstack/boxel-ui/helpers'; import { Download } from '@cardstack/boxel-ui/icons'; +import { cardIdToURL } from '@cardstack/runtime-common'; import { createURLSignature } from '@cardstack/runtime-common/url-signature'; import RealmDropdown from '@cardstack/host/components/realm-dropdown'; @@ -90,12 +91,12 @@ export default class CodeSubmodeLeftPanelToggle extends Component { this.recentFilesService.findRecentFileByRealmURL(realmUrl); if (recentFile) { this.operatorModeStateService.updateCodePath( - new URL(`${realmUrl}${recentFile.filePath}`), + new URL(recentFile.filePath, cardIdToURL(realmUrl)), ); return; } this.operatorModeStateService.updateCodePath( - new URL('./index.json', realmUrl), + new URL('./index.json', cardIdToURL(realmUrl)), ); } } @@ -105,13 +106,16 @@ export default class CodeSubmodeLeftPanelToggle extends Component { }; private get downloadFilename() { - return fallbackDownloadName(new URL(this.args.realmURL)); + return fallbackDownloadName(cardIdToURL(this.args.realmURL)); } downloadRealm = async (event: Event) => { event.preventDefault(); - let downloadURL = new URL('/_download-realm', this.args.realmURL); + let downloadURL = new URL( + '/_download-realm', + cardIdToURL(this.args.realmURL), + ); downloadURL.searchParams.set('realm', this.args.realmURL); let token = this.realm.token(this.args.realmURL); diff --git a/packages/host/app/components/operator-mode/code-submode/module-inspector.gts b/packages/host/app/components/operator-mode/code-submode/module-inspector.gts index 21e562f38eb..0ce1d5da61f 100644 --- a/packages/host/app/components/operator-mode/code-submode/module-inspector.gts +++ b/packages/host/app/components/operator-mode/code-submode/module-inspector.gts @@ -43,6 +43,7 @@ import { localId, meta, hasExtension, + cardIdToURL, resolveCardReference, } from '@cardstack/runtime-common'; @@ -151,7 +152,7 @@ export default class ModuleInspector extends Component return state; } let loader = this.loaderService.loader; - let relativeTo = new URL(this.operatorModeStateService.realmURL); + let relativeTo = cardIdToURL(this.operatorModeStateService.realmURL); runWhileActive(on, async (isActive) => { try { let cardDef = await loadCardDef(codeRef, { @@ -355,7 +356,7 @@ export default class ModuleInspector extends Component } const fileUrl = hasExtension(cardId) ? cardId : `${cardId}.json`; - await this.operatorModeStateService.updateCodePath(new URL(fileUrl)); + await this.operatorModeStateService.updateCodePath(cardIdToURL(fileUrl)); }; @action private setActivePanel(item: ModuleInspectorView) { diff --git a/packages/host/app/components/operator-mode/code-submode/playground/playground-panel.gts b/packages/host/app/components/operator-mode/code-submode/playground/playground-panel.gts index 23299299b8e..58edbe59c2f 100644 --- a/packages/host/app/components/operator-mode/code-submode/playground/playground-panel.gts +++ b/packages/host/app/components/operator-mode/code-submode/playground/playground-panel.gts @@ -23,6 +23,7 @@ import { Folder, IconPlusThin } from '@cardstack/boxel-ui/icons'; import { CardContextName, + cardIdToURL, cardTypeDisplayName, getMenuItems, isSpecCard, @@ -425,7 +426,7 @@ export default class PlaygroundPanel extends Component { return undefined; } try { - let cardURL = new URL(selectedCardId); + let cardURL = cardIdToURL(selectedCardId); return this.realm.realmOfURL(cardURL)?.href; } catch { return undefined; diff --git a/packages/host/app/components/operator-mode/code-submode/schema-editor.gts b/packages/host/app/components/operator-mode/code-submode/schema-editor.gts index 5bc4f4fdec3..873c3a35fba 100644 --- a/packages/host/app/components/operator-mode/code-submode/schema-editor.gts +++ b/packages/host/app/components/operator-mode/code-submode/schema-editor.gts @@ -4,7 +4,7 @@ import Component from '@glimmer/component'; import { cached } from '@glimmer/tracking'; -import { getPlural } from '@cardstack/runtime-common'; +import { cardIdToURL, getPlural } from '@cardstack/runtime-common'; import type { CodeRef } from '@cardstack/runtime-common/code-ref'; import { ModuleSyntax } from '@cardstack/runtime-common/module-syntax'; @@ -172,7 +172,7 @@ export default class SchemaEditor extends Component { get moduleSyntax() { return new ModuleSyntax( this.args.file.content, - new URL(this.args.file.url), + cardIdToURL(this.args.file.url), ); } diff --git a/packages/host/app/components/operator-mode/code-submode/spec-preview.gts b/packages/host/app/components/operator-mode/code-submode/spec-preview.gts index fcf975f1e36..eae48051510 100644 --- a/packages/host/app/components/operator-mode/code-submode/spec-preview.gts +++ b/packages/host/app/components/operator-mode/code-submode/spec-preview.gts @@ -20,6 +20,7 @@ import { cn } from '@cardstack/boxel-ui/helpers'; import { type ResolvedCodeRef, + cardIdToURL, type getCard, type getCards, type getCardCollection, @@ -173,7 +174,7 @@ class SpecPreviewContent extends GlimmerComponent { return; } - const selectedUrl = new URL(this.selectedId); + const selectedUrl = cardIdToURL(this.selectedId); await this.operatorModeStateService.updateCodePath(selectedUrl); } @@ -345,7 +346,7 @@ export default class SpecPreview extends GlimmerComponent { return state; } let loader = this.loaderService.loader; - let relativeTo = new URL(this.operatorModeStateService.realmURL); + let relativeTo = cardIdToURL(this.operatorModeStateService.realmURL); runWhileActive(on, async (isActive) => { try { let cardDef = await loadCardDef(codeRef, { @@ -426,7 +427,7 @@ export default class SpecPreview extends GlimmerComponent { } function getRelativePath(baseUrl: string, targetUrl: string) { - const basePath = new URL(baseUrl).pathname; - const targetPath = new URL(targetUrl).pathname; + const basePath = cardIdToURL(baseUrl).pathname; + const targetPath = cardIdToURL(targetUrl).pathname; return targetPath.replace(basePath, '') || '/'; } 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 15a10e7bdc9..bed9e922932 100644 --- a/packages/host/app/components/operator-mode/create-file-modal.gts +++ b/packages/host/app/components/operator-mode/create-file-modal.gts @@ -617,7 +617,10 @@ export default class CreateFileModal extends Component { ); } this.clearSaveError(); - this.currentRequest = { ...this.currentRequest, realmURL: new URL(path) }; + this.currentRequest = { + ...this.currentRequest, + realmURL: cardIdToURL(path), + }; } @action private setDisplayName(name: string) { @@ -796,7 +799,7 @@ export default class CreateFileModal extends Component { let isField = this.fileType.id === 'field-definition'; let isFileDef = this.fileType.id === 'file-definition'; - let realmPath = new RealmPaths(new URL(this.selectedRealmURL)); + let realmPath = new RealmPaths(cardIdToURL(this.selectedRealmURL)); // assert that filename is a GTS file and is a LocalPath let fileName: LocalPath = `${this.fileName.replace( /\.[^.].+$/, @@ -835,14 +838,14 @@ export default class CreateFileModal extends Component { module: spec?.moduleHref ?? module, name: exportName, }, - new URL(this.selectedRealmURL), + cardIdToURL(this.selectedRealmURL), ) as ResolvedCodeRef ).module; - const absoluteModule = new URL(absoluteModuleHref); + const absoluteModule = cardIdToURL(absoluteModuleHref); let moduleURL = maybeRelativeURL( absoluteModule, url, - new URL(this.selectedRealmURL), + cardIdToURL(this.selectedRealmURL), ); let src: string[] = []; @@ -915,7 +918,9 @@ export class ${className} extends ${exportName} { sourceCard: this.currentRequest.sourceInstance, targetRealm: this.selectedRealmURL, }); - this.currentRequest.newFileDeferred.fulfill(new URL(`${newCardId}.json`)); + this.currentRequest.newFileDeferred.fulfill( + cardIdToURL(`${newCardId}.json`), + ); }); private createCardInstance = restartableTask(async () => { @@ -968,7 +973,9 @@ export class ${className} extends ${exportName} { let error = maybeId; throw error; } - this.currentRequest.newFileDeferred.fulfill(new URL(`${maybeId}.json`)); + this.currentRequest.newFileDeferred.fulfill( + cardIdToURL(`${maybeId}.json`), + ); } catch (e: any) { console.log('Error saving', e); this.saveError = e; @@ -995,7 +1002,7 @@ export class ${className} extends ${exportName} { return; } - let realmPath = new RealmPaths(new URL(this.selectedRealmURL)); + let realmPath = new RealmPaths(cardIdToURL(this.selectedRealmURL)); let filePath: LocalPath = fileName as LocalPath; let url = realmPath.fileURL(filePath); diff --git a/packages/host/app/components/operator-mode/create-listing-modal.gts b/packages/host/app/components/operator-mode/create-listing-modal.gts index 9be96171931..52780a9847b 100644 --- a/packages/host/app/components/operator-mode/create-listing-modal.gts +++ b/packages/host/app/components/operator-mode/create-listing-modal.gts @@ -19,6 +19,7 @@ import { import { IconX, IconPlus } from '@cardstack/boxel-ui/icons'; import { + cardIdToURL, chooseCard, isResolvedCodeRef, removeFileExtension, @@ -96,7 +97,7 @@ export default class CreateListingModal extends Component { private get selectedExampleRealms(): string[] { let realms = this.selectedExampleURLs.flatMap((cardUrl) => { try { - let realmURL = this.realm.realmOfURL(new URL(cardUrl))?.href; + let realmURL = this.realm.realmOfURL(cardIdToURL(cardUrl))?.href; return realmURL ? [realmURL] : []; } catch (_error) { return []; @@ -111,7 +112,7 @@ export default class CreateListingModal extends Component { return; } let consumingRealm = this.payload?.targetRealm - ? new URL(this.payload.targetRealm) + ? cardIdToURL(this.payload.targetRealm) : undefined; let selected = await chooseCard( { filter: { type: codeRef } }, @@ -164,7 +165,7 @@ export default class CreateListingModal extends Component { } await this.operatorModeStateService.updateSubmode(Submodes.Code); await this.operatorModeStateService.updateCodePath( - new URL(cardUrl + '.json'), + cardIdToURL(`${cardUrl}.json`), 'preview', ); this.operatorModeStateService.updateCardPreviewFormat('isolated'); 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 058ae97e889..f6ea36e2961 100644 --- a/packages/host/app/components/operator-mode/edit-field-modal.gts +++ b/packages/host/app/components/operator-mode/edit-field-modal.gts @@ -22,6 +22,7 @@ import { import { bool, cssVar } from '@cardstack/boxel-ui/helpers'; import { + cardIdToURL, chooseCard, loadCardDef, identifyCard, @@ -141,8 +142,8 @@ export default class EditFieldModal extends Component { throw error; } - this.fieldModuleURL = new URL(ref.module); - this.cardURL = new URL(ref.module); + this.fieldModuleURL = cardIdToURL(ref.module); + this.cardURL = cardIdToURL(ref.module); this.fieldRef = ref; return; } @@ -160,8 +161,8 @@ export default class EditFieldModal extends Component { }); let moduleRef = moduleFrom(ref); - this.fieldModuleURL = new URL(moduleRef); - this.cardURL = new URL(moduleRef); + this.fieldModuleURL = cardIdToURL(moduleRef); + this.cardURL = cardIdToURL(moduleRef); this.fieldRef = ref; // Field's card can descend from a FieldDef or a CardDef, so we need to determine which one it is. We do this by checking the field's type - @@ -185,11 +186,11 @@ export default class EditFieldModal extends Component { if (spec && isCardInstance(spec)) { this.fieldCard = await loadCardDef(spec.ref, { loader: this.loaderService.loader, - relativeTo: new URL(specId), + relativeTo: cardIdToURL(specId), }); this.isFieldDef = spec.isField; - this.cardURL = new URL(spec.id); + this.cardURL = cardIdToURL(spec.id); this.fieldRef = spec.ref; // This transforms relative module paths, such as "../person", to absolute ones - @@ -238,10 +239,8 @@ export default class EditFieldModal extends Component { fieldType, fieldDefinitionType: this.isFieldDef ? 'field' : 'card', incomingRelativeTo, - outgoingRelativeTo: new URL( - this.operatorModeStateService.state.codePath!, - ), - outgoingRealmURL: new URL(this.args.file.realmURL), + outgoingRelativeTo: this.operatorModeStateService.state.codePath!, + outgoingRealmURL: cardIdToURL(this.args.file.realmURL), addFieldAtIndex, }); } catch (error) { diff --git a/packages/host/app/components/operator-mode/interact-submode.gts b/packages/host/app/components/operator-mode/interact-submode.gts index cdbc6ff087f..1d433695807 100644 --- a/packages/host/app/components/operator-mode/interact-submode.gts +++ b/packages/host/app/components/operator-mode/interact-submode.gts @@ -33,6 +33,7 @@ import { Deferred, cardTypeDisplayName, cardTypeIcon, + cardIdToURL, codeRefWithAbsoluteURL, identifyCard, isCardInstance, @@ -424,7 +425,7 @@ export default class InteractSubmode extends Component { title: loadedCard.cardTitle, }; } else { - let cardUrl = card instanceof URL ? card : new URL(card as string); + let cardUrl = card instanceof URL ? card : cardIdToURL(card as string); let loadedCard = await this.store.get(cardUrl.href); if (isCardInstance(loadedCard)) { cardToDelete = { @@ -504,7 +505,7 @@ export default class InteractSubmode extends Component { private openSelectedSearchResultInStack = restartableTask( async (cardId: string) => { let waiterToken = waiter.beginAsync(); - let url = new URL(cardId); + let url = cardIdToURL(cardId); try { let searchSheetTrigger = this.searchSheetTrigger; // Will be set by showSearchWithTrigger @@ -704,9 +705,14 @@ export default class InteractSubmode extends Component { } // assumption: take actions in the right-most stack - await this.createCard(this.rightMostStackIndex, spec.ref, new URL(specId), { - realmURL: this.operatorModeStateService.getWritableRealmURL(), - }); + await this.createCard( + this.rightMostStackIndex, + spec.ref, + cardIdToURL(specId), + { + realmURL: this.operatorModeStateService.getWritableRealmURL(), + }, + ); }); private createNewFromRecentType = restartableTask( diff --git a/packages/host/app/components/operator-mode/overlays.gts b/packages/host/app/components/operator-mode/overlays.gts index ea193339691..bc5c2eb5d96 100644 --- a/packages/host/app/components/operator-mode/overlays.gts +++ b/packages/host/app/components/operator-mode/overlays.gts @@ -12,7 +12,10 @@ import { dropTask } from 'ember-concurrency'; import { velcro } from 'ember-velcro'; import { isEqual, omit } from 'lodash'; -import { localId as localIdSymbol } from '@cardstack/runtime-common'; +import { + cardIdToURL, + localId as localIdSymbol, +} from '@cardstack/runtime-common'; import type CardService from '@cardstack/host/services/card-service'; import type RealmService from '@cardstack/host/services/realm'; @@ -263,7 +266,7 @@ export default class Overlays extends Component { format = canWrite ? format : 'isolated'; if (this.args.viewCard) { let target = - typeof cardDefOrId === 'string' ? new URL(cardId) : cardDefOrId; + typeof cardDefOrId === 'string' ? cardIdToURL(cardId) : cardDefOrId; await this.args.viewCard( target, format, diff --git a/packages/host/app/components/operator-mode/preview-panel/index.gts b/packages/host/app/components/operator-mode/preview-panel/index.gts index b6db2446a30..0662f916d12 100644 --- a/packages/host/app/components/operator-mode/preview-panel/index.gts +++ b/packages/host/app/components/operator-mode/preview-panel/index.gts @@ -18,6 +18,7 @@ import { eq, toMenuItems } from '@cardstack/boxel-ui/helpers'; import { Eye, IconCode } from '@cardstack/boxel-ui/icons'; import { + cardIdToURL, cardTypeDisplayName, cardTypeIcon, formats as allFormats, @@ -114,7 +115,7 @@ export default class PreviewPanel extends Component { const gtsFileUrl = type.module.endsWith('.gts') ? type.module : `${type.module}.gts`; - this.operatorModeStateService.updateCodePath(new URL(gtsFileUrl)); + this.operatorModeStateService.updateCodePath(cardIdToURL(gtsFileUrl)); } }; diff --git a/packages/host/app/components/operator-mode/publish-realm-modal.gts b/packages/host/app/components/operator-mode/publish-realm-modal.gts index 4631cfa1418..e04a3463768 100644 --- a/packages/host/app/components/operator-mode/publish-realm-modal.gts +++ b/packages/host/app/components/operator-mode/publish-realm-modal.gts @@ -24,7 +24,7 @@ import { import { not } from '@cardstack/boxel-ui/helpers'; import { IconX, Warning as WarningIcon } from '@cardstack/boxel-ui/icons'; -import { ensureTrailingSlash } from '@cardstack/runtime-common'; +import { cardIdToURL, ensureTrailingSlash } from '@cardstack/runtime-common'; import { getPublishedRealmDomainOverrides } from '@cardstack/runtime-common/constants'; import ModalContainer from '@cardstack/host/components/modal-container'; @@ -239,7 +239,7 @@ export default class PublishRealmModal extends Component { return null; } - let overriddenURL = new URL(publishedRealmURL); + let overriddenURL = cardIdToURL(publishedRealmURL); overriddenURL.host = overrideDomain; return ensureTrailingSlash(overriddenURL.toString()); } @@ -476,8 +476,8 @@ export default class PublishRealmModal extends Component { } try { - const pathSegments = new URL(realmUrl).pathname - .split('/') + const pathSegments = cardIdToURL(realmUrl) + .pathname.split('/') .filter((segment) => segment); const lastSegment = pathSegments[pathSegments.length - 1]; diff --git a/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts b/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts index 74d3ed856de..4f46f16c288 100644 --- a/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts +++ b/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts @@ -31,6 +31,7 @@ import { } from '@cardstack/boxel-ui/icons'; import { + cardIdToURL, hasExecutableExtension, SupportedMimeType, } from '@cardstack/runtime-common'; @@ -836,7 +837,7 @@ export default class Workspace extends Component { private get displayPublishedURL() { try { - let url = new URL(this.primaryPublishedURL); + let url = cardIdToURL(this.primaryPublishedURL); return url.host + url.pathname.replace(/\/$/, ''); } catch { return this.primaryPublishedURL; @@ -1028,7 +1029,7 @@ function summarizeWorkspaceContents( ): WorkspaceDeleteSummary { return fileURLs.reduce( (summary, fileURL) => { - let path = new URL(fileURL).pathname; + let path = cardIdToURL(fileURL).pathname; if (path.endsWith('/.realm.json')) { return summary; } diff --git a/packages/host/app/lib/gc-card-store.ts b/packages/host/app/lib/gc-card-store.ts index 53e1b0ff952..c9c54a0f750 100644 --- a/packages/host/app/lib/gc-card-store.ts +++ b/packages/host/app/lib/gc-card-store.ts @@ -663,7 +663,12 @@ export default class CardStoreWithGarbageCollection implements CardStore { if (!localId) { localId = remoteId.split('/').pop()!; item = bucket.get(localId) ?? silentBucket.get(localId); - if (item && type === 'instance' && isCardOrFileInstance(item)) { + if ( + item && + type === 'instance' && + isCardOrFileInstance(item) && + (!item.id || isLocalId(item.id)) + ) { item.id = remoteId; } } diff --git a/packages/host/app/resources/code-diff.ts b/packages/host/app/resources/code-diff.ts index 93f42c643d0..56ca273fd50 100644 --- a/packages/host/app/resources/code-diff.ts +++ b/packages/host/app/resources/code-diff.ts @@ -4,6 +4,8 @@ import { tracked } from '@glimmer/tracking'; import { restartableTask } from 'ember-concurrency'; import { Resource } from 'ember-modify-based-class-resource'; +import { cardIdToURL } from '@cardstack/runtime-common'; + import type CardService from '@cardstack/host/services/card-service'; import type CommandService from '@cardstack/host/services/command-service'; @@ -96,7 +98,7 @@ export class CodeDiffResource extends Resource { return; } try { - let result = await this.cardService.getSource(new URL(fileUrl)); + let result = await this.cardService.getSource(cardIdToURL(fileUrl)); if (result.status === 404) { this.originalCode = ''; // We are creating a new file, so we don't have the original code } else { diff --git a/packages/host/app/routes/module.ts b/packages/host/app/routes/module.ts index 4f3bfe108e2..702bc1c262b 100644 --- a/packages/host/app/routes/module.ts +++ b/packages/host/app/routes/module.ts @@ -11,6 +11,7 @@ import { parse } from 'date-fns'; import isEqual from 'lodash/isEqual'; import { + cardIdToURL, type Definition, type CodeRef, type ResolvedCodeRef, @@ -200,7 +201,7 @@ export async function buildModuleModel( context: ModuleModelContext, ): Promise { let parsedOptions = renderOptions ?? {}; - let moduleURL = trimExecutableExtension(new URL(id)); + let moduleURL = trimExecutableExtension(cardIdToURL(id)); registerBoxelTransitionTo(context.router, context.owner); if (parsedOptions.clearCache) { @@ -290,7 +291,9 @@ export async function buildModuleModel( let consumes = ( await context.loaderService.loader.getConsumedModules(id) ).filter((u) => u !== id); - deps = consumes.map((d) => trimExecutableExtension(new URL(d)).href); + deps = consumes.map( + (d) => trimExecutableExtension(cardIdToURL(d)).href, + ); let lastModifiedRFC7321 = response.headers.get('last-modified'); let createdAtRFC7321 = response.headers.get('x-created'); if (!lastModifiedRFC7321) { @@ -394,7 +397,7 @@ async function makeDefinition( return { type: 'definition', definition, - moduleURL: trimExecutableExtension(new URL(url)).href, + moduleURL: trimExecutableExtension(url).href, types: typesMaybeError.types.map(({ refURL }) => refURL), }; } catch (err: any) { diff --git a/packages/host/app/services/ai-assistant-panel-service.ts b/packages/host/app/services/ai-assistant-panel-service.ts index 595f938fd7c..dadc5bd988e 100644 --- a/packages/host/app/services/ai-assistant-panel-service.ts +++ b/packages/host/app/services/ai-assistant-panel-service.ts @@ -9,7 +9,12 @@ import { timeout } from 'ember-concurrency'; import window from 'ember-window-mock'; -import { isCardInstance } from '@cardstack/runtime-common'; +import { + isCardInstance, + isRegisteredPrefix, + resolveCardReference, + unresolveCardReference, +} from '@cardstack/runtime-common'; import { APP_BOXEL_ACTIVE_LLM, APP_BOXEL_LLM_MODE, @@ -54,6 +59,31 @@ export interface SessionRoomData { lastActiveTimestamp: number; } +function canonicalCardId(id: string): string { + try { + if (isRegisteredPrefix(id)) { + let stripped = id.split('#')[0] ?? id; + return stripped.split('?')[0] ?? stripped; + } + let resolved = resolveCardReference(id, undefined); + let parsed = new URL(resolved); + parsed.search = ''; + parsed.hash = ''; + return unresolveCardReference(parsed.href); + } catch (_e) { + let stripped = id.split('#')[0] ?? id; + return stripped.split('?')[0] ?? stripped; + } +} + +function resolveAbsoluteCardId(id: string): string | undefined { + try { + return resolveCardReference(id, undefined); + } catch (_e) { + return undefined; + } +} + export default class AiAssistantPanelService extends Service { @service declare private codeSemanticsService: CodeSemanticsService; @service declare private commandService: CommandService; @@ -383,10 +413,18 @@ export default class AiAssistantPanelService extends Service { // Collect attached cards (using sourceUrl from the message's attachedCardIds) if (message.attachedCardIds) { for (const cardId of message.attachedCardIds) { - if (cardId && !seenCardUrls.has(cardId)) { - seenCardUrls.add(cardId); + if (!cardId) { + continue; + } + let canonicalId = canonicalCardId(cardId); + if (!seenCardUrls.has(canonicalId)) { + seenCardUrls.add(canonicalId); // We need to get the actual card from the store - const card = this.store.peek(cardId); + const resolvedId = resolveAbsoluteCardId(cardId); + const card = + this.store.peek(cardId) ?? + (resolvedId ? this.store.peek(resolvedId) : undefined) ?? + this.store.peek(canonicalId); if (card && isCardInstance(card)) { attachedCards.push(card); } diff --git a/packages/host/app/services/command-service.ts b/packages/host/app/services/command-service.ts index bc0ee776cc4..8a78ce276c1 100644 --- a/packages/host/app/services/command-service.ts +++ b/packages/host/app/services/command-service.ts @@ -15,6 +15,7 @@ import { v4 as uuidv4 } from 'uuid'; import type { Command, CommandContext } from '@cardstack/runtime-common'; import { + cardIdToURL, Deferred, CommandContextStamp, delay, @@ -157,7 +158,7 @@ export default class CommandService extends Service { let realmURL: URL | undefined; try { - realmURL = this.realm.realmOfURL(new URL(fileUrl)) ?? undefined; + realmURL = this.realm.realmOfURL(cardIdToURL(fileUrl)) ?? undefined; } catch (_e) { return clientRequestId; } diff --git a/packages/host/app/services/network.ts b/packages/host/app/services/network.ts index c452419d577..42826c0da11 100644 --- a/packages/host/app/services/network.ts +++ b/packages/host/app/services/network.ts @@ -5,6 +5,7 @@ import { VirtualNetwork, authorizationMiddleware, baseRealm, + cardIdToURL, registerCardReferencePrefix, fetcher, } from '@cardstack/runtime-common'; @@ -39,10 +40,13 @@ export default class NetworkService extends Service { } get authedFetch() { - return fetcher(this.fetch, [ + let authenticatedFetch = fetcher(this.fetch, [ authorizationMiddleware(this.realm), authErrorEventMiddleware(), ]); + return (url: string | URL | Request, init?: RequestInit) => { + return authenticatedFetch(normalizeAuthedFetchURL(url), init); + }; } get mount() { @@ -100,3 +104,14 @@ declare module '@ember/service' { function withTrailingSlash(url: string): string { return url.endsWith('/') ? url : `${url}/`; } + +function normalizeAuthedFetchURL(url: string | URL | Request) { + if (url instanceof URL || url instanceof Request) { + return url; + } + try { + return cardIdToURL(url).href; + } catch { + return url; + } +} diff --git a/packages/host/app/services/operator-mode-state-service.ts b/packages/host/app/services/operator-mode-state-service.ts index 70b77d412ea..479c8771ec1 100644 --- a/packages/host/app/services/operator-mode-state-service.ts +++ b/packages/host/app/services/operator-mode-state-service.ts @@ -308,35 +308,47 @@ export default class OperatorModeStateService extends Service { } async deleteCard(cardId: string) { - let cardRealmUrl = (await this.network.authedFetch(cardId)).headers.get( - 'X-Boxel-Realm-Url', - ); + let resolvedCardId = cardIdToURL(cardId).href; + let cardRealmUrl = ( + await this.network.authedFetch(resolvedCardId) + ).headers.get('X-Boxel-Realm-Url'); if (!cardRealmUrl) { throw new Error(`Could not determine the realm for card "${cardId}"`); } - await this.store.delete(cardId); + await this.store.delete(resolvedCardId); // remove all stack items for the deleted card + let normalizeCardId = (id: string) => { + let trimmed = removeFileExtension(id) ?? id; + try { + return cardIdToURL(trimmed).href; + } catch { + return trimmed; + } + }; + let normalizedDeletedId = normalizeCardId(cardId); let items: StackItem[] = []; for (let stack of this._state.stacks || []) { items.push( ...(stack.filter( - (i: StackItem) => i.id && removeFileExtension(i.id) === cardId, + (i: StackItem) => + i.id != null && normalizeCardId(i.id) === normalizedDeletedId, ) as StackItem[]), ); } for (let item of items) { this.trimItemsFromStack(item); } - let realmPaths = new RealmPaths(new URL(cardRealmUrl)); - let cardPath = realmPaths.local(cardIdToURL(`${cardId}.json`)); + let realmPaths = new RealmPaths(cardIdToURL(cardRealmUrl)); + let cardPath = realmPaths.local(cardIdToURL(`${resolvedCardId}.json`)); this.recentFilesService.removeRecentFile(cardPath); this.recentCardsService.remove(cardId); + this.recentCardsService.remove(resolvedCardId); } async copySource(fromUrl: string, toUrl: string) { - await this.cardService.copySource(new URL(fromUrl), new URL(toUrl)); + await this.cardService.copySource(cardIdToURL(fromUrl), cardIdToURL(toUrl)); } trimItemsFromStack(item: StackItem) { @@ -643,7 +655,7 @@ export default class OperatorModeStateService extends Service { if (codeRef && isResolvedCodeRef(codeRef)) { //(possibly) in a different module this._state.codeSelection = codeRef.name; - await this.updateCodePath(new URL(codeRef.module)); + await this.updateCodePath(cardIdToURL(codeRef.module)); } else if ( codeRef && 'type' in codeRef && @@ -653,7 +665,7 @@ export default class OperatorModeStateService extends Service { ) { this._state.fieldSelection = codeRef.field; this._state.codeSelection = codeRef.card.name; - await this.updateCodePath(new URL(codeRef.card.module)); + await this.updateCodePath(cardIdToURL(codeRef.card.module)); } else if (localName && onLocalSelection) { //in the same module this._state.codeSelection = localName; @@ -665,7 +677,7 @@ export default class OperatorModeStateService extends Service { get codePathRelativeToRealm() { if (this._state.codePath && this.realmURL) { - let realmPath = new RealmPaths(new URL(this.realmURL)); + let realmPath = new RealmPaths(cardIdToURL(this.realmURL)); if (realmPath.inRealm(this._state.codePath)) { try { @@ -687,7 +699,7 @@ export default class OperatorModeStateService extends Service { } onFileSelected = async (entryPath: LocalPath) => { - let fileUrl = new RealmPaths(new URL(this.realmURL)).fileURL(entryPath); + let fileUrl = new RealmPaths(cardIdToURL(this.realmURL)).fileURL(entryPath); await this.updateCodePath(fileUrl); }; @@ -955,7 +967,7 @@ export default class OperatorModeStateService extends Service { let newState: OperatorModeState = new TrackedObject({ stacks: new TrackedArray([]), submode: rawState.submode ?? Submodes.Interact, - codePath: rawState.codePath ? new URL(rawState.codePath) : null, + codePath: rawState.codePath ? cardIdToURL(rawState.codePath) : null, hostModePrimaryCard: rawState.trail?.[0]?.replace(/\.json$/, '') ?? null, hostModeStack: new TrackedArray( rawState.trail @@ -1109,11 +1121,11 @@ export default class OperatorModeStateService extends Service { let foundURL = urlsToCheck.find((url) => this.realm.canWrite(url)); if (foundURL) { - return new URL(this.realm.url(foundURL)!); + return cardIdToURL(this.realm.url(foundURL)!); } if (this.realm.defaultWritableRealm) { - return new URL(this.realm.defaultWritableRealm.path); + return cardIdToURL(this.realm.defaultWritableRealm.path); } return undefined; // no writable realm found @@ -1142,7 +1154,7 @@ export default class OperatorModeStateService extends Service { url: codePath!.href, onStateChange: (state: FileResource['state']) => { if (state === 'ready') { - this.cachedRealmURL = new URL(this.readyFile.realmURL); + this.cachedRealmURL = cardIdToURL(this.readyFile.realmURL); this.updateOpenDirsForNestedPath(); } @@ -1154,7 +1166,7 @@ export default class OperatorModeStateService extends Service { if (!url) { return; } - this.replaceCodePath(new URL(url)); + this.replaceCodePath(cardIdToURL(url)); }, })); }); @@ -1225,17 +1237,17 @@ export default class OperatorModeStateService extends Service { this.addItemToStack(stackItem); let lastOpenedFile = this.recentFilesService.recentFiles.find( - (file: RecentFile) => file.realmURL.href === realmUrl, + (file: RecentFile) => file.realmURL.href === cardIdToURL(realmUrl).href, ); await this.updateCodePath( lastOpenedFile - ? new URL(`${lastOpenedFile.realmURL}${lastOpenedFile.filePath}`) - : new URL(id), + ? cardIdToURL(`${lastOpenedFile.realmURL}${lastOpenedFile.filePath}`) + : cardIdToURL(id), ); this.updateSubmode(Submodes.Interact); this._state.workspaceChooserOpened = false; - this.cachedRealmURL = new URL(realmUrl); + this.cachedRealmURL = cardIdToURL(realmUrl); }; get workspaceChooserOpened() { diff --git a/packages/host/app/services/realm-server.ts b/packages/host/app/services/realm-server.ts index 7d79cef9bb9..ddf84bd5b2f 100644 --- a/packages/host/app/services/realm-server.ts +++ b/packages/host/app/services/realm-server.ts @@ -14,6 +14,7 @@ import { TrackedArray } from 'tracked-built-ins'; import { baseRealm, + cardIdToURL, ensureTrailingSlash, SupportedMimeType, Deferred, @@ -173,7 +174,7 @@ export default class RealmServerService extends Service { let { data: { id: realmURL }, } = (await response.json()) as { data: { id: string } }; - return new URL(realmURL); + return cardIdToURL(realmURL); } async deleteRealm(realmURL: string) { diff --git a/packages/host/app/services/recent-files-service.ts b/packages/host/app/services/recent-files-service.ts index 791437ffe43..a15e5cdc0e5 100644 --- a/packages/host/app/services/recent-files-service.ts +++ b/packages/host/app/services/recent-files-service.ts @@ -7,7 +7,7 @@ import { tracked } from '@glimmer/tracking'; import window from 'ember-window-mock'; import { TrackedArray } from 'tracked-built-ins'; -import { RealmPaths } from '@cardstack/runtime-common'; +import { cardIdToURL, RealmPaths } from '@cardstack/runtime-common'; import type { LocalPath } from '@cardstack/runtime-common/paths'; import { RecentFiles } from '../utils/local-storage-keys'; @@ -15,7 +15,7 @@ import { RecentFiles } from '../utils/local-storage-keys'; import type OperatorModeStateService from './operator-mode-state-service'; import type ResetService from './reset'; -type SerialRecentFile = [URL, string, CursorPosition, number]; +type SerialRecentFile = [string, string, CursorPosition, number]; export type CursorPosition = { line: number; @@ -63,7 +63,7 @@ export default class RecentFilesService extends Service { } removeRecentFilesForRealmURL(url: string) { - let realmURL = new RealmPaths(new URL(url)).url; + let realmURL = new RealmPaths(cardIdToURL(url)).url; let removedAny = false; for (let index = this.recentFiles.length - 1; index >= 0; index--) { @@ -87,8 +87,8 @@ export default class RecentFilesService extends Service { let realmURL = this.operatorModeStateService.realmURL; if (realmURL) { - let realmPaths = new RealmPaths(new URL(realmURL)); - let url = new URL(urlString); + let realmPaths = new RealmPaths(cardIdToURL(realmURL)); + let url = cardIdToURL(urlString); if (realmPaths.inRealm(url)) { this.addRecentFile(realmPaths.local(url)); @@ -116,7 +116,7 @@ export default class RecentFilesService extends Service { } this.recentFiles.unshift({ - realmURL: new URL(currentRealmUrl), + realmURL: cardIdToURL(currentRealmUrl), filePath: file, cursorPosition: cursorPosition ?? null, timestamp: Date.now(), @@ -136,7 +136,7 @@ export default class RecentFilesService extends Service { findRecentFileByRealmURL(url: string) { return this.recentFiles.find((recentFile) => { - const realmUrl = new RealmPaths(new URL(url)).url; + const realmUrl = new RealmPaths(cardIdToURL(url)).url; return realmUrl === recentFile.realmURL.href; }); } @@ -195,7 +195,7 @@ export default class RecentFilesService extends Service { [realmUrl, filePath, cursorPosition, timestamp]: SerialRecentFile, ) { try { - let url = new URL(realmUrl); + let url = cardIdToURL(realmUrl); recentFiles.push({ realmURL: url, filePath, diff --git a/packages/host/app/services/store.ts b/packages/host/app/services/store.ts index 1367ca19f69..f40ef1e2bcc 100644 --- a/packages/host/app/services/store.ts +++ b/packages/host/app/services/store.ts @@ -279,7 +279,7 @@ export default class StoreService extends Service implements StoreInterface { ? instanceOrError[realmURLSymbol]?.href : instanceOrError.realm; if (realmURL) { - this.subscribeToRealm(new URL(realmURL)); + this.subscribeToRealm(cardIdToURL(realmURL)); } } } else { @@ -687,7 +687,7 @@ export default class StoreService extends Service implements StoreInterface { } > { let normalizedRealms = (realms ?? []) - .map((realm) => new RealmPaths(new URL(realm)).url) + .map((realm) => new RealmPaths(cardIdToURL(realm)).url) .filter(Boolean); let searchRealms = normalizedRealms.length > 0 @@ -2173,7 +2173,7 @@ function resolveDocUrl(id?: string, realm?: string, local?: string) { if (!realm) { throw new Error('Cannot resolve target url without a realm'); } - let path = new RealmPaths(new URL(realm)); + let path = new RealmPaths(cardIdToURL(realm)); if (local) { return path.directoryURL(local).href; } diff --git a/packages/host/tests/unit/garbage-collection-test.ts b/packages/host/tests/unit/garbage-collection-test.ts index cff3b1ee93d..0631a23183a 100644 --- a/packages/host/tests/unit/garbage-collection-test.ts +++ b/packages/host/tests/unit/garbage-collection-test.ts @@ -749,6 +749,36 @@ module('Unit | identity-context garbage collection', function (hooks) { ); }); + test('does not overwrite id for a saved instance when tail-correlation resolves an alias id', async function (assert) { + let { + store, + instances: { hassan }, + } = await setupTest(async ({ store, hassan }) => { + await saveCard( + hassan, + `${testRealmURL}${hassan[localId]}`, + loader, + store, + ); + }); + let originalId = hassan.id; + if (!originalId) { + throw new Error('expected hassan to have a remote id'); + } + let aliasId = `https://alias.example/${originalId.split('/').pop()}`; + + assert.strictEqual( + store.getCardInstanceOrError(aliasId), + hassan, + 'card instance is returned via tail-correlation lookup', + ); + assert.strictEqual( + hassan.id, + originalId, + 'saved instance keeps its original id', + ); + }); + test('return a stale instance when the server state reflects an error for an id', async function (assert) { let { store, diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index bf5d0a12a21..47a4d1e4ab8 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -28,7 +28,9 @@ import { visitInstanceURLs, maybeRelativeURL, codeRefFromInternalKey, + unresolveResourceInstanceURLs, } from '.'; +import { canonicalURL } from './index-runner/dependency-url'; import type { Realm } from './realm'; import { FILE_META_RESERVED_KEYS } from './realm'; import { RealmPaths } from './paths'; @@ -663,10 +665,14 @@ export class RealmIndexQueryEngine { } for (let result of realmResults) { - if (!result?.id || seen.has(result.id)) { + if (!result?.id) { continue; } - seen.add(result.id); + let canonicalResultId = canonicalURL(result.id); + if (seen.has(canonicalResultId)) { + continue; + } + seen.add(canonicalResultId); aggregated.push(result); } @@ -887,10 +893,11 @@ export class RealmIndexQueryEngine { opts?: Options, ): Promise<(CardResource | FileMetaResource)[]> { if (resource.id != null) { - if (visited.includes(resource.id)) { + let canonicalId = canonicalURL(resource.id); + if (visited.includes(canonicalId)) { return []; } - visited.push(resource.id); + visited.push(canonicalId); } let realmPath = new RealmPaths(realmURL); let processedRelationships = new Set(); @@ -1012,11 +1019,17 @@ export class RealmIndexQueryEngine { recursiveOpts, )) { foundLinks = true; - if ( - includedResource.id && - !omit.includes(includedResource.id) && - !included.find((r) => r.id === includedResource.id) - ) { + if (!includedResource.id) { + continue; + } + const id = includedResource.id; + let isAlreadyOmitted = omit.some((item) => + item ? canonicalURL(item) === canonicalURL(id) : false, + ); + let isAlreadyIncluded = included.some((r) => + r.id ? canonicalURL(r.id) === canonicalURL(id) : false, + ); + if (!isAlreadyOmitted && !isAlreadyIncluded) { let rewrittenResource = cloneDeep({ ...includedResource, ...{ links: { self: includedResource.id } }, @@ -1027,6 +1040,7 @@ export class RealmIndexQueryEngine { visitModuleDeps(rewrittenResource, (url, setURL) => absolutizeInstanceURL(url, rewrittenResource.id, setURL), ); + unresolveResourceInstanceURLs(rewrittenResource); included.push(rewrittenResource); } } @@ -1051,11 +1065,13 @@ export class RealmIndexQueryEngine { // Use prefix form (e.g. @cardstack/catalog/...) when available, // so relationship data.id stays portable across environments. let relationshipIdStr = unresolveCardReference(relationshipId.href); - if ( - foundLinks || - omit.includes(relationshipIdStr) || - included.find((i) => i.id === relationshipIdStr) - ) { + let isOmitted = omit.some((item) => + item ? canonicalURL(item) === canonicalURL(relationshipIdStr) : false, + ); + let isIncluded = included.some((i) => + i.id ? canonicalURL(i.id) === canonicalURL(relationshipIdStr) : false, + ); + if (foundLinks || isOmitted || isIncluded) { relationship.data = { type: linkResource?.type ?? CardResourceType, id: relationshipIdStr, diff --git a/packages/runtime-common/search-utils.ts b/packages/runtime-common/search-utils.ts index 58c091e5f27..a6c59866115 100644 --- a/packages/runtime-common/search-utils.ts +++ b/packages/runtime-common/search-utils.ts @@ -10,6 +10,7 @@ import type { PrerenderedCardCollectionDocument, } from './document-types'; import { SupportedMimeType } from './router'; +import { canonicalURL } from './index-runner/dependency-url'; export type SearchRequestErrorCode = | 'missing-realms' @@ -268,10 +269,11 @@ export function combineSearchResults( if (doc.included) { for (let resource of doc.included) { if (resource.id) { - if (includedById.has(resource.id)) { + let canonicalId = canonicalURL(resource.id); + if (includedById.has(canonicalId)) { continue; } - includedById.add(resource.id); + includedById.add(canonicalId); } included.push(resource); }