diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bff9e7b587..19d255c3c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,8 +70,8 @@ jobs: restore-keys: | ${{ runner.os }}-yarn- - run: yarn --prefer-offline - - name: Base realm server - run: yarn start:base & + - name: Start realm servers + run: yarn start:test-realms & working-directory: realm-server - name: realm server test suite run: yarn test diff --git a/README.md b/README.md index 73fffd0a04..1bc8fbc318 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,6 @@ The tests are available at `http://localhost:4200/tests` ### Realm Server To run the `realm-server/` workspace tests start: -1. `yarn start:base` in the `realm-server/` workspace to serve the base realm (alternatively you can use `yarn start:test-realms` which also serves the base realm) +1. `yarn start:test-realms` in the `realm-server/` to serve _both_ the base realm and the realm that serves the test cards for node. Run `yarn test` in the `realm-server/` workspace to run the realm tests diff --git a/host/app/app.ts b/host/app/app.ts index cb1556f0bf..091168688a 100644 --- a/host/app/app.ts +++ b/host/app/app.ts @@ -12,6 +12,9 @@ import './lib/glint-embroider-workaround'; * runtime-common/index.js file. */ +// TODO eventually we should replace this with +// import "@cardstack/runtime-common/external-globals"; +// when our common external-globals can support glimmer (window as any).RUNTIME_SPIKE_EXTERNALS = new Map(); import * as glimmerComponent from '@glimmer/component'; (window as any).RUNTIME_SPIKE_EXTERNALS.set( diff --git a/host/tests/cards/person.gts b/host/tests/cards/person.gts index b85027c7d8..3c4b990324 100644 --- a/host/tests/cards/person.gts +++ b/host/tests/cards/person.gts @@ -1,5 +1,5 @@ -import { contains, field, Component, Card } from 'https://cardstack.com/base/card-api'; -import StringCard from 'https://cardstack.com/base/string'; +import { contains, field, Component, Card } from "https://cardstack.com/base/card-api"; +import StringCard from "https://cardstack.com/base/string"; export class Person extends Card { @field firstName = contains(StringCard); diff --git a/host/tests/cards/post.gts b/host/tests/cards/post.gts index 7a4f616d24..5b5e855666 100644 --- a/host/tests/cards/post.gts +++ b/host/tests/cards/post.gts @@ -1,5 +1,5 @@ -import { contains, field, Component, Card } from 'https://cardstack.com/base/card-api'; -import StringCard from 'https://cardstack.com/base/string'; +import { contains, field, Component, Card } from "https://cardstack.com/base/card-api"; +import StringCard from "https://cardstack.com/base/string"; import { Person } from "./person"; export class Post extends Card { diff --git a/realm-server/package.json b/realm-server/package.json index 04b0eccfd1..e9c57c3c09 100644 --- a/realm-server/package.json +++ b/realm-server/package.json @@ -27,6 +27,6 @@ "test": "qunit --require ts-node/register tests/index.ts", "start": "ts-node --transpileOnly main", "start:base": "ts-node --transpileOnly main --port=4201 --path='../base' --url='https://cardstack.com/base/' --baseRealmURL='http://localhost:4201/base/'", - "start:test-realms": "ts-node --transpileOnly main --port=4201 --path='../base' --url='https://cardstack.com/base/' --path='../host/tests/cards' --url='http://test-realm/test/' --baseRealmURL='http://localhost:4201/base/'" + "start:test-realms": "ts-node --transpileOnly main --port=4201 --path='../base' --url='https://cardstack.com/base/' --path='../host/tests/cards' --url='http://test-realm/test/' --path='./tests/cards' --url='http://test-realm/node-test/' --baseRealmURL='http://localhost:4201/base/'" } } diff --git a/realm-server/server.ts b/realm-server/server.ts index 1be666bee7..a0e426a908 100644 --- a/realm-server/server.ts +++ b/realm-server/server.ts @@ -5,6 +5,7 @@ import { resolve } from "path"; import { webStreamToText } from "@cardstack/runtime-common/stream"; import { LocalPath, RealmPaths } from "@cardstack/runtime-common/paths"; import { Readable } from "stream"; +import "@cardstack/runtime-common/externals-global"; const externalsPath = "/externals/"; @@ -137,7 +138,9 @@ function handleExternals(req: IncomingMessage, res: ServerResponse): void { return; } - let src = [`const m = window.RUNTIME_SPIKE_EXTERNALS.get('${moduleName}');`]; + let src = [ + `const m = globalThis.RUNTIME_SPIKE_EXTERNALS.get('${moduleName}');`, + ]; for (let name of names) { if (name === "default") { diff --git a/realm-server/tests/cards/cycle-one.js b/realm-server/tests/cards/cycle-one.js new file mode 100644 index 0000000000..a54d2ea83c --- /dev/null +++ b/realm-server/tests/cards/cycle-one.js @@ -0,0 +1,5 @@ +import { two } from "./cycle-two"; + +export function one() { + return two() - 1; +} diff --git a/realm-server/tests/cards/cycle-two.js b/realm-server/tests/cards/cycle-two.js new file mode 100644 index 0000000000..80cb7f6bb6 --- /dev/null +++ b/realm-server/tests/cards/cycle-two.js @@ -0,0 +1,9 @@ +import { one } from "./cycle-one"; + +export function two() { + return 2; +} + +export function three() { + return one() * 3; +} diff --git a/realm-server/tests/cards/person.gts b/realm-server/tests/cards/person.gts index b85027c7d8..3c4b990324 100644 --- a/realm-server/tests/cards/person.gts +++ b/realm-server/tests/cards/person.gts @@ -1,5 +1,5 @@ -import { contains, field, Component, Card } from 'https://cardstack.com/base/card-api'; -import StringCard from 'https://cardstack.com/base/string'; +import { contains, field, Component, Card } from "https://cardstack.com/base/card-api"; +import StringCard from "https://cardstack.com/base/string"; export class Person extends Card { @field firstName = contains(StringCard); diff --git a/realm-server/tests/realm-server-test.ts b/realm-server/tests/realm-server-test.ts index 646a72e238..30b6a61e14 100644 --- a/realm-server/tests/realm-server-test.ts +++ b/realm-server/tests/realm-server-test.ts @@ -9,12 +9,14 @@ import { cardSrc, compiledCard, } from "@cardstack/runtime-common/etc/test-fixtures"; -import { CardRef, isCardDocument } from "@cardstack/runtime-common"; +import { CardRef, isCardDocument, Realm } from "@cardstack/runtime-common"; import { stringify } from "qs"; +import { NodeRealm } from "../node-realm"; setGracefulCleanup(); const testRealmURL = new URL("http://127.0.0.1:4444/"); const testRealmHref = testRealmURL.href; +const testRealm2Href = "http://localhost:4201/node-test/"; module("Realm Server", function (hooks) { let server: Server; @@ -282,6 +284,22 @@ module("Realm Server", function (hooks) { kind: "file", }, }, + "cycle-one.js": { + links: { + related: "http://127.0.0.1:4444/cycle-one.js", + }, + meta: { + kind: "file", + }, + }, + "cycle-two.js": { + links: { + related: "http://127.0.0.1:4444/cycle-two.js", + }, + meta: { + kind: "file", + }, + }, "person-1.json": { links: { related: `${testRealmHref}person-1.json`, @@ -413,4 +431,51 @@ module("Realm Server", function (hooks) { "card ID is correct" ); }); + + test("can dynamically load a card from own realm", async function (assert) { + let nodeRealm = new NodeRealm(dir.name); + let realm = new Realm( + "http://test-realm/", + nodeRealm, + "http://localhost:4201/base/" + ); + await realm.ready; + + let module = await realm.load>("./person"); + let Person = module["Person"]; + let person = Person.fromSerialized({ firstName: "Mango" }); + assert.strictEqual(person.firstName, "Mango", "card data is correct"); + }); + + test("can dynamically load a card from a different realm", async function (assert) { + let nodeRealm = new NodeRealm(dir.name); + let realm = new Realm( + "http://test-realm/", + nodeRealm, + "http://localhost:4201/base/" + ); + await realm.ready; + + let module = await realm.load>( + `${testRealm2Href}person` + ); + let Person = module["Person"]; + let person = Person.fromSerialized({ firstName: "Mango" }); + assert.strictEqual(person.firstName, "Mango", "card data is correct"); + }); + + test("can dynamically modules with cycles", async function (assert) { + let nodeRealm = new NodeRealm(dir.name); + let realm = new Realm( + "http://test-realm/", + nodeRealm, + "http://localhost:4201/base/" + ); + await realm.ready; + + let module = await realm.load<{ three(): number }>( + `${testRealm2Href}cycle-two` + ); + assert.strictEqual(module.three(), 3); + }); }); diff --git a/runtime-common/etc/test-fixtures.ts b/runtime-common/etc/test-fixtures.ts index 3d74fba64f..4dd1d9bf91 100644 --- a/runtime-common/etc/test-fixtures.ts +++ b/runtime-common/etc/test-fixtures.ts @@ -1,6 +1,6 @@ export const cardSrc = ` -import { contains, field, Component, Card } from 'https://cardstack.com/base/card-api'; -import StringCard from 'https://cardstack.com/base/string'; +import { contains, field, Component, Card } from "https://cardstack.com/base/card-api"; +import StringCard from "https://cardstack.com/base/string"; export class Person extends Card { @field firstName = contains(StringCard); @@ -25,8 +25,8 @@ function _applyDecoratedDescriptor(target, property, decorators, descriptor, con function _initializerWarningHelper(descriptor, context) { throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and runs after the decorators transform.'); } -import { contains, field, Component, Card } from 'https://cardstack.com/base/card-api'; -import StringCard from 'https://cardstack.com/base/string'; +import { contains, field, Component, Card } from "https://cardstack.com/base/card-api"; +import StringCard from "https://cardstack.com/base/string"; export let Person = (_class = (_class2 = class Person extends Card { constructor(...args) { super(...args); diff --git a/runtime-common/externals-global.ts b/runtime-common/externals-global.ts new file mode 100644 index 0000000000..6156a635b5 --- /dev/null +++ b/runtime-common/externals-global.ts @@ -0,0 +1,59 @@ +/* The following modules are made available to cards as external modules. + * This is paired with the worker/src/externals.ts file which is responsible + * for compiling the external module stubs into the cards, which consumes the + * modules in the globalThis.RUNTIME_SPIKE_EXTERNALS Map. Any changes to the + * globalThis.RUNTIME_SPIKE_EXTERNALS Map should also be reflected in the in the + * runtime-common/index.js file. + */ + +(globalThis as any).RUNTIME_SPIKE_EXTERNALS = new Map(); +// import * as glimmerComponent from "@glimmer/component"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set("@glimmer/component", { + default: class {}, +}); +// import * as emberComponent from "ember-source/dist/packages/@ember/component"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set("@ember/component", { + default: class {}, + setComponentTemplate() {}, +}); +// import * as emberComponentTemplateOnly from "ember-source/dist/packages/@ember/component/template-only"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set( + "@ember/component/template-only", + { default() {} } +); +// import * as emberTemplateFactory from "ember-source/dist/packages/@ember/template-factory"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set("@ember/template-factory", { + createTemplateFactory() {}, +}); +// import * as glimmerTracking from "@glimmer/tracking"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set("@glimmer/tracking", { + tracked() {}, +}); +// import * as emberObject from "ember-source/dist/packages/@ember/object"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set("@ember/object", { + action() {}, + get() {}, +}); +// import * as emberHelper from "ember-source/dist/packages/@ember/helper"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set("@ember/helper", { + get() {}, + fn() {}, +}); +// import * as emberModifier from "ember-source/dist/packages/@ember/modifier"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set("@ember/modifier", { + on() {}, +}); +// import * as emberDestroyable from "ember-source/dist/packages/@ember/destroyable"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set("@ember/destroyable", { + registerDestructor() {}, +}); +// import * as tracked from "tracked-built-ins"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set("tracked-built-ins", { + // TODO replace with actual TrackedWeakMap when we add real glimmer + // implementations + TrackedWeakMap: WeakMap, +}); +import * as lodash from "lodash"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set("lodash", lodash); +import * as dateFns from "date-fns"; +(globalThis as any).RUNTIME_SPIKE_EXTERNALS.set("date-fns", dateFns); diff --git a/runtime-common/loader.ts b/runtime-common/loader.ts new file mode 100644 index 0000000000..b7e979c356 --- /dev/null +++ b/runtime-common/loader.ts @@ -0,0 +1,256 @@ +// @ts-ignore +import TransformModulesAmd from "@babel/plugin-transform-modules-amd"; +import { transformSync } from "@babel/core"; +import { Deferred } from "./deferred"; +import type { Realm } from "@cardstack/runtime-common/realm"; +import { RealmPaths } from "./paths"; +import { baseRealm } from "@cardstack/runtime-common"; + +type RegisteredModule = { + state: "registered"; + dependencyList: string[]; + implementation: Function; +}; + +// a module is in this state until its own code *and the code for all its deps* +// have been loaded. Modules move from fetching to registered depth-first. +type FetchingModule = { + state: "fetching"; + + // if you encounter a module in this state, you should wait for the deferred + // and then retry load where you're guarantee to see a new state + deferred: Deferred; +}; + +type Module = + | FetchingModule + | RegisteredModule + | { + // this state represents the *synchronous* window of time where this + // module's dependencies are moving from registered to preparing to + // evaluated. Because this is synchronous, you can rely on the fact that + // encountering a load for a module that is in "preparing" means you have a + // cycle. + state: "preparing"; + implementation: Function; + moduleInstance: object; + } + | { + state: "evaluated"; + moduleInstance: object; + } + | { + state: "broken"; + exception: any; + }; + +export class Loader { + private modules = new Map(); + private realmPath: RealmPaths; + + constructor( + private realm: Realm, + private readFileAsText: (url: URL) => Promise + ) { + this.realmPath = new RealmPaths(realm.url); + } + + async load(moduleIdentifier: string): Promise { + moduleIdentifier = this.resolveModule(moduleIdentifier); + let module = await this.fetchModule(moduleIdentifier); + switch (module.state) { + case "fetching": + await module.deferred.promise; + return this.evaluateModule(moduleIdentifier); + case "preparing": + case "evaluated": + return module.moduleInstance as T; + case "broken": + throw module.exception; + case "registered": + return this.evaluateModule(moduleIdentifier); + default: + throw assertNever(module); + } + } + + clearCache() { + this.modules = new Map(); + } + + private resolveModule(moduleIdentifier: string, relativeTo?: string): string { + if (relativeTo) { + moduleIdentifier = new URL(moduleIdentifier, relativeTo).href; + } + + if (!moduleIdentifier.startsWith("http")) { + throw new Error( + `expected module identifier to be a URL: "${moduleIdentifier}"` + ); + } + let moduleURL = new URL(moduleIdentifier); + if (baseRealm.inRealm(moduleURL)) { + return this.realm.baseRealmURL + baseRealm.local(moduleURL); + } + return moduleIdentifier; + } + + private async fetchModule(moduleIdentifier: string): Promise { + let module = this.modules.get(moduleIdentifier); + if (module) { + return module; + } + module = { + state: "fetching", + deferred: new Deferred(), + }; + this.modules.set(moduleIdentifier, module); + + let src: string; + try { + src = await this.fetch(new URL(moduleIdentifier)); + } catch (exception) { + this.modules.set(moduleIdentifier, { + state: "broken", + exception, + }); + throw exception; + } + src = transformSync(src, { + plugins: [ + [TransformModulesAmd, { noInterop: true, moduleId: moduleIdentifier }], + ], + })?.code!; + + let dependencyList: string[]; + let implementation: Function; + + // this local is here for the evals to see + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let define = (_mid: string, depList: string[], impl: Function) => { + dependencyList = depList.map((depId) => { + if (depId === "exports") { + return "exports"; + } else { + return this.resolveModule(depId, moduleIdentifier); + } + }); + implementation = impl; + }; + + try { + eval(src); + } catch (exception) { + this.modules.set(moduleIdentifier, { + state: "broken", + exception, + }); + throw exception; + } + + await Promise.all( + dependencyList!.map((depId) => { + if (depId !== "exports") { + return this.fetchModule(depId); + } + return undefined; + }) + ); + + let registeredModule: RegisteredModule = { + state: "registered", + dependencyList: dependencyList!, + implementation: implementation!, + }; + + this.modules.set(moduleIdentifier, registeredModule); + module.deferred.fulfill(); + return registeredModule; + } + + private evaluateModule(moduleIdentifier: string): T { + let module = this.modules.get(moduleIdentifier); + if (!module) { + throw new Error( + `bug in module loader. ${moduleIdentifier} should have been registered before entering evaluateModule` + ); + } + switch (module.state) { + case "fetching": + throw new Error( + `bug in module loader. ${moduleIdentifier} should have been registered before entering evaluateModule` + ); + case "preparing": + case "evaluated": + return module.moduleInstance as T; + case "broken": + throw module.exception; + case "registered": + return this.evaluate(moduleIdentifier, module); + default: + throw assertNever(module); + } + } + + private evaluate(moduleIdentifier: string, module: RegisteredModule): T { + let moduleInstance = Object.create(null); + this.modules.set(moduleIdentifier, { + state: "preparing", + implementation: module.implementation, + moduleInstance, + }); + + try { + let dependencies = module.dependencyList.map((dependencyIdentifier) => { + if (dependencyIdentifier === "exports") { + return moduleInstance; + } else { + return this.evaluateModule(dependencyIdentifier); + } + }); + + module.implementation(...dependencies); + this.modules.set(moduleIdentifier, { + state: "evaluated", + moduleInstance, + }); + return moduleInstance; + } catch (exception) { + this.modules.set(moduleIdentifier, { + state: "broken", + exception, + }); + throw exception; + } + } + + private async fetch(moduleURL: URL): Promise { + if (this.realmPath.inRealm(moduleURL)) { + return await this.readFileAsText(moduleURL); + } + + let response: Response; + try { + response = await fetch(moduleURL.href); + } catch (err) { + console.error(`fetch failed for ${moduleURL}`, err); // to aid in debugging, since this exception doesn't include the URL that failed + // this particular exception might not be worth caching the module in a + // "broken" state, since the server hosting the module is likely down. it + // might be a good idea to be able to try again in this case... + throw err; + } + if (!response.ok) { + throw new Error( + `Could not retrieve ${moduleURL}: ${ + response.status + } - ${await response.text()}` + ); + } + return await response.text(); + } +} + +function assertNever(value: never) { + throw new Error(`should never happen ${value}`); +} diff --git a/runtime-common/package.json b/runtime-common/package.json index 56b326a860..ce77d1b394 100644 --- a/runtime-common/package.json +++ b/runtime-common/package.json @@ -9,11 +9,18 @@ "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-decorators": "^7.17.12", "@babel/plugin-syntax-typescript": "^7.17.12", + "@babel/plugin-transform-modules-amd": "^7.13.0", "@babel/plugin-transform-typescript": "^7.16.8", "@babel/traverse": "^7.17.9", "@cardstack/ember-template-imports": "2.0.1", "@types/babel__core": "^7.1.19", "@types/babel__traverse": "^7.14.2", - "ember-source": "~4.2.0" + "@types/lodash": "^4.14.182", + "date-fns": "^2.28.0", + "ember-source": "~4.2.0", + "lodash": "^4.17.21" + }, + "volta": { + "extends": "../package.json" } } diff --git a/runtime-common/realm.ts b/runtime-common/realm.ts index ece4c11e4b..6e4f7cef0c 100644 --- a/runtime-common/realm.ts +++ b/runtime-common/realm.ts @@ -6,6 +6,7 @@ import { CardDocument, isCardDocument, } from "./search-index"; +import { Loader } from "./loader"; import { RealmPaths, LocalPath, join } from "./paths"; import { systemError, @@ -78,6 +79,7 @@ export class Realm { #paths: RealmPaths; #jsonAPIRouter: Router; #cardSourceRouter: Router; + #loader: Loader; get url(): string { return this.#paths.url; @@ -97,6 +99,9 @@ export class Realm { this.#adapter.readdir.bind(this.#adapter), this.readFileAsText.bind(this) ); + this.#loader = new Loader(this, async (url: URL) => { + return this.transpileJS((await this.getCardSourceAsText(url))!, url.href); + }); this.#jsonAPIRouter = new Router(new URL(url)) .post("/", this.createCard.bind(this)) @@ -117,6 +122,11 @@ export class Realm { .delete("/.+", this.removeCardSource.bind(this)); } + async load(moduleIdentifier: string): Promise { + let moduleURL = new URL(moduleIdentifier, this.url); + return await this.#loader.load(moduleURL.href); + } + async write( path: LocalPath, contents: string @@ -206,6 +216,7 @@ export class Realm { this.#paths.local(new URL(request.url)), await request.text() ); + this.#loader.clearCache(); return new Response(null, { status: 204, headers: { @@ -234,6 +245,16 @@ export class Realm { return await this.serveLocalFile(handle); } + // as opposed to getCardSourceOrRedirect, this will follow the redirect + private async getCardSourceAsText(url: URL): Promise { + let localName = this.#paths.local(url); + let handle = await this.getFileWithFallbacks(localName); + if (!handle) { + return undefined; + } + return (await this.readFileAsText(handle.path))?.content; + } + private async removeCardSource(request: Request): Promise { let localName = this.#paths.local(new URL(request.url)); let handle = await this.getFileWithFallbacks(localName); @@ -244,43 +265,48 @@ export class Realm { delete: true, }); await this.#adapter.remove(handle.path); + this.#loader.clearCache(); return new Response(null, { status: 204 }); } + private transpileJS(content: string, debugFilename: string): string { + content = preprocessEmbeddedTemplates(content, { + relativePath: debugFilename, + getTemplateLocals: etc._GlimmerSyntax.getTemplateLocals, + templateTag: "template", + templateTagReplacement: "__GLIMMER_TEMPLATE", + includeSourceMaps: true, + includeTemplateTokens: true, + }).output; + return babel.transformSync(content, { + filename: debugFilename, + plugins: [ + glimmerTemplatePlugin, + [typescriptPlugin, { allowDeclareFields: true }], + [decoratorsProposalPlugin, { legacy: true }], + classPropertiesProposalPlugin, + // this "as any" is because typescript is using the Node-specific types + // from babel-plugin-ember-template-compilation, but we're using the + // browser interface + isNode + ? [ + makeEmberTemplatePlugin, + { + precompile: etc.precompile, + }, + ] + : (makeEmberTemplatePlugin as any)(() => etc.precompile), + [externalsPlugin, { realm: this }], + ], + })!.code!; + } + private async makeJS( content: string, debugFilename: string ): Promise { try { - content = preprocessEmbeddedTemplates(content, { - relativePath: debugFilename, - getTemplateLocals: etc._GlimmerSyntax.getTemplateLocals, - templateTag: "template", - templateTagReplacement: "__GLIMMER_TEMPLATE", - includeSourceMaps: true, - includeTemplateTokens: true, - }).output; - content = babel.transformSync(content, { - filename: debugFilename, - plugins: [ - glimmerTemplatePlugin, - [typescriptPlugin, { allowDeclareFields: true }], - [decoratorsProposalPlugin, { legacy: true }], - classPropertiesProposalPlugin, - // this "as any" is because typescript is using the Node-specific types - // from babel-plugin-ember-template-compilation, but we're using the - // browser interface - isNode - ? [ - makeEmberTemplatePlugin, - { - precompile: etc.precompile, - }, - ] - : (makeEmberTemplatePlugin as any)(() => etc.precompile), - [externalsPlugin, { realm: this }], - ], - })!.code!; + content = this.transpileJS(content, debugFilename); } catch (err: any) { Promise.resolve().then(() => { throw err; diff --git a/worker/src/main.ts b/worker/src/main.ts index 2526f20ac1..b42f755313 100644 --- a/worker/src/main.ts +++ b/worker/src/main.ts @@ -3,6 +3,7 @@ import { LivenessWatcher } from './liveness'; import { MessageHandler } from './message-handler'; import { LocalRealm } from './local-realm'; import { Realm } from '@cardstack/runtime-common'; +import '@cardstack/runtime-common/externals-global'; const worker = globalThis as unknown as ServiceWorkerGlobalScope;