diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index 9bf3ac10e9..39e7cd64cc 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -48,6 +48,7 @@ import { type RealmInfo, } from '@cardstack/runtime-common'; import type { ComponentLike } from '@glint/template'; +import { initSharedState } from './shared-state'; export { primitive, isField, type BoxComponent }; export const serialize = Symbol.for('cardstack-serialize'); @@ -170,14 +171,29 @@ function isStaleValue(value: any): value is StaleValue { return false; } } -const deserializedData = new WeakMap>(); -const recomputePromises = new WeakMap>(); -const identityContexts = new WeakMap(); -const subscribers = new WeakMap>(); +const deserializedData = initSharedState( + 'deserializedData', + () => new WeakMap>(), +); +const recomputePromises = initSharedState( + 'recomputePromises', + () => new WeakMap>(), +); +const identityContexts = initSharedState( + 'identityContexts', + () => new WeakMap(), +); +const subscribers = initSharedState( + 'subscribers', + () => new WeakMap>(), +); // our place for notifying Glimmer when a card is ready to re-render (which will // involve rerunning async computed fields) -const cardTracking = new TrackedWeakMap(); +const cardTracking = initSharedState( + 'cardTracking', + () => new TrackedWeakMap(), +); const isBaseInstance = Symbol.for('isBaseInstance'); diff --git a/packages/base/field-component.gts b/packages/base/field-component.gts index bf1ea7b2fa..6eca02d93b 100644 --- a/packages/base/field-component.gts +++ b/packages/base/field-component.gts @@ -16,6 +16,7 @@ import { getField } from '@cardstack/runtime-common'; import type { ComponentLike } from '@glint/template'; import { CardContainer } from '@cardstack/boxel-ui/components'; import Modifier from 'ember-modifier'; +import { initSharedState } from './shared-state'; import { eq } from '@cardstack/boxel-ui/helpers'; interface BoxComponentSignature { @@ -25,7 +26,10 @@ interface BoxComponentSignature { export type BoxComponent = ComponentLike; -const componentCache = new WeakMap, BoxComponent>(); +const componentCache = initSharedState( + 'componentCache', + () => new WeakMap, BoxComponent>(), +); export function getBoxComponent( card: typeof BaseDef, diff --git a/packages/base/room.gts b/packages/base/room.gts index 8dcf98c1f5..2ed4f9bc68 100644 --- a/packages/base/room.gts +++ b/packages/base/room.gts @@ -21,6 +21,7 @@ import { } from '@cardstack/runtime-common'; //@ts-expect-error cached type not available yet import { cached } from '@glimmer/tracking'; +import { initSharedState } from './shared-state'; import BooleanField from './boolean'; // this is so we can have triple equals equivalent room member cards @@ -277,14 +278,26 @@ interface RoomState { // in addition to acting as a cache, this also ensures we have // triple equal equivalence for the interior cards of RoomField -const eventCache = new WeakMap>(); -const messageCache = new WeakMap>(); -const roomMemberCache = new WeakMap>(); -const roomStateCache = new WeakMap(); -const fragmentCache = new WeakMap< - RoomField, - Map ->(); +const eventCache = initSharedState( + 'eventCache', + () => new WeakMap>(), +); +const messageCache = initSharedState( + 'messageCache', + () => new WeakMap>(), +); +const roomMemberCache = initSharedState( + 'roomMemberCache', + () => new WeakMap>(), +); +const roomStateCache = initSharedState( + 'roomStateCache', + () => new WeakMap(), +); +const fragmentCache = initSharedState( + 'fragmentCache', + () => new WeakMap>(), +); export class RoomField extends FieldDef { static displayName = 'Room'; diff --git a/packages/base/shared-state.ts b/packages/base/shared-state.ts new file mode 100644 index 0000000000..100dae00b6 --- /dev/null +++ b/packages/base/shared-state.ts @@ -0,0 +1,25 @@ +/* + This module needs to exist so long as we have multiple loaders cooperating in + the same environment. It ensures that any shared global state is really + global. Ultimately we would like to get to the point where the loader itself + is scoped so broadly that there's only one, and module-scoped state is safe to + treat as global. +*/ + +const bucket: Map = (() => { + let g = globalThis as unknown as { + __card_api_shared_state: Map | undefined; + }; + if (!g.__card_api_shared_state) { + g.__card_api_shared_state = new Map(); + } + return g.__card_api_shared_state; +})(); + +export function initSharedState(key: string, fn: () => T): T { + if (bucket.has(key)) { + return bucket.get(key) as T; + } + bucket.set(key, fn()); + return bucket.get(key) as T; +} diff --git a/packages/host/app/components/operator-mode/interact-submode.gts b/packages/host/app/components/operator-mode/interact-submode.gts index e239d624e6..f297a5d3fd 100644 --- a/packages/host/app/components/operator-mode/interact-submode.gts +++ b/packages/host/app/components/operator-mode/interact-submode.gts @@ -167,7 +167,7 @@ export default class InteractSubmode extends Component { doc, relativeTo, ); - await here.cardService.saveModel(here, newCard); + let newItem = new StackItem({ owner: here, card: newCard, @@ -176,6 +176,13 @@ export default class InteractSubmode extends Component { isLinkedCard: opts?.isLinkedCard, stackIndex, }); + + // TODO: it is important saveModel happens after newItem because it + // looks like perhaps there is a race condition (or something else) when a + // new linked card is created, and when it is added to the stack and closed + // - the parent card is not updated with the new linked card + await here.cardService.saveModel(here, newCard); + await newItem.ready(); here.addToStack(newItem); return await newItem.request?.promise; diff --git a/packages/host/app/lib/externals.ts b/packages/host/app/lib/externals.ts index 2a94ebcce8..774b6965f6 100644 --- a/packages/host/app/lib/externals.ts +++ b/packages/host/app/lib/externals.ts @@ -29,41 +29,46 @@ import * as boxelUiHelpers from '@cardstack/boxel-ui/helpers'; import * as boxelUiIcons from '@cardstack/boxel-ui/icons'; import * as runtime from '@cardstack/runtime-common'; -import { Loader } from '@cardstack/runtime-common/loader'; +import { VirtualNetwork } from '@cardstack/runtime-common'; -export function shimExternals(loader: Loader) { - loader.shimModule('@cardstack/runtime-common', runtime); - loader.shimModule('@cardstack/boxel-ui/components', boxelUiComponents); - loader.shimModule('@cardstack/boxel-ui/helpers', boxelUiHelpers); - loader.shimModule('@cardstack/boxel-ui/icons', boxelUiIcons); - loader.shimModule('@glimmer/component', glimmerComponent); - loader.shimModule('@ember/component', emberComponent); - loader.shimModule( +export function shimExternals(virtualNetwork: VirtualNetwork) { + virtualNetwork.shimModule('@cardstack/runtime-common', runtime); + virtualNetwork.shimModule( + '@cardstack/boxel-ui/components', + boxelUiComponents, + ); + virtualNetwork.shimModule('@cardstack/boxel-ui/helpers', boxelUiHelpers); + virtualNetwork.shimModule('@cardstack/boxel-ui/icons', boxelUiIcons); + virtualNetwork.shimModule('@glimmer/component', glimmerComponent); + virtualNetwork.shimModule('@ember/component', emberComponent); + virtualNetwork.shimModule( '@ember/component/template-only', emberComponentTemplateOnly, ); - loader.shimModule('ember-css-url', cssUrl); - loader.shimModule('@ember/template-factory', emberTemplateFactory); - loader.shimModule('@ember/template', emberTemplate); - loader.shimModule('@glimmer/tracking', glimmerTracking); - loader.shimModule('@ember/object', emberObject); - loader.shimModule('@ember/object/internals', emberObjectInternals); - loader.shimModule('@ember/helper', emberHelper); - loader.shimModule('@ember/modifier', emberModifier); - loader.shimModule('ember-resources', emberResources); - loader.shimModule('ember-concurrency', emberConcurrency); - loader.shimModule( + virtualNetwork.shimModule('ember-css-url', cssUrl); + virtualNetwork.shimModule('@ember/template-factory', emberTemplateFactory); + virtualNetwork.shimModule('@ember/template', emberTemplate); + virtualNetwork.shimModule('@glimmer/tracking', glimmerTracking); + virtualNetwork.shimModule('@ember/object', emberObject); + virtualNetwork.shimModule('@ember/object/internals', emberObjectInternals); + virtualNetwork.shimModule('@ember/helper', emberHelper); + virtualNetwork.shimModule('@ember/modifier', emberModifier); + virtualNetwork.shimModule('ember-resources', emberResources); + virtualNetwork.shimModule('ember-concurrency', emberConcurrency); + virtualNetwork.shimModule( 'ember-concurrency/-private/async-arrow-runtime', emberConcurrencyAsyncArrowRuntime, ); - loader.shimModule('ember-modifier', emberModifier2); - loader.shimModule('flat', flat); - loader.shimModule('lodash', lodash); - loader.shimModule('tracked-built-ins', tracked); - loader.shimModule('date-fns', dateFns); - loader.shimModule('@ember/destroyable', emberDestroyable); - loader.shimModule('marked', marked); - loader.shimModule('ethers', ethers); - loader.shimModule('ember-source/types', { default: class {} }); - loader.shimModule('ember-source/types/preview', { default: class {} }); + virtualNetwork.shimModule('ember-modifier', emberModifier2); + virtualNetwork.shimModule('flat', flat); + virtualNetwork.shimModule('lodash', lodash); + virtualNetwork.shimModule('tracked-built-ins', tracked); + virtualNetwork.shimModule('date-fns', dateFns); + virtualNetwork.shimModule('@ember/destroyable', emberDestroyable); + virtualNetwork.shimModule('marked', marked); + virtualNetwork.shimModule('ethers', ethers); + virtualNetwork.shimModule('ember-source/types', { default: class {} }); + virtualNetwork.shimModule('ember-source/types/preview', { + default: class {}, + }); } diff --git a/packages/host/app/services/loader-service.ts b/packages/host/app/services/loader-service.ts index c142442ed2..edb76b407d 100644 --- a/packages/host/app/services/loader-service.ts +++ b/packages/host/app/services/loader-service.ts @@ -1,11 +1,10 @@ import Service, { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; -import { baseRealm } from '@cardstack/runtime-common'; +import { VirtualNetwork, baseRealm } from '@cardstack/runtime-common'; import { Loader } from '@cardstack/runtime-common/loader'; import config from '@cardstack/host/config/environment'; -import { shimExternals } from '@cardstack/host/lib/externals'; import { type RealmSessionResource, getRealmSession, @@ -13,6 +12,8 @@ import { import MatrixService from '@cardstack/host/services/matrix-service'; import RealmInfoService from '@cardstack/host/services/realm-info-service'; +import { shimExternals } from '../lib/externals'; + export default class LoaderService extends Service { @service declare fastboot: { isFastBoot: boolean }; @service private declare matrixService: MatrixService; @@ -24,10 +25,11 @@ export default class LoaderService extends Service { // which in turn assures the resources will not get torn down. private realmSessions: Map = new Map(); + virtualNetwork = new VirtualNetwork(); + reset() { if (this.loader) { this.loader = Loader.cloneLoader(this.loader); - shimExternals(this.loader); } else { this.loader = this.makeInstance(); } @@ -35,18 +37,18 @@ export default class LoaderService extends Service { private makeInstance() { if (this.fastboot.isFastBoot) { - let loader = new Loader(); - shimExternals(loader); + let loader = this.virtualNetwork.createLoader(); + shimExternals(this.virtualNetwork); return loader; } - let loader = new Loader(); + let loader = this.virtualNetwork.createLoader(); loader.addURLMapping( new URL(baseRealm.url), new URL(config.resolvedBaseRealmURL), ); loader.prependURLHandlers([(req) => this.fetchWithAuth(req)]); - shimExternals(loader); + shimExternals(this.virtualNetwork); return loader; } diff --git a/packages/host/tests/acceptance/basic-test.gts b/packages/host/tests/acceptance/basic-test.gts index 21f7fe38a5..be7b297015 100644 --- a/packages/host/tests/acceptance/basic-test.gts +++ b/packages/host/tests/acceptance/basic-test.gts @@ -28,8 +28,10 @@ module('Acceptance | basic tests', function (hooks) { hooks.beforeEach(async function () { window.localStorage.removeItem('recent-files'); - let loader = (this.owner.lookup('service:loader-service') as LoaderService) - .loader; + let loaderService = this.owner.lookup( + 'service:loader-service', + ) as LoaderService; + let loader = loaderService.loader; let { field, contains, CardDef, Component } = await loader.import< typeof import('https://cardstack.com/base/card-api') >(`${baseRealm.url}card-api`); diff --git a/packages/host/tests/acceptance/interact-submode-test.gts b/packages/host/tests/acceptance/interact-submode-test.gts index b7c4114288..61dc6aa9a9 100644 --- a/packages/host/tests/acceptance/interact-submode-test.gts +++ b/packages/host/tests/acceptance/interact-submode-test.gts @@ -313,6 +313,8 @@ module('Acceptance | interact submode tests', function (hooks) { assert.dom('[data-test-search-sheet]').doesNotHaveClass('results'); // Search closed // The card appears on a new stack + await waitFor('[data-test-operator-mode-stack]'); + assert.dom('[data-test-operator-mode-stack]').exists({ count: 1 }); assert .dom( @@ -640,6 +642,7 @@ module('Acceptance | interact submode tests', function (hooks) { assert.dom('[data-test-search-sheet]').doesNotHaveClass('prompt'); // Search closed // There are now 2 stacks + assert.dom('[data-test-operator-mode-stack]').exists({ count: 2 }); assert.dom('[data-test-operator-mode-stack="0"]').includesText('Mango'); // Mango goes on the left stack assert.dom('[data-test-operator-mode-stack="1"]').includesText('Fadhlan'); @@ -668,6 +671,7 @@ module('Acceptance | interact submode tests', function (hooks) { assert.dom('[data-test-search-sheet]').doesNotHaveClass('prompt'); // Search closed // There are now 2 stacks + await waitFor('[data-test-operator-mode-stack="0"]'); assert.dom('[data-test-operator-mode-stack]').exists({ count: 2 }); assert.dom('[data-test-operator-mode-stack="0"]').includesText('Fadhlan'); assert.dom('[data-test-operator-mode-stack="1"]').includesText('Mango'); // Mango gets move onto the right stack @@ -1357,6 +1361,7 @@ module('Acceptance | interact submode tests', function (hooks) { ); }, }); + await waitUntil(() => document .querySelector('[data-test-operator-mode-stack="0"] [data-test-person]') diff --git a/packages/host/tests/helpers/index.gts b/packages/host/tests/helpers/index.gts index 90e6d01839..7678a2fa41 100644 --- a/packages/host/tests/helpers/index.gts +++ b/packages/host/tests/helpers/index.gts @@ -471,14 +471,14 @@ async function setupTestRealm({ isAcceptanceTest?: boolean; permissions?: RealmPermissions; }) { + let owner = (getContext() as TestContext).owner; + realmURL = realmURL ?? testRealmURL; + for (const [path, mod] of Object.entries(contents)) { if (path.endsWith('.gts') && typeof mod !== 'string') { - await shimModule( - `${realmURL}${path.replace(/\.gts$/, '')}`, - mod as object, - loader, - ); + let moduleURLString = `${realmURL}${path.replace(/\.gts$/, '')}`; + loader.shimModule(moduleURLString, mod as object); } } let api = await loader.import(`${baseRealm.url}card-api`); @@ -506,7 +506,6 @@ async function setupTestRealm({ } } let adapter = new TestRealmAdapter(flatFiles, new URL(realmURL)); - let owner = (getContext() as TestContext).owner; if (isAcceptanceTest) { await visit('/acceptance-test-setup'); } else { @@ -567,22 +566,6 @@ export async function saveCard(instance: CardDef, id: string, loader: Loader) { return doc; } -export async function shimModule( - moduleURL: string, - module: Record, - loader: Loader, -) { - if (loader) { - loader.shimModule(moduleURL, module); - } - await Promise.all( - Object.keys(module).map(async (name) => { - let m = await loader.import(moduleURL); - m[name]; - }), - ); -} - export function setupCardLogs( hooks: NestedHooks, apiThunk: () => Promise, diff --git a/packages/host/tests/integration/components/card-basics-test.gts b/packages/host/tests/integration/components/card-basics-test.gts index ab5736ed2a..c385d2c614 100644 --- a/packages/host/tests/integration/components/card-basics-test.gts +++ b/packages/host/tests/integration/components/card-basics-test.gts @@ -34,7 +34,6 @@ import { cleanWhiteSpace, p, testRealmURL, - shimModule, setupCardLogs, saveCard, } from '../../helpers'; @@ -163,7 +162,7 @@ module('Integration | card-basics', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Post, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Post, Person }); let helloWorld = new Post({ title: 'First Post', @@ -413,7 +412,7 @@ module('Integration | card-basics', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Guest, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Guest, Person }); let g1 = new Guest({ name: 'Madeleine', @@ -470,7 +469,7 @@ module('Integration | card-basics', function (hooks) { class Person extends CardDef { @field firstName = contains(StringField); } - await shimModule(`${testRealmURL}test-cards`, { Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Person }); // deserialize a card with an ID to mark it as "saved" let card = new Person({ firstName: 'Mango' }); @@ -782,7 +781,7 @@ module('Integration | card-basics', function (hooks) { @field firstName = contains(StringField); @field pet = linksTo(Pet); } - await shimModule(`${testRealmURL}test-cards`, { Person, Pet }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Person, Pet }); let mango = new Pet({ firstName: 'Mango', @@ -879,7 +878,7 @@ module('Integration | card-basics', function (hooks) { @field firstName = contains(StringField); @field pets = linksToMany(Pet); } - await shimModule(`${testRealmURL}test-cards`, { Person, Pet }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Person, Pet }); let mango = new Pet({ firstName: 'Mango', @@ -967,7 +966,7 @@ module('Integration | card-basics', function (hooks) { // @ts-expect-error Have to purposefully bypass type-checking in order to get into this runtime error state @field pet = linksTo(StringField); } - await shimModule(`${testRealmURL}test-cards`, { Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Person }); try { new Person({ firstName: 'Hassan', pet: 'Mango' }); @@ -1006,11 +1005,7 @@ module('Integration | card-basics', function (hooks) { @field firstName = contains(StringField); @field pet = linksTo(Pet); } - await shimModule( - `${testRealmURL}test-cards`, - { Person, Pet, NotAPet }, - loader, - ); + loader.shimModule(`${testRealmURL}test-cards`, { Person, Pet, NotAPet }); let door = new NotAPet({ firstName: 'door' }); try { @@ -1060,7 +1055,7 @@ module('Integration | card-basics', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Person, Pet }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Person, Pet }); let vanGogh = new Pet({ firstName: 'Van Gogh' }); let mango = new Pet({ firstName: 'Mango', friend: vanGogh }); @@ -1149,7 +1144,7 @@ module('Integration | card-basics', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Post, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Post, Person }); let helloWorld = new Post({ author: new Person({ @@ -1196,16 +1191,12 @@ module('Integration | card-basics', function (hooks) { }; } - await shimModule( - `${testRealmURL}test-cards`, - { - Post, - Person, - TestNumber, - TestString, - }, - loader, - ); + loader.shimModule(`${testRealmURL}test-cards`, { + Post, + Person, + TestNumber, + TestString, + }); let helloWorld = new Post({ author: new Person({ firstName: 'Arthur', number: 10 }), @@ -1234,7 +1225,7 @@ module('Integration | card-basics', function (hooks) { @field title = contains(title); @field author = contains(Person); } - await shimModule(`${testRealmURL}test-cards`, { Post, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Post, Person }); let helloWorld = new Post({ title: 'First Post', @@ -1260,7 +1251,7 @@ module('Integration | card-basics', function (hooks) { }, }); } - await shimModule(`${testRealmURL}test-cards`, { Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Person }); let helloWorld = new Person({ firstName: 'Arthur', lastName: 'M', @@ -1300,7 +1291,7 @@ module('Integration | card-basics', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Person }); let helloWorld = new Person({ firstName: 'Arthur', age: 10 }); await renderCard(loader, helloWorld, 'atom'); @@ -1376,7 +1367,7 @@ module('Integration | card-basics', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Family, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Family, Person }); let abdelRahmans = new Family({ people: [ @@ -1452,7 +1443,7 @@ module('Integration | card-basics', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Family, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Family, Person }); let abdelRahmans = new Family({ people: [ @@ -1498,7 +1489,7 @@ module('Integration | card-basics', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Family, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Family, Person }); let mango = new Person({ firstName: 'Mango', }); @@ -1559,16 +1550,12 @@ module('Integration | card-basics', function (hooks) { }; } - await shimModule( - `${testRealmURL}test-cards`, - { - Person, - Employee, - Customer, - Group, - }, - loader, - ); + loader.shimModule(`${testRealmURL}test-cards`, { + Person, + Employee, + Customer, + Group, + }); let group = new Group({ people: [ @@ -1708,7 +1695,7 @@ module('Integration | card-basics', function (hooks) { @field firstName = contains(StringField); @field languagesSpoken = containsMany(StringField); } - await shimModule(`${testRealmURL}test-cards`, { Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Person }); assert.throws( () => new Person({ languagesSpoken: 'english' }), /Expected array for field value languagesSpoken/, @@ -1735,7 +1722,7 @@ module('Integration | card-basics', function (hooks) { @field title = contains(StringField); @field author = contains(Person); } - await shimModule(`${testRealmURL}test-cards`, { Post, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Post, Person }); let helloWorld = new Post({ title: 'My Post', @@ -1777,7 +1764,7 @@ module('Integration | card-basics', function (hooks) { }, }); } - await shimModule(`${testRealmURL}test-cards`, { Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Person }); let mango = new Person({ firstName: 'Mango', isCool: true }); let root = await renderCard(loader, mango, 'isolated'); @@ -1797,7 +1784,7 @@ module('Integration | card-basics', function (hooks) { @field isCool = contains(BooleanField); @field isHuman = contains(BooleanField); } - await shimModule(`${testRealmURL}test-cards`, { Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Person }); let mango = new Person({ firstName: 'Mango', isCool: true, @@ -1887,7 +1874,7 @@ module('Integration | card-basics', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Post, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Post, Person }); let helloWorld = new Post({ title: 'First Post', @@ -2183,11 +2170,7 @@ module('Integration | card-basics', function (hooks) { @field favoritePlaces = linksToMany(Country); } - await shimModule( - `${testRealmURL}test-cards`, - { Country, ContactCard }, - loader, - ); + loader.shimModule(`${testRealmURL}test-cards`, { Country, ContactCard }); let us = new Country({ countryName: 'United States', flag: 'πŸ‡ΊπŸ‡Έ' }); let canada = new Country({ countryName: 'Canada', flag: 'πŸ‡¨πŸ‡¦' }); diff --git a/packages/host/tests/integration/components/card-editor-test.gts b/packages/host/tests/integration/components/card-editor-test.gts index d2bca5676d..d39434e4ca 100644 --- a/packages/host/tests/integration/components/card-editor-test.gts +++ b/packages/host/tests/integration/components/card-editor-test.gts @@ -24,7 +24,6 @@ import { CardDef } from 'https://cardstack.com/base/card-api'; import { testRealmURL, - shimModule, setupCardLogs, setupLocalIndexing, saveCard, @@ -241,7 +240,7 @@ module('Integration | card-editor', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { TestCard }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { TestCard }); let card = new TestCard({ firstName: 'Mango', lastName: 'Abdel-Rahman' }); await saveCard(card, `${testRealmURL}test-cards/test-card`, loader); @@ -284,7 +283,7 @@ module('Integration | card-editor', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { TestCard }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { TestCard }); let card = new TestCard({ firstName: 'Mango' }); await saveCard(card, `${testRealmURL}test-cards/test-card`, loader); @@ -334,7 +333,7 @@ module('Integration | card-editor', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { TestCard }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { TestCard }); let card = new TestCard({ firstName: 'Mango' }); await saveCard(card, `${testRealmURL}test-cards/test-card`, loader); await renderComponent( diff --git a/packages/host/tests/integration/components/computed-test.gts b/packages/host/tests/integration/components/computed-test.gts index 5e0e847395..2ca3a15499 100644 --- a/packages/host/tests/integration/components/computed-test.gts +++ b/packages/host/tests/integration/components/computed-test.gts @@ -8,12 +8,7 @@ import { Loader } from '@cardstack/runtime-common/loader'; import type LoaderService from '@cardstack/host/services/loader-service'; -import { - cleanWhiteSpace, - testRealmURL, - shimModule, - setupCardLogs, -} from '../../helpers'; +import { cleanWhiteSpace, testRealmURL, setupCardLogs } from '../../helpers'; import { renderCard } from '../../helpers/render-component'; let cardApi: typeof import('https://cardstack.com/base/card-api'); @@ -107,7 +102,7 @@ module('Integration | computeds', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Post, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Post, Person }); let firstPost = new Post({ title: 'First Post', @@ -277,7 +272,7 @@ module('Integration | computeds', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Post, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Post, Person }); let firstPost = new Post({ title: 'First Post', @@ -412,7 +407,7 @@ module('Integration | computeds', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Family, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Family, Person }); let abdelRahmans = new Family({ people: [ @@ -495,7 +490,7 @@ module('Integration | computeds', function (hooks) { return totalAge; } } - await shimModule(`${testRealmURL}test-cards`, { Family, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Family, Person }); let family = new Family({ people: [ @@ -571,7 +566,7 @@ module('Integration | computeds', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { Location, Person }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { Location, Person }); let person = new Person({ firstName: 'Mango', diff --git a/packages/host/tests/integration/components/operator-mode-test.gts b/packages/host/tests/integration/components/operator-mode-test.gts index 99cef66a4f..8668042630 100644 --- a/packages/host/tests/integration/components/operator-mode-test.gts +++ b/packages/host/tests/integration/components/operator-mode-test.gts @@ -1276,9 +1276,8 @@ module('Integration | operator-mode', function (hooks) { await click(`[data-test-select="${testRealmURL}Person/fadhlan"]`); await click('[data-test-card-catalog-go-button]'); - assert - .dom(`[data-test-stack-card="${testRealmURL}Person/fadhlan"]`) - .isVisible(); + + await waitFor(`[data-test-stack-card="${testRealmURL}Person/fadhlan"]`); }); test('displays cards on cards-grid and includes `catalog-entry` instances', async function (assert) { @@ -1390,6 +1389,7 @@ module('Integration | operator-mode', function (hooks) { await waitFor(`[data-test-cards-grid-item]`); await click(`[data-test-cards-grid-item="${testRealmURL}Person/burcu"]`); + await waitFor(`[data-test-stack-card-index="1"]`); assert.dom(`[data-test-stack-card-index="1"]`).exists(); // Opens card on the stack assert .dom(`[data-test-stack-card-index="1"] [data-test-boxel-header-title]`) @@ -1400,7 +1400,7 @@ module('Integration | operator-mode', function (hooks) { }); test('create new card editor opens in the stack at each nesting level', async function (assert) { - assert.expect(11); + assert.expect(9); await setCardInOperatorModeState(`${testRealmURL}grid`); await renderComponent( class TestDriver extends GlimmerComponent { @@ -1479,9 +1479,13 @@ module('Integration | operator-mode', function (hooks) { await click('[data-test-stack-card-index="3"] [data-test-close-button]'); await waitFor('[data-test-stack-card-index="3"]', { count: 0 }); - assert - .dom('[data-test-stack-card-index="2"] [data-test-field="authorBio"]') - .containsText('Alice Enwunder'); + await waitUntil(() => + /Alice\s*Enwunder/.test( + document.querySelector( + '[data-test-stack-card-index="2"] [data-test-field="authorBio"]', + )!.textContent!, + ), + ); await click('[data-test-stack-card-index="2"] [data-test-close-button]'); await waitFor('[data-test-stack-card-index="2"]', { count: 0 }); @@ -1508,11 +1512,14 @@ module('Integration | operator-mode', function (hooks) { }); await click('[data-test-stack-card-index="1"] [data-test-edit-button]'); - assert - .dom(`[data-test-stack-card="${packetId}"]`) - .containsText( - 'Everyone knows that Alice ran the show in the Brady household.', - ); + + await waitUntil(() => + document + .querySelector(`[data-test-stack-card="${packetId}"]`) + ?.textContent?.includes( + 'Everyone knows that Alice ran the show in the Brady household.', + ), + ); }); test('can choose a card for a linksTo field that has an existing value', async function (assert) { @@ -1878,6 +1885,7 @@ module('Integration | operator-mode', function (hooks) { await waitFor(`[data-test-stack-card="${testRealmURL}grid"]`); await waitFor(`[data-test-cards-grid-item]`); await click(`[data-test-cards-grid-item="${testRealmURL}Person/fadhlan"]`); + await waitFor(`[data-test-stack-card-index="1"]`); assert.dom(`[data-test-stack-card-index="1"]`).exists(); await waitFor('[data-test-person]'); @@ -1905,6 +1913,7 @@ module('Integration | operator-mode', function (hooks) { await waitFor(`[data-test-cards-grid-item]`); await click(`[data-test-cards-grid-item="${testRealmURL}Person/fadhlan"]`); + await waitFor(`[data-test-stack-card-index="1"]`); assert.dom(`[data-test-stack-card-index="1"]`).exists(); assert .dom( @@ -1931,7 +1940,8 @@ module('Integration | operator-mode', function (hooks) { await waitFor(`[data-test-cards-grid-item]`); await click(`[data-test-cards-grid-item="${testRealmURL}Person/fadhlan"]`); - assert.dom(`[data-test-stack-card-index="1"]`).exists(); + await waitFor(`[data-test-stack-card-index="1"]`); + assert .dom( `[data-test-stack-card="${testRealmURL}Person/fadhlan"] [data-test-boxel-header-title]`, @@ -2510,7 +2520,7 @@ module('Integration | operator-mode', function (hooks) { assert.dom('[data-test-overlay-selected]').doesNotExist(); await click(`[data-test-cards-grid-item="${testRealmURL}Person/fadhlan"]`); - assert.dom(`[data-test-stack-card-index="1"]`).exists(); + await waitFor(`[data-test-stack-card-index="1"]`, { count: 1 }); }); test('displays realm name as header title when hovering realm icon', async function (assert) { @@ -3048,17 +3058,17 @@ module('Integration | operator-mode', function (hooks) { }, ); - let savedCards = new Set(); - this.onSave((url) => savedCards.add(url.href)); + await waitFor(`[data-test-stack-card="${testRealmURL}BlogPost/2"]`); await click('[data-test-edit-button]'); assert.dom('[data-test-add-new]').exists(); await click('[data-test-add-new]'); await waitFor(`[data-test-card-catalog-modal]`); await click(`[data-test-card-catalog-create-new-button]`); - await waitFor('[data-test-edit-button]'); + await waitFor('[data-test-stack-card-index="1"]'); + await click('[data-test-edit-button]'); - await waitFor('[data-test-isolated-author'); + await waitFor('[data-test-isolated-author]'); assert.dom('[data-test-isolated-author]').exists(); }); }); diff --git a/packages/host/tests/integration/components/preview-test.gts b/packages/host/tests/integration/components/preview-test.gts index 18d3ac9ba7..6063998526 100644 --- a/packages/host/tests/integration/components/preview-test.gts +++ b/packages/host/tests/integration/components/preview-test.gts @@ -12,7 +12,7 @@ import Preview from '@cardstack/host/components/preview'; import type LoaderService from '@cardstack/host/services/loader-service'; -import { testRealmURL, shimModule } from '../../helpers'; +import { testRealmURL } from '../../helpers'; import { renderComponent } from '../../helpers/render-component'; let cardApi: typeof import('https://cardstack.com/base/card-api'); @@ -45,7 +45,7 @@ module('Integration | preview', function (hooks) { }; } - await shimModule(`${testRealmURL}test-cards`, { TestCard }, loader); + loader.shimModule(`${testRealmURL}test-cards`, { TestCard }); let card = new TestCard({ firstName: 'Mango ' }); await renderComponent( class TestDriver extends GlimmerComponent { diff --git a/packages/host/tests/integration/components/serialization-test.gts b/packages/host/tests/integration/components/serialization-test.gts index 9e43968a44..be8a48dff9 100644 --- a/packages/host/tests/integration/components/serialization-test.gts +++ b/packages/host/tests/integration/components/serialization-test.gts @@ -16,13 +16,7 @@ import type LoaderService from '@cardstack/host/services/loader-service'; import { type CardDef as CardDefType } from 'https://cardstack.com/base/card-api'; -import { - p, - cleanWhiteSpace, - shimModule, - setupCardLogs, - saveCard, -} from '../../helpers'; +import { p, cleanWhiteSpace, setupCardLogs, saveCard } from '../../helpers'; import { renderCard } from '../../helpers/render-component'; @@ -83,7 +77,7 @@ module('Integration | serialization', function (hooks) { }; } - await shimModule(`${realmURL}test-cards`, { Post }, loader); + loader.shimModule(`${realmURL}test-cards`, { Post }); // initialize card data as serialized to force us to deserialize instead of using cached data let resource = { @@ -127,7 +121,7 @@ module('Integration | serialization', function (hooks) { }; } - await shimModule(`${realmURL}test-cards`, { Item }, loader); + loader.shimModule(`${realmURL}test-cards`, { Item }); // initialize card data as serialized to force us to deserialize instead of using cached data let resource = { @@ -160,7 +154,8 @@ module('Integration | serialization', function (hooks) { class Person extends CardDef { @field firstName = contains(StringField); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person }); // deserialize a card with an ID to mark it as "saved" let resource = { @@ -216,7 +211,8 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person }); let mango = new Person({ id: `${realmURL}Person/mango`, @@ -248,7 +244,8 @@ module('Integration | serialization', function (hooks) { @field firstName = contains(StringField); @field picture = contains(Base64ImageField); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person }); let mango = new Person({ firstName: 'Mango', @@ -283,7 +280,8 @@ module('Integration | serialization', function (hooks) { class Person extends CardDef { @field firstName = contains(StringField); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person }); let card = new Person({ id: `${realmURL}Person/mango`, @@ -329,7 +327,8 @@ module('Integration | serialization', function (hooks) { class Person extends CardDef { @field firstName = contains(StringField); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person }); // deserialize a card with an ID to mark it as "saved" let resource = { @@ -386,7 +385,8 @@ module('Integration | serialization', function (hooks) { }; } - await shimModule(`${realmURL}test-cards`, { DriverCard }, loader); + + loader.shimModule(`${realmURL}test-cards`, { DriverCard }); let ref = { module: `http://localhost:4202/test/person`, name: 'Person' }; let resource = { @@ -428,7 +428,8 @@ module('Integration | serialization', function (hooks) { }; } - await shimModule(`${realmURL}test-cards`, { DriverCard }, loader); + + loader.shimModule(`${realmURL}test-cards`, { DriverCard }); let ref = { module: `http://localhost:4202/test/person`, name: 'Person' }; let driver = new DriverCard({ ref }); @@ -453,7 +454,8 @@ module('Integration | serialization', function (hooks) { @field created = contains(DateField); @field published = contains(DatetimeField); } - await shimModule(`${realmURL}test-cards`, { Post }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Post }); // initialize card data as deserialized to force us to serialize instead of using cached data let firstPost = new Post({ @@ -489,7 +491,9 @@ module('Integration | serialization', function (hooks) { }; } - await shimModule(`${realmURL}test-cards`, { Post }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Post }); + let resource = { attributes: { title: 'First Post', @@ -552,7 +556,8 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }); let spookyToiletPaper = new Toy({ description: 'Toilet paper ghost: Poooo!', @@ -662,7 +667,7 @@ module('Integration | serialization', function (hooks) { @field firstName = contains(StringField); @field pet = linksTo(Pet); } - await shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }); let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -816,7 +821,9 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}test-cards`, { Person, Pet }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person, Pet }); + let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -921,7 +928,8 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}test-cards`, { Person, Pet }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person, Pet }); let hassan = new Person({ firstName: 'Hassan' }); @@ -984,7 +992,8 @@ module('Integration | serialization', function (hooks) { @field firstName = contains(StringField); @field pet = linksTo(Pet); } - await shimModule(`${realmURL}test-cards`, { Person, Pet }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person, Pet }); let doc: LooseSingleCardDocument = { data: { @@ -1044,7 +1053,8 @@ module('Integration | serialization', function (hooks) { @field favoriteToy = contains(Toy); @field toys = containsMany(Toy); } - await shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }); let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -1162,7 +1172,8 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person }); let mango = new Person({ firstName: 'Mango' }); let hassan = new Person({ firstName: 'Hassan', friend: mango }); @@ -1226,7 +1237,8 @@ module('Integration | serialization', function (hooks) { @field firstName = contains(StringField); @field friend = linksTo(() => Person); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person }); let doc: LooseSingleCardDocument = { data: { @@ -1300,7 +1312,8 @@ module('Integration | serialization', function (hooks) { @field firstName = contains(StringField); @field pet = linksTo(Pet); } - await shimModule(`${realmURL}test-cards`, { Person, Pet }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person, Pet }); let mango = new Pet({ firstName: 'Mango' }); let hassan = new Person({ firstName: 'Hassan', pet: mango }); @@ -1362,7 +1375,8 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }); let spookyToiletPaper = new Toy({ description: 'Toilet paper ghost: Poooo!', @@ -1491,7 +1505,8 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }); let spookyToiletPaper = new Toy({ description: 'Toilet paper ghost: Poooo!', @@ -1624,7 +1639,8 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }); let spookyToiletPaper = new Toy({ description: 'Toilet paper ghost: Poooo!', @@ -1731,7 +1747,8 @@ module('Integration | serialization', function (hooks) { @field parent = linksTo(() => Person); @field favorite = linksTo(() => Person); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person }); let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -1806,7 +1823,8 @@ module('Integration | serialization', function (hooks) { @field created = contains(DateField); @field published = contains(DatetimeField); } - await shimModule(`${realmURL}test-cards`, { Post }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Post }); let firstPost = new Post({ created: null, published: null }); let serialized = serializeCard(firstPost, { @@ -1846,7 +1864,8 @@ module('Integration | serialization', function (hooks) { }; } - await shimModule(`${realmURL}test-cards`, { Post, Person }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Post, Person }); let doc = { data: { @@ -1915,7 +1934,7 @@ module('Integration | serialization', function (hooks) { }; } - await shimModule(`${realmURL}test-cards`, { Post, Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Post, Person }); let doc = { data: { @@ -1974,7 +1993,8 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule(`${realmURL}test-cards`, { Person, Post }, loader); + + loader.shimModule(`${realmURL}test-cards`, { Person, Post }); let firstPost = new Post({ author: new Person({ @@ -2027,11 +2047,7 @@ module('Integration | serialization', function (hooks) { @field author = contains(Person); } - await shimModule( - `${realmURL}test-cards`, - { Person, Employee, Post }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { Person, Employee, Post }); let firstPost = new Post({ author: new Employee({ @@ -2104,7 +2120,7 @@ module('Integration | serialization', function (hooks) { }; } - await shimModule(`${realmURL}test-cards`, { Post, Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Post, Person }); let helloWorld = new Post({ title: 'First Post', @@ -2154,7 +2170,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person }); let mango = new Person({ birthdate: p('2019-10-30') }); let serialized = serializeCard(mango, { includeComputeds: true, @@ -2190,7 +2206,7 @@ module('Integration | serialization', function (hooks) { computeVia: () => '../../person.svg', }); } - await shimModule(`${realmURL}test-cards`, { Pet, Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Pet, Person }); let mango = new Pet({ name: 'Mango' }); let hassan = new Person({ firstName: 'Hassan', pet: mango }); await saveCard(mango, `${realmURL}Pet/mango`, loader); @@ -2286,7 +2302,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule(`${realmURL}test-cards`, { Pet, Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Pet, Person }); let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -2391,7 +2407,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule(`${realmURL}test-cards`, { Pet, Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Pet, Person }); let person = new Person({ firstName: 'Burcu' }); let serialized = serializeCard(person, { includeUnrenderedFields: true, @@ -2441,7 +2457,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule(`${realmURL}test-cards`, { Pet, Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Pet, Person }); let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -2493,7 +2509,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule(`${realmURL}test-cards`, { Pet, Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Pet, Person }); let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -2553,7 +2569,7 @@ module('Integration | serialization', function (hooks) { }; } - await shimModule(`${realmURL}test-cards`, { Schedule }, loader); + loader.shimModule(`${realmURL}test-cards`, { Schedule }); let doc = { data: { @@ -2615,11 +2631,7 @@ module('Integration | serialization', function (hooks) { }; } - await shimModule( - `${realmURL}test-cards`, - { Schedule, Appointment }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { Schedule, Appointment }); let doc = { data: { @@ -2661,7 +2673,7 @@ module('Integration | serialization', function (hooks) { class Schedule extends CardDef { @field dates = containsMany(DateField); } - await shimModule(`${realmURL}test-cards`, { Schedule }, loader); + loader.shimModule(`${realmURL}test-cards`, { Schedule }); let classSchedule = new Schedule({ dates: [p('2022-4-1'), p('2022-4-4')] }); assert.deepEqual( @@ -2689,11 +2701,7 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule( - `${realmURL}test-cards`, - { Schedule, Appointment }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { Schedule, Appointment }); let classSchedule = new Schedule({ appointments: [ @@ -2738,7 +2746,7 @@ module('Integration | serialization', function (hooks) { @field created = contains(DateField); @field published = contains(DatetimeField); } - await shimModule(`${realmURL}test-cards`, { Post }, loader); + loader.shimModule(`${realmURL}test-cards`, { Post }); let firstPost = new Post({ title: 'First Post', @@ -2800,7 +2808,7 @@ module('Integration | serialization', function (hooks) { @field description = contains(StringField, { computeVia: () => 'Post' }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}test-cards`, { Post, Person, Animal }, loader); + loader.shimModule(`${realmURL}test-cards`, { Post, Person, Animal }); let firstPost = new Post({ title: 'First Post', @@ -2871,11 +2879,7 @@ module('Integration | serialization', function (hooks) { @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule( - `${realmURL}test-cards`, - { Person, Employee, Post }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { Person, Employee, Post }); let firstPost = new Post({ title: 'First Post', @@ -2976,11 +2980,7 @@ module('Integration | serialization', function (hooks) { @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule( - `${realmURL}test-cards`, - { Person, Employee, Post, Pet }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { Person, Employee, Post, Pet }); let firstPost = new Post({ title: 'First Post', @@ -3108,16 +3108,12 @@ module('Integration | serialization', function (hooks) { @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule( - `${realmURL}test-cards`, - { - Person, - Employee, - Customer, - Group, - }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { + Person, + Employee, + Customer, + Group, + }); let group = new Group({ people: [ @@ -3252,17 +3248,13 @@ module('Integration | serialization', function (hooks) { @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule( - `${realmURL}test-cards`, - { - Person, - Role, - DogWalker, - Employee, - Group, - }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { + Person, + Role, + DogWalker, + Employee, + Group, + }); let group = new Group({ people: [ @@ -3378,7 +3370,7 @@ module('Integration | serialization', function (hooks) { @field firstName = contains(StringField); @field pet = linksTo(Pet); } - await shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }); let doc: LooseSingleCardDocument = { data: { @@ -3472,7 +3464,7 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}person`, { Person }, loader); + loader.shimModule(`${realmURL}person`, { Person }); let doc: LooseSingleCardDocument = { data: { @@ -3545,8 +3537,8 @@ module('Integration | serialization', function (hooks) { @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}person`, { Person }, loader); - await shimModule(`${realmURL}post`, { Post }, loader); + loader.shimModule(`${realmURL}person`, { Person }); + loader.shimModule(`${realmURL}post`, { Post }); let doc: LooseSingleCardDocument = { data: { @@ -3636,9 +3628,9 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}person`, { Person }, loader); - await shimModule(`${realmURL}post`, { Post }, loader); - await shimModule(`${realmURL}blog`, { Blog }, loader); + loader.shimModule(`${realmURL}person`, { Person }); + loader.shimModule(`${realmURL}post`, { Post }); + loader.shimModule(`${realmURL}blog`, { Blog }); let doc: LooseSingleCardDocument = { data: { @@ -3764,11 +3756,12 @@ module('Integration | serialization', function (hooks) { @field editor = contains(Person); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule( - `${realmURL}test-cards`, - { Certificate, Person, Post, Blog }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { + Certificate, + Person, + Post, + Blog, + }); let doc: LooseSingleCardDocument = { data: { @@ -4024,7 +4017,7 @@ module('Integration | serialization', function (hooks) { }); @field thumbnailURL = contains(StringField, { computeVia: () => null }); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person }); let mango = new Person({ birthdate: p('2019-10-30') }); await renderCard(loader, mango, 'isolated'); @@ -4083,7 +4076,7 @@ module('Integration | serialization', function (hooks) { }; } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person }); let mango = new Person({ id: `${realmURL}Person/mango`, @@ -4133,7 +4126,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule(`${realmURL}test-cards`, { Person, Pet }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person, Pet }); let mango = new Pet({ firstName: 'Mango', @@ -4224,7 +4217,7 @@ module('Integration | serialization', function (hooks) { @field firstName = contains(StringField); @field pets = linksToMany(Pet); } - await shimModule(`${realmURL}test-cards`, { Person, Pet }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person, Pet }); let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -4377,7 +4370,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person, Pet, Toy }); let spookyToiletPaper = new Toy({ description: 'Toilet paper ghost: Poooo!', @@ -4518,7 +4511,7 @@ module('Integration | serialization', function (hooks) { computeVia: () => 'person.svg', }); } - await shimModule(`${realmURL}test-cards`, { Person, Pet }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person, Pet }); let hassan = new Person({ firstName: 'Hassan' }); @@ -4576,7 +4569,7 @@ module('Integration | serialization', function (hooks) { @field firstName = contains(StringField); @field pets = linksToMany(Pet); } - await shimModule(`${realmURL}test-cards`, { Person, Pet }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person, Pet }); let doc: LooseSingleCardDocument = { data: { @@ -4648,7 +4641,7 @@ module('Integration | serialization', function (hooks) { computeVia: () => 'person.svg', }); } - await shimModule(`${realmURL}test-cards`, { Person, Pet }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person, Pet }); let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -4785,7 +4778,7 @@ module('Integration | serialization', function (hooks) { computeVia: () => 'person.svg', }); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person }); let mango = new Person({ firstName: 'Mango' }); let vanGogh = new Person({ firstName: 'Van Gogh' }); @@ -4869,7 +4862,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule(`${realmURL}test-cards`, { Person }, loader); + loader.shimModule(`${realmURL}test-cards`, { Person }); let doc: LooseSingleCardDocument = { data: { @@ -4976,11 +4969,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule( - `${realmURL}test-cards`, - { Pet, Friend, Person }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { Pet, Friend, Person }); let mango = new Pet({ name: 'Mango' }); let vanGogh = new Pet({ name: 'Van Gogh' }); let hassan = new Friend({ firstName: 'Hassan', pets: [mango, vanGogh] }); @@ -5100,11 +5089,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule( - `${realmURL}test-cards`, - { Pet, Friend, Person }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { Pet, Friend, Person }); let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -5216,11 +5201,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule( - `${realmURL}test-cards`, - { Pet, Friend, Person }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { Pet, Friend, Person }); let person = new Person({ firstName: 'Burcu' }); let serialized = serializeCard(person, { includeUnrenderedFields: true, @@ -5272,11 +5253,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule( - `${realmURL}test-cards`, - { Pet, Friend, Person }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { Pet, Friend, Person }); let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -5330,11 +5307,7 @@ module('Integration | serialization', function (hooks) { }, }); } - await shimModule( - `${realmURL}test-cards`, - { Pet, Friend, Person }, - loader, - ); + loader.shimModule(`${realmURL}test-cards`, { Pet, Friend, Person }); let doc: LooseSingleCardDocument = { data: { type: 'card', @@ -5456,7 +5429,7 @@ module('Integration | serialization', function (hooks) { @field notANumber = contains(NumberField); @field infinity = contains(NumberField); } - await shimModule(`${realmURL}test-cards`, { Sample }, loader); + loader.shimModule(`${realmURL}test-cards`, { Sample }); let resource = { attributes: { @@ -5512,7 +5485,7 @@ module('Integration | serialization', function (hooks) { @field someNull = contains(NumberField); } - await shimModule(`${realmURL}test-cards`, { Sample }, loader); + loader.shimModule(`${realmURL}test-cards`, { Sample }); let sample = new Sample({ someNumber: 42, @@ -5554,7 +5527,7 @@ module('Integration | serialization', function (hooks) { @field someDecimal = contains(BigIntegerField); @field someZeroString = contains(BigIntegerField); } - await shimModule(`${realmURL}test-cards`, { Sample }, loader); + loader.shimModule(`${realmURL}test-cards`, { Sample }); let resource = { attributes: { @@ -5602,7 +5575,7 @@ module('Integration | serialization', function (hooks) { @field someNull = contains(BigIntegerField); } - await shimModule(`${realmURL}test-cards`, { Sample }, loader); + loader.shimModule(`${realmURL}test-cards`, { Sample }); let sample = new Sample({ someBigInt: BigInt('9223372036854775808'), @@ -5665,7 +5638,7 @@ module('Integration | serialization', function (hooks) { // }, // }); } - await shimModule(`${realmURL}test-cards`, { Sample }, loader); + loader.shimModule(`${realmURL}test-cards`, { Sample }); let sample = new Sample({ someBigInt: BigInt('1'), @@ -5704,7 +5677,7 @@ module('Integration | serialization', function (hooks) { @field someString = contains(EthereumAddressField); @field someNull = contains(EthereumAddressField); } - await shimModule(`${realmURL}test-cards`, { Sample }, loader); + loader.shimModule(`${realmURL}test-cards`, { Sample }); let resource = { attributes: { @@ -5757,7 +5730,7 @@ module('Integration | serialization', function (hooks) { @field someNull = contains(EthereumAddressField); } - await shimModule(`${realmURL}test-cards`, { Sample }, loader); + loader.shimModule(`${realmURL}test-cards`, { Sample }); let sample = new Sample({ someAddress: '0x00317f9aF5141dC211e9EbcdCE690cf0E98Ef53b', diff --git a/packages/host/tests/integration/search-index-test.gts b/packages/host/tests/integration/search-index-test.gts index 66bca988b0..dd4ff1df91 100644 --- a/packages/host/tests/integration/search-index-test.gts +++ b/packages/host/tests/integration/search-index-test.gts @@ -2643,33 +2643,34 @@ module('Integration | search-index', function (hooks) { // Exclude synthetic imports that encapsulate scoped CSS .filter((key) => !key.includes('glimmer-scoped.css')), [ - '@cardstack/boxel-ui/components', - '@cardstack/boxel-ui/helpers', - '@cardstack/boxel-ui/icons', - '@cardstack/runtime-common', - '@ember/component', - '@ember/component/template-only', - '@ember/helper', - '@ember/modifier', - '@ember/object', - '@ember/template-factory', - '@glimmer/component', - '@glimmer/tracking', - 'ember-concurrency', - 'ember-concurrency/-private/async-arrow-runtime', - 'ember-modifier', 'http://localhost:4201/base/card-api', 'http://localhost:4201/base/contains-many-component', 'http://localhost:4201/base/field-component', 'http://localhost:4201/base/links-to-editor', 'http://localhost:4201/base/links-to-many-component', 'http://localhost:4201/base/number', + 'http://localhost:4201/base/shared-state', 'http://localhost:4201/base/string', 'http://localhost:4201/base/text-input-validator', 'http://localhost:4201/base/watched-array', 'http://localhost:4202/test/person', - 'lodash', - 'tracked-built-ins', + 'https://packages/@cardstack/boxel-ui/components', + 'https://packages/@cardstack/boxel-ui/helpers', + 'https://packages/@cardstack/boxel-ui/icons', + 'https://packages/@cardstack/runtime-common', + 'https://packages/@ember/component', + 'https://packages/@ember/component/template-only', + 'https://packages/@ember/helper', + 'https://packages/@ember/modifier', + 'https://packages/@ember/object', + 'https://packages/@ember/template-factory', + 'https://packages/@glimmer/component', + 'https://packages/@glimmer/tracking', + 'https://packages/ember-concurrency', + 'https://packages/ember-concurrency/-private/async-arrow-runtime', + 'https://packages/ember-modifier', + 'https://packages/lodash', + 'https://packages/tracked-built-ins', ], 'the card references for the instance are correct', ); diff --git a/packages/realm-server/lib/externals.ts b/packages/realm-server/lib/externals.ts index c2094071e4..a142501132 100644 --- a/packages/realm-server/lib/externals.ts +++ b/packages/realm-server/lib/externals.ts @@ -1,103 +1,102 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import { Loader } from '@cardstack/runtime-common/loader'; - import * as runtime from '@cardstack/runtime-common'; import * as flat from 'flat'; import * as lodash from 'lodash'; import * as dateFns from 'date-fns'; import * as ethers from 'ethers'; +import { VirtualNetwork } from '@cardstack/runtime-common'; -export function shimExternals(loader: Loader) { - loader.shimModule('@cardstack/runtime-common', runtime); - loader.shimModule('@cardstack/boxel-ui/components', { +export function shimExternals(virtualNetwork: VirtualNetwork) { + virtualNetwork.shimModule('@cardstack/runtime-common', runtime); + virtualNetwork.shimModule('@cardstack/boxel-ui/components', { Button() {}, }); - loader.shimModule('@cardstack/boxel-ui/helpers', { + virtualNetwork.shimModule('@cardstack/boxel-ui/helpers', { cssVar() {}, eq() {}, }); - loader.shimModule('@cardstack/boxel-ui/icons', { + virtualNetwork.shimModule('@cardstack/boxel-ui/icons', { default() {}, }); // import * as glimmerComponent from "@glimmer/component"; - loader.shimModule('@glimmer/component', { + virtualNetwork.shimModule('@glimmer/component', { default: class {}, }); // import * as emberComponent from "ember-source/dist/packages/@ember/component"; - loader.shimModule('@ember/component', { + virtualNetwork.shimModule('@ember/component', { default: class {}, setComponentTemplate() {}, }); // import * as emberComponentTemplateOnly from "ember-source/dist/packages/@ember/component/template-only"; - loader.shimModule('@ember/component/template-only', { default() {} }); + virtualNetwork.shimModule('@ember/component/template-only', { default() {} }); // import * as emberTemplateFactory from "ember-source/dist/packages/@ember/template-factory"; - loader.shimModule('@ember/template-factory', { + virtualNetwork.shimModule('@ember/template-factory', { createTemplateFactory() {}, }); // import * as emberTemplate from "ember-source/dist/packages/@ember/template"; - loader.shimModule('@ember/template', { + virtualNetwork.shimModule('@ember/template', { htmlSafe(html: string) { return html; }, }); // import * as cssUrl from 'ember-css-url'; - loader.shimModule('ember-css-url', { + virtualNetwork.shimModule('ember-css-url', { default: () => {}, }); // import * as glimmerTracking from "@glimmer/tracking"; - loader.shimModule('@glimmer/tracking', { + virtualNetwork.shimModule('@glimmer/tracking', { tracked() {}, }); // import * as emberObject from "ember-source/dist/packages/@ember/object"; - loader.shimModule('@ember/object', { + virtualNetwork.shimModule('@ember/object', { action() {}, get() {}, }); // import * as emberObjectInternals from "ember-source/dist/packages/@ember/object/internals"; - loader.shimModule('@ember/object/internals', { + virtualNetwork.shimModule('@ember/object/internals', { guidFor() {}, }); // import * as emberHelper from "ember-source/dist/packages/@ember/helper"; - loader.shimModule('@ember/helper', { + virtualNetwork.shimModule('@ember/helper', { get() {}, fn() {}, concat() {}, }); // import * as emberModifier from "ember-source/dist/packages/@ember/modifier"; - loader.shimModule('@ember/modifier', { + virtualNetwork.shimModule('@ember/modifier', { on() {}, }); // import * as emberResources from 'ember-resources'; - loader.shimModule('ember-resources', { + virtualNetwork.shimModule('ember-resources', { Resource: class {}, useResource() {}, }); // import * as emberConcurrency from 'ember-concurrency'; - loader.shimModule('ember-concurrency', { + virtualNetwork.shimModule('ember-concurrency', { task() {}, restartableTask() {}, }); // import * as emberConcurrencyAsyncArrowRuntime from 'ember-concurrency/-private/async-arrow-runtime'; - loader.shimModule('ember-concurrency/-private/async-arrow-runtime', { + virtualNetwork.shimModule('ember-concurrency/-private/async-arrow-runtime', { default: () => {}, buildTask: () => {}, }); // import * as emberModifier from 'ember-modifier'; - loader.shimModule('ember-modifier', { + virtualNetwork.shimModule('ember-modifier', { default: class {}, modifier: () => {}, }); - loader.shimModule('flat', flat); + virtualNetwork.shimModule('flat', flat); // import * as tracked from "tracked-built-ins"; - loader.shimModule('tracked-built-ins', { + virtualNetwork.shimModule('tracked-built-ins', { // TODO replace with actual TrackedWeakMap when we add real glimmer // implementations TrackedWeakMap: WeakMap, }); - loader.shimModule('lodash', lodash); - loader.shimModule('date-fns', dateFns); - loader.shimModule('ember-resources', { Resource: class {} }); - loader.shimModule('@ember/destroyable', {}); - loader.shimModule('marked', { marked: () => {} }); - loader.shimModule('ethers', ethers); + virtualNetwork.shimModule('lodash', lodash); + virtualNetwork.shimModule('date-fns', dateFns); + virtualNetwork.shimModule('ember-resources', { Resource: class {} }); + virtualNetwork.shimModule('@ember/destroyable', {}); + virtualNetwork.shimModule('marked', { marked: () => {} }); + virtualNetwork.shimModule('ethers', ethers); } diff --git a/packages/realm-server/main.ts b/packages/realm-server/main.ts index e4dd1ed19a..bc15cfd0af 100644 --- a/packages/realm-server/main.ts +++ b/packages/realm-server/main.ts @@ -1,6 +1,5 @@ import './setup-logger'; // This should be first -import { Realm, logger } from '@cardstack/runtime-common'; -import { Loader } from '@cardstack/runtime-common/loader'; +import { Realm, VirtualNetwork, logger } from '@cardstack/runtime-common'; import { NodeAdapter } from './node-realm'; import yargs from 'yargs'; import { RealmServer } from './server'; @@ -113,9 +112,9 @@ if ( } let log = logger('main'); - -let loader = new Loader(); -shimExternals(loader); +let virtualNetwork = new VirtualNetwork(); +let loader = virtualNetwork.createLoader(); +shimExternals(virtualNetwork); let urlMappings = fromUrls.map((fromUrl, i) => [ new URL(String(fromUrl), `http://localhost:${port}`), diff --git a/packages/realm-server/tests/auth-client-test.ts b/packages/realm-server/tests/auth-client-test.ts index 3f338d0945..adc7e22208 100644 --- a/packages/realm-server/tests/auth-client-test.ts +++ b/packages/realm-server/tests/auth-client-test.ts @@ -5,7 +5,7 @@ import { RealmAuthClient, type RealmAuthMatrixClientInterface, } from '@cardstack/runtime-common/realm-auth-client'; -import { Loader } from '@cardstack/runtime-common'; +import { VirtualNetwork } from '@cardstack/runtime-common'; import jwt from 'jsonwebtoken'; function createJWT(expiresIn: string | number) { @@ -33,7 +33,9 @@ module('realm-auth-client', function (assert) { return Promise.resolve(); }, } as RealmAuthMatrixClientInterface; - let loader = new Loader(); + + let virtualNetwork = new VirtualNetwork(); + let loader = virtualNetwork.createLoader(); client = new RealmAuthClient( new URL('http://testrealm.com/'), diff --git a/packages/realm-server/tests/indexing-test.ts b/packages/realm-server/tests/indexing-test.ts index 42cdb2dbd3..f748285409 100644 --- a/packages/realm-server/tests/indexing-test.ts +++ b/packages/realm-server/tests/indexing-test.ts @@ -1,10 +1,10 @@ import { module, test } from 'qunit'; import { dirSync, setGracefulCleanup } from 'tmp'; import { - Loader, baseRealm, LooseSingleCardDocument, Realm, + VirtualNetwork, } from '@cardstack/runtime-common'; import { createRealm, @@ -33,12 +33,14 @@ setGracefulCleanup(); // underlying filesystem in a manner that doesn't leak into other tests (as well // as to test through loader caching) module('indexing', function (hooks) { - let loader = new Loader(); + let virtualNetwork = new VirtualNetwork(); + let loader = virtualNetwork.createLoader(); + loader.addURLMapping( new URL(baseRealm.url), new URL('http://localhost:4201/base/'), ); - shimExternals(loader); + shimExternals(virtualNetwork); setupCardLogs( hooks, @@ -51,12 +53,12 @@ module('indexing', function (hooks) { setupBaseRealmServer(hooks, loader); hooks.beforeEach(async function () { - let testRealmLoader = new Loader(); + let testRealmLoader = virtualNetwork.createLoader(); testRealmLoader.addURLMapping( new URL(baseRealm.url), new URL('http://localhost:4201/base/'), ); - shimExternals(testRealmLoader); + shimExternals(virtualNetwork); dir = dirSync().name; realm = await createRealm(testRealmLoader, dir, { diff --git a/packages/realm-server/tests/loader-test.ts b/packages/realm-server/tests/loader-test.ts index 304708274a..8746746cd1 100644 --- a/packages/realm-server/tests/loader-test.ts +++ b/packages/realm-server/tests/loader-test.ts @@ -1,5 +1,5 @@ import { module, test } from 'qunit'; -import { Loader } from '@cardstack/runtime-common'; +import { Loader, VirtualNetwork } from '@cardstack/runtime-common'; import { dirSync, setGracefulCleanup, DirResult } from 'tmp'; import { createRealm, @@ -22,12 +22,14 @@ module('loader', function (hooks) { let dir: DirResult; let testRealmServer: Server; - let loader = new Loader(); + let virtualNetwork = new VirtualNetwork(); + let loader = virtualNetwork.createLoader(); + loader.addURLMapping( new URL(baseRealm.url), new URL('http://localhost:4201/base/'), ); - shimExternals(loader); + shimExternals(virtualNetwork); setupBaseRealmServer(hooks, loader); @@ -45,7 +47,7 @@ module('loader', function (hooks) { }); test('can dynamically load modules with cycles', async function (assert) { - let loader = new Loader(); + let loader = virtualNetwork.createLoader(); loader.addURLMapping(new URL(baseRealm.url), new URL(localBaseRealm)); let module = await loader.import<{ three(): number }>( `${testRealmHref}cycle-two`, @@ -54,7 +56,7 @@ module('loader', function (hooks) { }); test('can resolve multiple import load races against a common dep', async function (assert) { - let loader = new Loader(); + let loader = virtualNetwork.createLoader(); loader.addURLMapping(new URL(baseRealm.url), new URL(localBaseRealm)); let a = loader.import<{ a(): string }>(`${testRealmHref}a`); let b = loader.import<{ b(): string }>(`${testRealmHref}b`); @@ -64,7 +66,7 @@ module('loader', function (hooks) { }); test('can resolve a import deadlock', async function (assert) { - let loader = new Loader(); + let loader = virtualNetwork.createLoader(); loader.addURLMapping(new URL(baseRealm.url), new URL(localBaseRealm)); let a = loader.import<{ a(): string }>(`${testRealmHref}deadlock/a`); let b = loader.import<{ b(): string }>(`${testRealmHref}deadlock/b`); @@ -76,7 +78,7 @@ module('loader', function (hooks) { }); test('supports import.meta', async function (assert) { - let loader = new Loader(); + let loader = virtualNetwork.createLoader(); let realm = await createRealm( loader, dir.name, @@ -101,7 +103,7 @@ module('loader', function (hooks) { }); test('can determine consumed modules', async function (assert) { - let loader = new Loader(); + let loader = virtualNetwork.createLoader(); await loader.import<{ a(): string }>(`${testRealmHref}a`); assert.deepEqual(await loader.getConsumedModules(`${testRealmHref}a`), [ `${testRealmHref}a`, @@ -111,7 +113,7 @@ module('loader', function (hooks) { }); test('can determine consumed modules when an error is encountered during loading', async function (assert) { - let loader = new Loader(); + let loader = virtualNetwork.createLoader(); loader.addURLMapping(new URL(baseRealm.url), new URL(localBaseRealm)); try { await loader.import<{ d(): string }>(`${testRealmHref}d`); @@ -129,7 +131,7 @@ module('loader', function (hooks) { }); test('can get consumed modules within a cycle', async function (assert) { - let loader = new Loader(); + let loader = virtualNetwork.createLoader(); loader.addURLMapping(new URL(baseRealm.url), new URL(localBaseRealm)); await loader.import<{ three(): number }>(`${testRealmHref}cycle-two`); let modules = await loader.getConsumedModules(`${testRealmHref}cycle-two`); @@ -140,8 +142,8 @@ module('loader', function (hooks) { }); test('supports identify API', async function (assert) { - let loader = new Loader(); - shimExternals(loader); + let loader = virtualNetwork.createLoader(); + shimExternals(virtualNetwork); loader.addURLMapping(new URL(baseRealm.url), new URL(localBaseRealm)); let { Person } = await loader.import<{ Person: unknown }>( `${testRealmHref}person`, @@ -158,8 +160,8 @@ module('loader', function (hooks) { }); test('exports cannot be mutated', async function (assert) { - let loader = new Loader(); - shimExternals(loader); + let loader = virtualNetwork.createLoader(); + shimExternals(virtualNetwork); loader.addURLMapping(new URL(baseRealm.url), new URL(localBaseRealm)); let module = await loader.import<{ Person: unknown }>( `${testRealmHref}person`, @@ -170,8 +172,8 @@ module('loader', function (hooks) { }); test('can get a loader used to import a specific card', async function (assert) { - let loader = new Loader(); - shimExternals(loader); + let loader = virtualNetwork.createLoader(); + shimExternals(virtualNetwork); loader.addURLMapping(new URL(baseRealm.url), new URL(localBaseRealm)); let module = await loader.import(`${testRealmHref}person`); let card = module.Person; diff --git a/packages/realm-server/tests/module-syntax-test.ts b/packages/realm-server/tests/module-syntax-test.ts index eaf033707c..9bc87cbb73 100644 --- a/packages/realm-server/tests/module-syntax-test.ts +++ b/packages/realm-server/tests/module-syntax-test.ts @@ -2,22 +2,24 @@ import { module, test } from 'qunit'; import { ModuleSyntax } from '@cardstack/runtime-common/module-syntax'; import { dirSync } from 'tmp'; import { - Loader, baseRealm, baseCardRef, baseFieldRef, + VirtualNetwork, } from '@cardstack/runtime-common'; import { testRealm, createRealm } from './helpers'; import '@cardstack/runtime-common/helpers/code-equality-assertion'; import { shimExternals } from '../lib/externals'; module('module-syntax', function () { - let loader = new Loader(); + let virtualNetwork = new VirtualNetwork(); + let loader = virtualNetwork.createLoader(); + loader.addURLMapping( new URL(baseRealm.url), new URL('http://localhost:4201/base/'), ); - shimExternals(loader); + shimExternals(virtualNetwork); function addField(src: string, addFieldAtIndex?: number) { let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person.gts`)); diff --git a/packages/realm-server/tests/realm-server-test.ts b/packages/realm-server/tests/realm-server-test.ts index 507f65d5dc..205e9f194e 100644 --- a/packages/realm-server/tests/realm-server-test.ts +++ b/packages/realm-server/tests/realm-server-test.ts @@ -18,7 +18,6 @@ import { } from '@cardstack/runtime-common/etc/test-fixtures'; import { isSingleCardDocument, - Loader, baseRealm, loadCard, Deferred, @@ -26,6 +25,7 @@ import { type LooseSingleCardDocument, Realm, RealmPermissions, + VirtualNetwork, } from '@cardstack/runtime-common'; import { stringify } from 'qs'; import { Query } from '@cardstack/runtime-common/query'; @@ -115,9 +115,11 @@ module('Realm Server', function (hooks) { let request: SuperTest; let dir: DirResult; - let loader = new Loader(); + let virtualNetwork = new VirtualNetwork(); + let loader = virtualNetwork.createLoader(); + loader.addURLMapping(new URL(baseRealm.url), new URL(localBaseRealm)); - shimExternals(loader); + shimExternals(virtualNetwork); setupCardLogs( hooks, @@ -1712,12 +1714,14 @@ module('Realm Server', function (hooks) { '*': ['read', 'write'], })); - let testRealmServer2Loader = new Loader(); + let virtualNetwork = new VirtualNetwork(); + + let testRealmServer2Loader = virtualNetwork.createLoader(); testRealmServer2Loader.addURLMapping( new URL(baseRealm.url), new URL(localBaseRealm), ); - shimExternals(testRealmServer2Loader); + shimExternals(virtualNetwork); testRealmServer2 = ( await runTestRealmServer( @@ -2025,9 +2029,11 @@ module('Realm Server serving from root', function (hooks) { let dir: DirResult; - let loader = new Loader(); + let virtualNetwork = new VirtualNetwork(); + let loader = virtualNetwork.createLoader(); + loader.addURLMapping(new URL(baseRealm.url), new URL(localBaseRealm)); - shimExternals(loader); + shimExternals(virtualNetwork); setupCardLogs( hooks, @@ -2040,7 +2046,9 @@ module('Realm Server serving from root', function (hooks) { dir = dirSync(); copySync(join(__dirname, 'cards'), dir.name); - let testRealmServerLoader = new Loader(); + let virtualNetwork = new VirtualNetwork(); + + let testRealmServerLoader = virtualNetwork.createLoader(); testRealmServerLoader.addURLMapping( new URL(baseRealm.url), new URL(localBaseRealm), @@ -2226,9 +2234,10 @@ module('Realm Server serving from a subdirectory', function (hooks) { let dir: DirResult; - let loader = new Loader(); + let virtualNetwork = new VirtualNetwork(); + let loader = virtualNetwork.createLoader(); loader.addURLMapping(new URL(baseRealm.url), new URL(localBaseRealm)); - shimExternals(loader); + shimExternals(virtualNetwork); setupCardLogs( hooks, @@ -2241,7 +2250,7 @@ module('Realm Server serving from a subdirectory', function (hooks) { dir = dirSync(); copySync(join(__dirname, 'cards'), dir.name); - let testRealmServerLoader = new Loader(); + let testRealmServerLoader = virtualNetwork.createLoader(); testRealmServerLoader.addURLMapping( new URL(baseRealm.url), new URL(localBaseRealm), @@ -2293,14 +2302,14 @@ async function setupPermissionedRealm(permissions: RealmPermissions) { let dir = dirSync(); copySync(join(__dirname, 'cards'), dir.name); - - let testRealmServerLoader = new Loader(); + let virtualNetwork = new VirtualNetwork(); + let testRealmServerLoader = virtualNetwork.createLoader(); testRealmServerLoader.addURLMapping( new URL(baseRealm.url), new URL(localBaseRealm), ); - shimExternals(testRealmServerLoader); + shimExternals(virtualNetwork); ({ testRealm, testRealmServer } = await runTestRealmServer( testRealmServerLoader, diff --git a/packages/runtime-common/index.ts b/packages/runtime-common/index.ts index 5fba893f4e..ccc59debb9 100644 --- a/packages/runtime-common/index.ts +++ b/packages/runtime-common/index.ts @@ -56,6 +56,8 @@ export const isNode = export { Realm } from './realm'; export { SupportedMimeType } from './router'; +export { VirtualNetwork } from './virtual-network'; + export type { Kind, RealmAdapter, diff --git a/packages/runtime-common/loader.ts b/packages/runtime-common/loader.ts index 46f6a8031a..8f341f2435 100644 --- a/packages/runtime-common/loader.ts +++ b/packages/runtime-common/loader.ts @@ -5,12 +5,9 @@ import { trimExecutableExtension, logger } from './index'; import { RealmPaths } from './paths'; import { CardError } from './error'; import flatMap from 'lodash/flatMap'; -import { type RunnerOpts } from './search-index'; import { decodeScopedCSSRequest, isScopedCSSRequest } from 'glimmer-scoped-css'; import jsEscapeString from 'js-string-escape'; -const isFastBoot = typeof (globalThis as any).FastBoot !== 'undefined'; - // this represents a URL that has already been resolved to aid in documenting // when resolution has already been performed export interface ResolvedURL extends URL { @@ -92,7 +89,6 @@ type EvaluatableModule = type UnregisteredDep = | { type: 'dep'; moduleURL: ResolvedURL } - | { type: 'shim-dep'; moduleId: string } | { type: '__import_meta__' } | { type: 'exports' }; @@ -105,15 +101,13 @@ type EvaluatableDep = type: 'completing-dep'; moduleURL: ResolvedURL; } - | { - type: 'shim-dep'; - moduleId: string; - } | { type: '__import_meta__' } | { type: 'exports' }; export type RequestHandler = (req: Request) => Promise; +type Fetch = typeof fetch; + let nonce = 0; export class Loader { nonce = nonce++; // the nonce is a useful debugging tool that let's us compare loaders @@ -136,8 +130,20 @@ export class Loader { private consumptionCache = new WeakMap(); private static loaders = new WeakMap(); + private fetchImplementation: Fetch; + private resolveImport: (moduleIdentifier: string) => string; + + constructor( + fetch: Fetch, + resolveImport?: (moduleIdentifier: string) => string, + ) { + this.fetchImplementation = fetch; + this.resolveImport = + resolveImport ?? ((moduleIdentifier) => moduleIdentifier); + } + static cloneLoader(loader: Loader): Loader { - let clone = new Loader(); + let clone = new Loader(loader.fetchImplementation, loader.resolveImport); clone.urlHandlers = loader.urlHandlers; clone.urlMappings = loader.urlMappings; for (let [moduleIdentifier, module] of loader.moduleShims) { @@ -159,13 +165,21 @@ export class Loader { } shimModule(moduleIdentifier: string, module: Record) { - this.moduleShims.set( - moduleIdentifier, - this.createModuleProxy(module, moduleIdentifier), - ); + moduleIdentifier = this.resolveImport(moduleIdentifier); + let proxiedModule = this.createModuleProxy(module, moduleIdentifier); + + for (let propName of Object.keys(module)) { + // Normal modules always end up in our identity map because the only way for other code to gain access to the module's exports is by getting it through the + // proxy our loader has wrapped around it. But shimmed modules may be used directly by our caller before we've had a chance to put them in the dientity map. + // So this eagerly puts them into the identity map. + proxiedModule[propName]; // Makes sure the shimmed modules get into the identity map. + } + + this.moduleShims.set(moduleIdentifier, proxiedModule); + this.setModule(moduleIdentifier, { state: 'evaluated', - moduleInstance: module, + moduleInstance: proxiedModule, consumedModules: new Set(), }); } @@ -179,13 +193,9 @@ export class Loader { } consumed.add(moduleIdentifier); - let module: Module | undefined; - if (isUrlLike(moduleIdentifier)) { - let resolvedModuleIdentifier = this.resolve(new URL(moduleIdentifier)); - module = this.getModule(resolvedModuleIdentifier.href); - } else { - module = this.getModule(moduleIdentifier); - } + let resolvedModuleIdentifier = this.resolve(new URL(moduleIdentifier)); + let module = this.getModule(resolvedModuleIdentifier.href); + if (!module || module.state === 'fetching') { // we haven't yet tried importing the module or we are still in the process of importing the module try { @@ -246,12 +256,11 @@ export class Loader { } async import(moduleIdentifier: string): Promise { + moduleIdentifier = this.resolveImport(moduleIdentifier); + let resolvedModule = this.resolve(moduleIdentifier); let resolvedModuleIdentifier = resolvedModule.href; - let shimmed = this.moduleShims.get(moduleIdentifier); - if (shimmed) { - return shimmed as T; - } + await this.advanceToState(resolvedModule, 'evaluated'); let module = this.getModule(resolvedModuleIdentifier); switch (module?.state) { @@ -301,16 +310,7 @@ export class Loader { maybeReadyDeps.push(entry); continue; } - let depModule = this.getModule( - entry.type === 'dep' ? entry.moduleURL.href : entry.moduleId, - ); - if (entry.type === 'shim-dep') { - maybeReadyDeps.push({ - type: 'shim-dep', - moduleId: entry.moduleId, - }); - continue; - } + let depModule = this.getModule(entry.moduleURL.href); if (!isEvaluatable(depModule)) { // we always only await the first dep that actually needs work and // then break back to the top-level state machine, so that we'll @@ -372,18 +372,7 @@ export class Loader { readyDeps.push(entry); continue; } - let depModuleId = - entry.type === 'dep' || entry.type === 'completing-dep' - ? entry.moduleURL.href - : entry.moduleId; - let depModule = this.getModule(depModuleId); - if (entry.type === 'shim-dep') { - readyDeps.push({ - type: 'shim-dep', - moduleId: entry.moduleId, - }); - continue; - } + let depModule = this.getModule(entry.moduleURL.href); if (entry.type === 'dep') { readyDeps.push({ type: 'dep', @@ -544,7 +533,19 @@ export class Loader { return await this.simulateFetch(request, result); } } - return await getNativeFetch()(this.asResolvedRequest(urlOrRequest, init)); + + let shimmedModule = this.moduleShims.get( + this.asUnresolvedRequest(urlOrRequest, init).url, + ); + if (shimmedModule) { + let response = new Response(); + (response as any)[Symbol.for('shimmed-module')] = shimmedModule; + return response; + } + + return await this.fetchImplementation( + this.asResolvedRequest(urlOrRequest, init), + ); } catch (err: any) { let url = urlOrRequest instanceof Request @@ -610,9 +611,9 @@ export class Loader { } private createModuleProxy(module: any, moduleIdentifier: string) { - let moduleId = isUrlLike(moduleIdentifier) - ? trimExecutableExtension(this.reverseResolution(moduleIdentifier)).href - : moduleIdentifier; + let moduleId = trimExecutableExtension( + this.reverseResolution(moduleIdentifier), + ).href; return new Proxy(module, { get: (target, property, received) => { let value = Reflect.get(target, property, received); @@ -646,9 +647,12 @@ export class Loader { }; this.setModule(moduleIdentifier, module); - let src: string | null | undefined; + let loaded: + | { type: 'source'; source: string } + | { type: 'shimmed'; module: Record }; + try { - src = await this.load(moduleURL); + loaded = await this.load(moduleURL); } catch (exception) { this.setModule(moduleIdentifier, { state: 'broken', @@ -657,6 +661,19 @@ export class Loader { }); throw exception; } + + if (loaded.type === 'shimmed') { + this.setModule(moduleIdentifier, { + state: 'evaluated', + moduleInstance: loaded.module, + consumedModules: new Set(), + }); + module.deferred.fulfill(); + return; + } + + let src: string | null | undefined = loaded.source; + src = transformSync(src, { plugins: [ [ @@ -683,13 +700,14 @@ export class Loader { return { type: 'exports' }; } else if (depId === '__import_meta__') { return { type: '__import_meta__' }; - } else if (isUrlLike(depId)) { + } else { return { type: 'dep', - moduleURL: this.resolve(depId, new URL(moduleIdentifier)), + moduleURL: this.resolve( + this.resolveImport(depId), + new URL(moduleIdentifier), + ), }; - } else { - return { type: 'shim-dep', moduleId: depId }; // for npm imports } }); implementation = impl; @@ -731,11 +749,7 @@ export class Loader { ); let consumedModules = new Set( flatMap(module.dependencies, (dep) => - dep.type === 'dep' - ? [dep.moduleURL.href] - : dep.type === 'shim-dep' - ? [dep.moduleId] - : [], + dep.type === 'dep' ? [dep.moduleURL.href] : [], ), ); @@ -763,15 +777,6 @@ export class Loader { } return this.evaluate(entry.moduleURL.href, depModule!); } - case 'shim-dep': { - let shimModule = this.getModule(entry.moduleId); - if (shimModule?.state !== 'evaluated') { - throw new Error( - `bug: shimmed modules should always be in an 'evaluated' state, but ${entry.moduleId} was in '${module.state}' state`, - ); - } - return shimModule.moduleInstance; - } default: throw assertNever(entry); } @@ -794,7 +799,12 @@ export class Loader { } } - private async load(moduleURL: ResolvedURL): Promise { + private async load( + moduleURL: ResolvedURL, + ): Promise< + | { type: 'source'; source: string } + | { type: 'shimmed'; module: Record } + > { let response: Response; try { response = await this.fetch(moduleURL); @@ -809,22 +819,15 @@ export class Loader { let error = await CardError.fromFetchResponse(moduleURL.href, response); throw error; } - return await response.text(); - } -} -function getNativeFetch(): typeof fetch { - if (isFastBoot) { - let optsId = (globalThis as any).runnerOptsId; - if (optsId == null) { - throw new Error(`Runner Options Identifier was not set`); + if (Symbol.for('shimmed-module') in response) { + return { + type: 'shimmed', + module: (response as any)[Symbol.for('shimmed-module')], + }; } - let getRunnerOpts = (globalThis as any).getRunnerOpts as ( - optsId: number, - ) => RunnerOpts; - return getRunnerOpts(optsId)._fetch; - } else { - return fetch; + + return { type: 'source', source: await response.text() }; } } @@ -832,19 +835,8 @@ function assertNever(value: never) { throw new Error(`should never happen ${value}`); } -function isUrlLike(moduleIdentifier: string): boolean { - return ( - moduleIdentifier.startsWith('.') || - moduleIdentifier.startsWith('/') || - moduleIdentifier.startsWith('http://') || - moduleIdentifier.startsWith('https://') - ); -} - function trimModuleIdentifier(moduleIdentifier: string): string { - return isUrlLike(moduleIdentifier) - ? trimExecutableExtension(new URL(moduleIdentifier)).href - : moduleIdentifier; + return trimExecutableExtension(new URL(moduleIdentifier)).href; } type ModuleState = Module['state']; diff --git a/packages/runtime-common/virtual-network.ts b/packages/runtime-common/virtual-network.ts new file mode 100644 index 0000000000..8b57394d45 --- /dev/null +++ b/packages/runtime-common/virtual-network.ts @@ -0,0 +1,88 @@ +import { Loader } from './loader'; +import type { RunnerOpts } from './search-index'; + +const isFastBoot = typeof (globalThis as any).FastBoot !== 'undefined'; +const PACKAGES_FAKE_ORIGIN = 'https://packages/'; + +function getNativeFetch(): typeof fetch { + if (isFastBoot) { + let optsId = (globalThis as any).runnerOptsId; + if (optsId == null) { + throw new Error(`Runner Options Identifier was not set`); + } + let getRunnerOpts = (globalThis as any).getRunnerOpts as ( + optsId: number, + ) => RunnerOpts; + return getRunnerOpts(optsId)._fetch; + } else { + return fetch.bind(globalThis); + } +} + +export type Handler = (req: Request) => Promise; + +export class VirtualNetwork { + private nativeFetch = getNativeFetch(); + private handlers: Handler[] = []; + + private resolveImport = (moduleIdentifier: string) => { + if (!isUrlLike(moduleIdentifier)) { + moduleIdentifier = new URL(moduleIdentifier, PACKAGES_FAKE_ORIGIN).href; + } + return moduleIdentifier; + }; + + private shimmingLoader = new Loader(() => { + throw new Error('This loader should never call fetch'); + }, this.resolveImport); + + constructor() { + this.mount(async (request) => { + if (request.url.startsWith(PACKAGES_FAKE_ORIGIN)) { + return this.shimmingLoader.fetch(request); + } + + return null; + }); + } + + createLoader() { + return new Loader(this.fetch, this.resolveImport); + } + + shimModule(moduleIdentifier: string, module: Record) { + this.shimmingLoader.shimModule(moduleIdentifier, module); + } + + mount(handler: Handler) { + this.handlers.push(handler); + } + + fetch: typeof fetch = async ( + urlOrRequest: string | URL | Request, + init?: RequestInit, + ) => { + let request = + urlOrRequest instanceof Request + ? urlOrRequest + : new Request(urlOrRequest, init); + + for (let handler of this.handlers) { + let response = await handler(request); + if (response) { + return response; + } + } + + return this.nativeFetch(request, init); + }; +} + +function isUrlLike(moduleIdentifier: string): boolean { + return ( + moduleIdentifier.startsWith('.') || + moduleIdentifier.startsWith('/') || + moduleIdentifier.startsWith('http://') || + moduleIdentifier.startsWith('https://') + ); +}