diff --git a/.github/workflows/ci-host.yaml b/.github/workflows/ci-host.yaml index 21c258d1194..d61e6ab7393 100644 --- a/.github/workflows/ci-host.yaml +++ b/.github/workflows/ci-host.yaml @@ -775,7 +775,16 @@ jobs: # raw notFound when the host store does a direct GET) point at # the same race; match both. A second shard pass typically lands # after the race resolves. - RETRY_PATTERN='ChunkLoadError|Failed to fetch dynamically imported module|NetworkError when attempting to fetch resource|unable to fetch https://icons\.[^:]+: fetch failed|cross-realm fetch failed for https://realm-test\.[^/]+|Could not find https://realm-test\.[^/"]+' + # The `Global error: Uncaught TypeError: Failed to fetch` form is a + # test-harness startup race: base card components (workspace, + # cards-grid) fire a `_types` fetch against the mock test realm the + # moment they render, and with base modules compiled into the host + # bundle that render can land before the test-realm service worker + # is intercepting — the fetch escapes to the real network and the + # rejection surfaces as a Global error. The race is rare per shard, + # so a second pass lands; the durable fix is queuing test-realm + # fetches in the harness until realm registration completes. + RETRY_PATTERN='ChunkLoadError|Failed to fetch dynamically imported module|NetworkError when attempting to fetch resource|unable to fetch https://icons\.[^:]+: fetch failed|cross-realm fetch failed for https://realm-test\.[^/]+|Could not find https://realm-test\.[^/"]+|Global error: Uncaught TypeError: Failed to fetch' if [ $exit_code -ne 0 ] && grep -Eq "$RETRY_PATTERN" /tmp/test-output.log; then echo "" echo "::warning::Transient chunk-fetch failure detected — retrying shard ${{ matrix.shardIndex }}" diff --git a/packages/base/audio-file-def.gts b/packages/base/audio-file-def.gts index 90199e98218..d3dc4005e5b 100644 --- a/packages/base/audio-file-def.gts +++ b/packages/base/audio-file-def.gts @@ -1,6 +1,6 @@ import MusicIcon from '@cardstack/boxel-icons/music'; import { - BaseDefComponent, + type BaseDefComponent, Component, NumberField, contains, diff --git a/packages/base/brand-guide.gts b/packages/base/brand-guide.gts index 2e1b982d947..f0880d04fe2 100644 --- a/packages/base/brand-guide.gts +++ b/packages/base/brand-guide.gts @@ -31,7 +31,7 @@ import { buildCssVariableName, sanitizeHtmlSafe, eq, - CssVariableEntry, + type CssVariableEntry, } from '@cardstack/boxel-ui/helpers'; import { cardTypeDisplayName } from '@cardstack/runtime-common'; diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index b4f970e8457..3b212235126 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -27,8 +27,8 @@ import { baseRef, CardContextName, CardError, - CodeRef, - ToolContext, + type CodeRef, + type ToolContext, Deferred, byteStreamToUint8Array, fields, @@ -56,12 +56,12 @@ import { loadCardDocument, Loader, localId, - LocalPath, + type LocalPath, meta, primitive, realmURL, relativeTo, - SingleCardDocument, + type SingleCardDocument, uuidv4, NumberSerializer, type Format, @@ -87,9 +87,9 @@ import { FileMetaResourceType, CardResourceType, loadFileMetaDocument, - CardResource, - LooseLinkableResource, - LooseSingleResourceDocument, + type CardResource, + type LooseLinkableResource, + type LooseSingleResourceDocument, shouldTrackRuntimeModuleGraph, shouldTrackRuntimeRelationship, trackRuntimeFileDependency, @@ -207,8 +207,8 @@ import { TextInputValidator } from './text-input-validator'; import { type GetMenuItemParams, getDefaultCardMenuItems } from './menu-items'; import { getDefaultFileMenuItems } from './file-menu-items'; import { - LinkableDocument, - SingleFileMetaDocument, + type LinkableDocument, + type SingleFileMetaDocument, } from '@cardstack/runtime-common/document-types'; import type { MarkdownEmbedChooser } from '@cardstack/runtime-common/bfm-card-references'; import type { FileMetaResource } from '@cardstack/runtime-common'; @@ -5072,16 +5072,22 @@ export function resolveRef( } function myLoader(): Loader { - // we know this code is always loaded by an instance of our Loader, which sets - // import.meta.loader. + // A Loader that evaluates this module injects `import.meta.loader`. When + // the module is compiled into the host bundle instead, the platform + // evaluates it and no loader is injected; the host publishes the loader + // bundled modules should use (see Loader.setForBundledModules). - // When type-checking realm-server, tsc sees this file and thinks - // it will be transpiled to CommonJS and so it complains about this line. But - // this file is always loaded through our loader and always has access to import.meta. + // When type-checking realm-server, tsc sees this file and thinks it will + // be transpiled to CommonJS and so it complains about import.meta. // @ts-ignore - return (import.meta as any).loader; + let loader = (import.meta as any).loader ?? Loader.forBundledModules(); + if (!loader) { + throw new Error('No Loader is available to this module'); + } + return loader; } + class FallbackCardStore implements CardStore { #instances: Map = new Map(); #fileMetaInstances: Map = new Map(); diff --git a/packages/base/card-serialization.ts b/packages/base/card-serialization.ts index 6a403ea0280..459e876d811 100644 --- a/packages/base/card-serialization.ts +++ b/packages/base/card-serialization.ts @@ -5,7 +5,6 @@ import type { CardResource, CardResourceMeta, FileMetaResource, - Loader, LooseCardResource, LooseFileMetaResource, LooseSingleCardDocument, @@ -25,6 +24,7 @@ import { isEqual, merge } from 'lodash-es'; import { assertIsSerializerName, CardResourceType, + Loader, fieldSerializer, FileMetaResourceType, getSerializer, @@ -92,14 +92,19 @@ export const deserialize = Symbol.for('cardstack-deserialize'); // --- Serialization Functions --- function myLoader(): Loader { - // we know this code is always loaded by an instance of our Loader, which sets - // import.meta.loader. + // A Loader that evaluates this module injects `import.meta.loader`. When + // the module is compiled into the host bundle instead, the platform + // evaluates it and no loader is injected; the host publishes the loader + // bundled modules should use (see Loader.setForBundledModules). - // When type-checking realm-server, tsc sees this file and thinks - // it will be transpiled to CommonJS and so it complains about this line. But - // this file is always loaded through our loader and always has access to import.meta. + // When type-checking realm-server, tsc sees this file and thinks it will + // be transpiled to CommonJS and so it complains about import.meta. // @ts-ignore - return (import.meta as any).loader; + let loader = (import.meta as any).loader ?? Loader.forBundledModules(); + if (!loader) { + throw new Error('No Loader is available to this module'); + } + return loader; } export async function cardClassFromResource( diff --git a/packages/base/cards-grid.gts b/packages/base/cards-grid.gts index a4f9c1491f6..904057c2b66 100644 --- a/packages/base/cards-grid.gts +++ b/packages/base/cards-grid.gts @@ -21,11 +21,12 @@ import { baseRealmRRI, baseFileRef, isCardInstance, + Loader, SupportedMimeType, subscribeToRealm, codeRefFromInternalKey, type Query, - CardErrorJSONAPI, + type CardErrorJSONAPI, } from '@cardstack/runtime-common'; import CardsGridLayout, { @@ -50,6 +51,24 @@ import type { RealmEventContent } from './matrix-event'; import { Spec } from './spec'; import StringField from './string'; +// A realm URL that reaches a card can be a virtual alias (e.g. +// `https://cardstack.com/base/`) that only the virtual network knows how to +// resolve; a native fetch of one leaves the page for a host that need not +// exist, failing with `TypeError: Failed to fetch`. The loader's fetch maps +// the alias to the URL the realm is really served from and carries realm +// auth, rather than relying on the auth service worker to inject it. A base +// module compiled into the host bundle is evaluated by the platform and so +// has no `import.meta.loader`; it uses the loader the host publishes for +// bundled modules instead. +function realmFetch(): typeof globalThis.fetch { + // When type-checking realm-server, tsc sees this file and thinks it will be + // transpiled to CommonJS and so it complains about import.meta. But this + // file always runs as ESM. + // @ts-ignore + let loader = (import.meta as any).loader ?? Loader.forBundledModules(); + return loader?.fetch ?? fetch; +} + const [_CardView, StripView, GridView] = VIEW_OPTIONS; class Isolated extends Component { @@ -333,7 +352,7 @@ class Isolated extends Component { if (!realm) { return; } - let response = await fetch(`${realm}_types`, { + let response = await realmFetch()(`${realm}_types`, { headers: { Accept: SupportedMimeType.CardTypeSummary, }, diff --git a/packages/base/code-ref.gts b/packages/base/code-ref.gts index e69e18b9251..f956a9f29e7 100644 --- a/packages/base/code-ref.gts +++ b/packages/base/code-ref.gts @@ -9,6 +9,7 @@ import { CardURLContextName, fieldSerializer, CodeRefSerializer, + Loader, } from '@cardstack/runtime-common'; import { not } from '@cardstack/boxel-ui/helpers'; import { BoxelInput } from '@cardstack/boxel-ui/components'; @@ -75,7 +76,14 @@ class EditView extends Component { module = new URL(module, new URL(this.cardURL)).href; } try { - let code = (await import(module))[name]; + // Load through the Loader rather than a bare dynamic import: the + // module is a runtime realm URL, which only a Loader can resolve + // (shims, realm mappings, authenticated fetch). Inside + // loader-evaluated modules the AMD transpile rewrites `import()` + // this way implicitly; compiled-in modules must do it explicitly. + let code = (await myLoader().import>(module))[ + name + ]; if (code) { this.validationState = 'valid'; if (!opts?.checkOnly) { @@ -91,6 +99,22 @@ class EditView extends Component { ); } +function myLoader(): Loader { + // A Loader that evaluates this module injects `import.meta.loader`. When + // the module is compiled into the host bundle instead, the platform + // evaluates it and no loader is injected; the host publishes the loader + // bundled modules should use (see Loader.setForBundledModules). + + // When type-checking realm-server, tsc sees this file and thinks it will + // be transpiled to CommonJS and so it complains about import.meta. + // @ts-ignore + let loader = (import.meta as any).loader ?? Loader.forBundledModules(); + if (!loader) { + throw new Error('No Loader is available to this module'); + } + return loader; +} + export default class CodeRefField extends FieldDef { static displayName = 'CodeRef'; static icon = CodeIcon; diff --git a/packages/base/color-field/components/advanced-color-picker.gts b/packages/base/color-field/components/advanced-color-picker.gts index ac79db7b084..1c7eb427815 100644 --- a/packages/base/color-field/components/advanced-color-picker.gts +++ b/packages/base/color-field/components/advanced-color-picker.gts @@ -13,10 +13,10 @@ import type { ColorFieldConfiguration } from '../util/color-utils'; import { parseCssColor, parseCssColorSafe } from '../util/color-utils'; import { detectColorFormat, - RichColorFormat, + type RichColorFormat, hexToRgba, hsvToRgb, - RGBA, + type RGBA, rgbaToFormatString, rgbaToHexString, rgbaToHsl, diff --git a/packages/base/color-field/components/color-wheel-picker.gts b/packages/base/color-field/components/color-wheel-picker.gts index 4e17071a2bf..87139e990fd 100644 --- a/packages/base/color-field/components/color-wheel-picker.gts +++ b/packages/base/color-field/components/color-wheel-picker.gts @@ -13,7 +13,7 @@ import type { import { detectColorFormat, hslToRgb, - RGBA, + type RGBA, rgbaToFormatString, rgbaToHsv, } from '@cardstack/boxel-ui/helpers'; diff --git a/packages/base/color-field/components/slider-picker.gts b/packages/base/color-field/components/slider-picker.gts index 252e18d46cd..f20686bb8b1 100644 --- a/packages/base/color-field/components/slider-picker.gts +++ b/packages/base/color-field/components/slider-picker.gts @@ -10,8 +10,8 @@ import type Owner from '@ember/owner'; import type { ColorFieldSignature } from '../util/color-field-signature'; import { parseCssColor, - SliderColorFormat, - SliderVariantConfiguration, + type SliderColorFormat, + type SliderVariantConfiguration, } from '../util/color-utils'; import { detectColorFormat, diff --git a/packages/base/color.gts b/packages/base/color.gts index 3d1977d5004..b2d71ebd528 100644 --- a/packages/base/color.gts +++ b/packages/base/color.gts @@ -1,5 +1,5 @@ -import { Component } from '@cardstack/base/card-api'; -import StringField from '@cardstack/base/string'; +import { Component } from './card-api'; +import StringField from './string'; import { Swatch } from '@cardstack/boxel-ui/components'; import { markdownEscape } from '@cardstack/boxel-ui/helpers'; import PaletteIcon from '@cardstack/boxel-icons/palette'; diff --git a/packages/base/contains-many-component.gts b/packages/base/contains-many-component.gts index 3e045e9ef1e..f054be40681 100644 --- a/packages/base/contains-many-component.gts +++ b/packages/base/contains-many-component.gts @@ -447,12 +447,18 @@ export function getContainsManyComponent({ } function myLoader(): Loader { - // we know this code is always loaded by an instance of our Loader, which sets - // import.meta.loader. + // A Loader that evaluates this module injects `import.meta.loader`. When + // the module is compiled into the host bundle instead, the platform + // evaluates it and no loader is injected; the host publishes the loader + // bundled modules should use (see Loader.setForBundledModules). - // When type-checking realm-server, tsc sees this file and thinks - // it will be transpiled to CommonJS and so it complains about this line. But - // this file is always loaded through our loader and always has access to import.meta. + // When type-checking realm-server, tsc sees this file and thinks it will + // be transpiled to CommonJS and so it complains about import.meta. // @ts-ignore - return (import.meta as any).loader; + let loader = (import.meta as any).loader ?? Loader.forBundledModules(); + if (!loader) { + throw new Error('No Loader is available to this module'); + } + return loader; } + diff --git a/packages/base/csv-file-def.gts b/packages/base/csv-file-def.gts index 3fbc4e3e309..bdb21411293 100644 --- a/packages/base/csv-file-def.gts +++ b/packages/base/csv-file-def.gts @@ -3,7 +3,7 @@ import { htmlSafe } from '@ember/template'; import CsvIcon from '@cardstack/boxel-icons/csv'; import GlimmerComponent from '@glimmer/component'; import { - BaseDefComponent, + type BaseDefComponent, Component, StringField, contains, diff --git a/packages/base/field-component.gts b/packages/base/field-component.gts index c558492cc66..6615d6611ea 100644 --- a/packages/base/field-component.gts +++ b/packages/base/field-component.gts @@ -8,10 +8,10 @@ import { type BaseDefComponent, type BaseDefConstructor, type Theme, - CardContext, + type CardContext, formats, - FieldFormats, - CardCrudFunctions, + type FieldFormats, + type CardCrudFunctions, } from './card-api'; import { isCard, isCompoundField } from './field-support'; import { @@ -23,7 +23,7 @@ import { isCardInstance, type CodeRef, type Permissions, - ResolvedCodeRef, + type ResolvedCodeRef, CardCrudFunctionsContextName, } from '@cardstack/runtime-common'; import type { ComponentLike } from '@glint/template'; diff --git a/packages/base/file-formats/file-resources.gts b/packages/base/file-formats/file-resources.gts index b80743dd271..a44aafe35a8 100644 --- a/packages/base/file-formats/file-resources.gts +++ b/packages/base/file-formats/file-resources.gts @@ -6,6 +6,8 @@ import { htmlSafe } from '@ember/template'; import GlimmerComponent from '@glimmer/component'; import { modifier } from 'ember-modifier'; +import { waitForPromise } from '@cardstack/runtime-common'; + import { profileForFile, type FileTypeProfile } from './file-type-profile'; // The image primitive lives in its own lean module because card-api's // universal graph reaches it (via `image-preview`); re-exported here so this @@ -244,34 +246,40 @@ const loadProtectedMediaBlob = modifier( let objectURL: string | undefined; let controller = new AbortController(); element.removeAttribute('src'); - void (async () => { - try { - // `same-origin` rather than `include`: the realm server answers with - // `Access-Control-Allow-Origin: *`, which a credentialed cross-origin - // request rejects. - let response = await fetch(resourceURL, { - credentials: 'same-origin', - signal: controller.signal, - }); - if (!response.ok) { - throw new Error(`Media fetch failed with HTTP ${response.status}`); - } - let blob = await response.blob(); - if (cancelled) { - return; - } - objectURL = URL.createObjectURL(blob); - element.src = objectURL; - element.load(); - } catch { - if (!cancelled) { - // Fall back to the canonical source so playback degrades rather - // than disappearing. - element.src = resourceURL; + // Wrapped so a test's `settled()` waits for the blob swap (and for the + // fallback assignment below when the fetch rejects) instead of asserting + // against an element whose src hasn't been decided yet. Outside tests + // `waitForPromise` passes the promise straight through. + void waitForPromise( + (async () => { + try { + // `same-origin` rather than `include`: the realm server answers with + // `Access-Control-Allow-Origin: *`, which a credentialed cross-origin + // request rejects. + let response = await fetch(resourceURL, { + credentials: 'same-origin', + signal: controller.signal, + }); + if (!response.ok) { + throw new Error(`Media fetch failed with HTTP ${response.status}`); + } + let blob = await response.blob(); + if (cancelled) { + return; + } + objectURL = URL.createObjectURL(blob); + element.src = objectURL; element.load(); + } catch { + if (!cancelled) { + // Fall back to the canonical source so playback degrades rather + // than disappearing. + element.src = resourceURL; + element.load(); + } } - } - })(); + })(), + ); return () => { cancelled = true; controller.abort(); diff --git a/packages/base/json-file-def.gts b/packages/base/json-file-def.gts index 392869b11c9..2133efcbd50 100644 --- a/packages/base/json-file-def.gts +++ b/packages/base/json-file-def.gts @@ -3,7 +3,7 @@ import { htmlSafe } from '@ember/template'; import JsonIcon from '@cardstack/boxel-icons/json'; import GlimmerComponent from '@glimmer/component'; import { - BaseDefComponent, + type BaseDefComponent, Component, NumberField, StringField, diff --git a/packages/base/links-to-editor.gts b/packages/base/links-to-editor.gts index 2c22b582412..d633c916217 100644 --- a/packages/base/links-to-editor.gts +++ b/packages/base/links-to-editor.gts @@ -14,7 +14,7 @@ import { type Field, type CardContext, type LinkableDefConstructor, - CreateCardFn, + type CreateCardFn, isFileDef, } from './card-api'; import { @@ -282,12 +282,18 @@ export class LinksToEditor extends GlimmerComponent { } function myLoader(): Loader { - // we know this code is always loaded by an instance of our Loader, which sets - // import.meta.loader. + // A Loader that evaluates this module injects `import.meta.loader`. When + // the module is compiled into the host bundle instead, the platform + // evaluates it and no loader is injected; the host publishes the loader + // bundled modules should use (see Loader.setForBundledModules). - // When type-checking realm-server, tsc sees this file and thinks - // it will be transpiled to CommonJS and so it complains about this line. But - // this file is always loaded through our loader and always has access to import.meta. + // When type-checking realm-server, tsc sees this file and thinks it will + // be transpiled to CommonJS and so it complains about import.meta. // @ts-ignore - return (import.meta as any).loader; + let loader = (import.meta as any).loader ?? Loader.forBundledModules(); + if (!loader) { + throw new Error('No Loader is available to this module'); + } + return loader; } + diff --git a/packages/base/links-to-many-component.gts b/packages/base/links-to-many-component.gts index d742d03eabd..9d10b215fa5 100644 --- a/packages/base/links-to-many-component.gts +++ b/packages/base/links-to-many-component.gts @@ -11,8 +11,8 @@ import { type FieldDef, type Format, type LinkableDefConstructor, - CreateCardFn, - CardCrudFunctions, + type CreateCardFn, + type CardCrudFunctions, isFileDef, brokenLinkFormat, } from './card-api'; @@ -22,7 +22,7 @@ import { } from './field-support'; import { rawArrayValues } from './watched-array'; import { - BoxComponentSignature, + type BoxComponentSignature, CardCrudFunctionsConsumer, DefaultFormatsConsumer, PermissionsConsumer, @@ -49,7 +49,7 @@ import { type ResolvedCodeRef, uuidv4, CardCrudFunctionsContextName, - CardErrorJSONAPI, + type CardErrorJSONAPI, cardTypeName, } from '@cardstack/runtime-common'; import { @@ -825,12 +825,18 @@ export function getLinksToManyComponent({ } function myLoader(): Loader { - // we know this code is always loaded by an instance of our Loader, which sets - // import.meta.loader. + // A Loader that evaluates this module injects `import.meta.loader`. When + // the module is compiled into the host bundle instead, the platform + // evaluates it and no loader is injected; the host publishes the loader + // bundled modules should use (see Loader.setForBundledModules). - // When type-checking realm-server, tsc sees this file and thinks - // it will be transpiled to CommonJS and so it complains about this line. But - // this file is always loaded through our loader and always has access to import.meta. + // When type-checking realm-server, tsc sees this file and thinks it will + // be transpiled to CommonJS and so it complains about import.meta. // @ts-ignore - return (import.meta as any).loader; + let loader = (import.meta as any).loader ?? Loader.forBundledModules(); + if (!loader) { + throw new Error('No Loader is available to this module'); + } + return loader; } + diff --git a/packages/base/markdown-file-def.gts b/packages/base/markdown-file-def.gts index 3b2ee786b1c..549ffb2966f 100644 --- a/packages/base/markdown-file-def.gts +++ b/packages/base/markdown-file-def.gts @@ -14,7 +14,7 @@ import GlimmerComponent from '@glimmer/component'; import { htmlSafe } from '@ember/template'; import { markdownToHtml } from '@cardstack/runtime-common/marked-sync'; import { - BaseDefComponent, + type BaseDefComponent, CardDef, Component, NumberField, diff --git a/packages/base/package.json b/packages/base/package.json index fce09db8dec..455097e31d6 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -15,6 +15,7 @@ "@types/lodash-es": "catalog:", "awesome-phonenumber": "catalog:", "concurrently": "catalog:", + "date-fns": "catalog:", "ember-cli-htmlbars": "^6.3.0", "ember-concurrency": "catalog:", "ember-css-url": "^1.0.0", @@ -29,7 +30,7 @@ "yaml": "catalog:" }, "peerDependencies": { - "ember-provide-consume-context": "^0.7.0", + "ember-provide-consume-context": "^0.8.0", "ember-source": "catalog:", "lodash-es": "catalog:" }, diff --git a/packages/base/skill-frontmatter-field.gts b/packages/base/skill-frontmatter-field.gts index 4bbc93cc293..f7300267102 100644 --- a/packages/base/skill-frontmatter-field.gts +++ b/packages/base/skill-frontmatter-field.gts @@ -3,7 +3,7 @@ import { codeRefWithAbsoluteIdentifier, getClass, rri, - type Loader, + Loader, type ResolvedCodeRef, type ToolContext, type ToolSchemaError, @@ -267,12 +267,18 @@ async function generateToolDefinitions( } function myLoader(): Loader { - // we know this code is always loaded by an instance of our Loader, which - // sets import.meta.loader. + // A Loader that evaluates this module injects `import.meta.loader`. When + // the module is compiled into the host bundle instead, the platform + // evaluates it and no loader is injected; the host publishes the loader + // bundled modules should use (see Loader.setForBundledModules). - // When type-checking realm-server, tsc sees this file and thinks it will be - // transpiled to CommonJS and so it complains about this line. But this file - // is always loaded through our loader and always has access to import.meta. + // When type-checking realm-server, tsc sees this file and thinks it will + // be transpiled to CommonJS and so it complains about import.meta. // @ts-ignore - return (import.meta as any).loader; + let loader = (import.meta as any).loader ?? Loader.forBundledModules(); + if (!loader) { + throw new Error('No Loader is available to this module'); + } + return loader; } + diff --git a/packages/base/spec.gts b/packages/base/spec.gts index 25463aacdfc..678e14d70cf 100644 --- a/packages/base/spec.gts +++ b/packages/base/spec.gts @@ -1118,12 +1118,18 @@ function getIcon(specType: string) { } function myLoader(): Loader { - // we know this code is always loaded by an instance of our Loader, which sets - // import.meta.loader. + // A Loader that evaluates this module injects `import.meta.loader`. When + // the module is compiled into the host bundle instead, the platform + // evaluates it and no loader is injected; the host publishes the loader + // bundled modules should use (see Loader.setForBundledModules). - // When type-checking realm-server, tsc sees this file and thinks - // it will be transpiled to CommonJS and so it complains about this line. But - // this file is always loaded through our loader and always has access to import.meta. + // When type-checking realm-server, tsc sees this file and thinks it will + // be transpiled to CommonJS and so it complains about import.meta. // @ts-ignore - return (import.meta as any).loader; + let loader = (import.meta as any).loader ?? Loader.forBundledModules(); + if (!loader) { + throw new Error('No Loader is available to this module'); + } + return loader; } + diff --git a/packages/base/text-file-def.gts b/packages/base/text-file-def.gts index f43ca680526..2b31a2c15cc 100644 --- a/packages/base/text-file-def.gts +++ b/packages/base/text-file-def.gts @@ -2,7 +2,7 @@ import { byteStreamToUint8Array } from '@cardstack/runtime-common'; import TextFileIcon from '@cardstack/boxel-icons/file-text'; import GlimmerComponent from '@glimmer/component'; import { - BaseDefComponent, + type BaseDefComponent, Component, NumberField, StringField, diff --git a/packages/base/ts-file-def.gts b/packages/base/ts-file-def.gts index 0ce19d7857c..ef42f2350e6 100644 --- a/packages/base/ts-file-def.gts +++ b/packages/base/ts-file-def.gts @@ -3,7 +3,7 @@ import { htmlSafe } from '@ember/template'; import FileCodeIcon from '@cardstack/boxel-icons/file-code'; import GlimmerComponent from '@glimmer/component'; import { - BaseDefComponent, + type BaseDefComponent, Component, NumberField, StringField, diff --git a/packages/base/workspace.gts b/packages/base/workspace.gts index 52e2f2d7934..40206fa64e3 100644 --- a/packages/base/workspace.gts +++ b/packages/base/workspace.gts @@ -38,15 +38,17 @@ import { codeRef, specRef, baseCardRef, + baseRealm, baseRealmRRI, isCardInstance, + Loader, SupportedMimeType, subscribeToRealm, codeRefFromInternalKey, type Query, type Filter, type CodeRef, - CardErrorJSONAPI, + type CardErrorJSONAPI, } from '@cardstack/runtime-common'; import CardsGridLayout, { @@ -75,12 +77,29 @@ import { MarkdownDef } from './markdown-file-def'; // realm README import type { RealmEventContent } from './matrix-event'; import { Spec } from './spec'; -// This file is always loaded through the Boxel loader, which supplies -// `import.meta`. When type-checking, tsc sees the file as CommonJS output and -// rejects the meta-property, so suppress it — the same pattern used elsewhere -// in packages/base. -// @ts-ignore -const here: string = (import.meta as any).url; +// This module's canonical URL, the base for sibling code refs below. +// `import.meta.url` can't provide it in every evaluation environment (a +// bundler reports the compiled chunk's URL, not the realm module's), so +// state it directly. +const here: string = new URL('./workspace', baseRealm.url).href; + +// A realm URL that reaches a card can be a virtual alias (e.g. +// `https://cardstack.com/base/`) that only the virtual network knows how to +// resolve; a native fetch of one leaves the page for a host that need not +// exist, failing with `TypeError: Failed to fetch`. The loader's fetch maps +// the alias to the URL the realm is really served from and carries realm +// auth, rather than relying on the auth service worker to inject it. A base +// module compiled into the host bundle is evaluated by the platform and so +// has no `import.meta.loader`; it uses the loader the host publishes for +// bundled modules instead. +function realmFetch(): typeof globalThis.fetch { + // When type-checking realm-server, tsc sees this file and thinks it will be + // transpiled to CommonJS and so it complains about import.meta. But this + // file always runs as ESM. + // @ts-ignore + let loader = (import.meta as any).loader ?? Loader.forBundledModules(); + return loader?.fetch ?? fetch; +} const [, StripView, GridView] = VIEW_OPTIONS; @@ -3336,7 +3355,7 @@ class Isolated extends Component { if (!realm) { return; } - let response = await fetch(`${realm}_types`, { + let response = await realmFetch()(`${realm}_types`, { headers: { Accept: SupportedMimeType.CardTypeSummary, }, diff --git a/packages/base/zip-file-def.gts b/packages/base/zip-file-def.gts index 45d8ba599ff..c6d965fdae0 100644 --- a/packages/base/zip-file-def.gts +++ b/packages/base/zip-file-def.gts @@ -5,7 +5,7 @@ import { htmlSafe } from '@ember/template'; import GlimmerComponent from '@glimmer/component'; import { - BaseDefComponent, + type BaseDefComponent, Component, FieldDef, NumberField, diff --git a/packages/host/app/lib/bundled-base-modules.d.ts b/packages/host/app/lib/bundled-base-modules.d.ts new file mode 100644 index 00000000000..54536dfaeed --- /dev/null +++ b/packages/host/app/lib/bundled-base-modules.d.ts @@ -0,0 +1,2 @@ +declare const BASE_MODULES: Record>; +export default BASE_MODULES; diff --git a/packages/host/app/lib/bundled-base-modules.js b/packages/host/app/lib/bundled-base-modules.js new file mode 100644 index 00000000000..35d8a4570bf --- /dev/null +++ b/packages/host/app/lib/bundled-base-modules.js @@ -0,0 +1,14 @@ +// The base realm's modules, compiled into the host bundle. Lives in an +// untyped .js module because `import.meta.glob` is a vite build-time +// construct that ember-tsc (module: nodenext, CJS-flavored app files) +// rejects; the .d.ts sibling carries the type. +const BASE_MODULES = import.meta.glob( + [ + '../../../base/**/*.{gts,ts}', + '!../../../base/node_modules/**', + '!../../../base/**/*.d.ts', + ], + { eager: true }, +); + +export default BASE_MODULES; diff --git a/packages/host/app/lib/bundled-base.ts b/packages/host/app/lib/bundled-base.ts new file mode 100644 index 00000000000..504fe4c7faa --- /dev/null +++ b/packages/host/app/lib/bundled-base.ts @@ -0,0 +1,86 @@ +// Bundles the @cardstack/base realm's modules into the host build and +// registers them as loader shims, so importing a base module (whether as +// `@cardstack/base/card-api` or its resolved base-realm URL) resolves to +// the compiled-in module instead of a network fetch of realm-server- +// transpiled source. This trades three properties of the fetched path for +// speed: +// +// - Base modules become singletons shared by every loader generation. +// A loader reset (test isolation, code-change flush) no longer +// re-evaluates base module state; whatever module-level state card-api +// and friends hold persists across resets. +// - The running base realm's *source* is no longer what executes in the +// host: editing base code in a realm (or reindexing it) has no effect +// until the host is rebuilt. This includes indexing — the prerender +// renders with the host dist, so index output reflects the bundled base, +// not the realm-served source. +// - An indexed instance's dependencies lose their transitive closure +// through base. The loader records `consumedModules` while evaluating +// fetched source; a shim carries no dependency chain, so a card's deps +// stop at the base modules it names directly and the base-internal and +// npm-package entries the fetched path records are absent. Three +// `Integration | realm indexing` tests assert the full closure and fail +// on that difference. Restoring it needs a *post-compile* dep map: +// the fetched closure includes deps the gts/babel transforms inject +// (`@ember/template-factory`, `@ember/component/template-only`, +// ember-concurrency's async-arrow runtime), which a scan of base's own +// import statements can't see. Whether the closure is worth restoring is +// a separate question — a bundled base module can't change without a +// host rebuild, so nothing in it can invalidate an index entry. +// +// The eager glob in bundled-base-modules.js compiles every base module +// into the host's initial bundle through the same vite/embroider pipeline +// as host app code. + +import type { VirtualNetwork } from '@cardstack/runtime-common'; + +import BASE_MODULES from './bundled-base-modules'; + +const GLOB_PREFIX = '../../../base/'; + +// Registers on the virtual network (not per loader) so that every loader +// sharing the network serves the bundled modules — including loaders +// constructed outside the loader service (test-realm adapters, in-browser +// indexing). Must run after the network's `@cardstack/base/` realm mapping +// is registered, since shim identifiers resolve at registration time. +// +// Known gap: only the RRI-resolved identifier form is registered. An +// import that names a base module by its canonical +// `https://cardstack.com/base/` URL misses the shim (resolveImport passes +// URL-form identifiers through unchanged; URL→URL mapping happens at the +// network's fetch boundary, which shim lookup precedes) and falls through +// to a network fetch that evaluates a second copy of the module. +// Registering the canonical form as a second shim entry is NOT the fix: +// loaders capture export identities under whichever identifier form they +// fetched, so dual registration makes a def's identified module URL +// depend on import order. The durable fix is normalizing the identifier +// through the network's URL mappings in the loader's module-fetch path so +// both forms converge on one module state. +// Shim registration order doubles as identity-capture order (loaders +// replay the network's shim inventory dependency-first — see the loader's +// captureVirtualNetworkShimIdentities). The def classes that serialization +// identifies are declared in card-api (several modules re-export them: +// file-api, markdown, image-file-def) and cards-grid (re-exported by +// index), so those two register ahead of the alphabetical remainder. +const DECLARING_MODULES_FIRST = ['card-api', 'cards-grid']; + +export function shimBundledBase(virtualNetwork: VirtualNetwork) { + let entries = Object.entries(BASE_MODULES) + .map(([path, module]) => ({ + name: path.slice(GLOB_PREFIX.length).replace(/\.(gts|ts)$/, ''), + module, + })) + .sort((a, b) => { + let ai = DECLARING_MODULES_FIRST.indexOf(a.name); + let bi = DECLARING_MODULES_FIRST.indexOf(b.name); + if (ai !== bi) { + return (ai === -1 ? Infinity : ai) < (bi === -1 ? Infinity : bi) + ? -1 + : 1; + } + return a.name < b.name ? -1 : 1; + }); + for (let { name, module } of entries) { + virtualNetwork.shimModule(`@cardstack/base/${name}`, module); + } +} diff --git a/packages/host/app/lib/sqlite-adapter.ts b/packages/host/app/lib/sqlite-adapter.ts index a335780527b..3fce2e598bb 100644 --- a/packages/host/app/lib/sqlite-adapter.ts +++ b/packages/host/app/lib/sqlite-adapter.ts @@ -13,6 +13,18 @@ import { Deferred, } from '@cardstack/runtime-common'; +// Names the pending operation in a stuck-waiter dump. A query the worker +// never answers otherwise reports as a bare `sqlite running`, which says +// nothing about which statement (or which of several concurrent queries) +// stalled — and since these run behind an in-page realm's request handler, +// a stall here surfaces as a test timing out on unrelated-looking fetches. +function sqliteWaiterLabel(args: unknown[]): string { + let [command, payload] = args as [unknown, { sql?: unknown } | undefined]; + let label = `sqlite running ${String(command)}`; + let sql = typeof payload?.sql === 'string' ? payload.sql : undefined; + return sql ? `${label} ${sql.replace(/\s+/g, ' ').slice(0, 160)}` : label; +} + export default class SQLiteAdapter implements DBAdapter { readonly kind = 'sqlite'; private _sqlite: typeof SQLiteWorker | undefined; @@ -122,7 +134,7 @@ export default class SQLiteAdapter implements DBAdapter { ); } return (async (...args: Parameters) => { - return await waitForPromise(worker(...args), 'sqlite running'); + return await waitForPromise(worker(...args), sqliteWaiterLabel(args)); }) as typeof SQLiteWorker; } diff --git a/packages/host/app/services/loader-service.ts b/packages/host/app/services/loader-service.ts index a307ce6c8e5..90034bfcfaa 100644 --- a/packages/host/app/services/loader-service.ts +++ b/packages/host/app/services/loader-service.ts @@ -76,7 +76,9 @@ export default class LoaderService extends Service { log.debug(`resetting loader for session boundary (${reason ?? ''})`); this.clearSessionCaches(); let previous = this.loader; - this.loader = previous ? Loader.cloneLoader(previous) : this.makeInstance(); + this.loader = previous + ? this.trackCurrent(Loader.cloneLoader(previous)) + : this.makeInstance(); previous?.dispose(); } @@ -145,7 +147,7 @@ export default class LoaderService extends Service { let previous = this.loader; this.recordLoaderReplacement(previous, options?.codeChange); if (previous) { - this.loader = Loader.cloneLoader(previous); + this.loader = this.trackCurrent(Loader.cloneLoader(previous)); previous.dispose(); } else { this.loader = this.makeInstance(); @@ -196,6 +198,15 @@ export default class LoaderService extends Service { ), virtualNetwork: this.network.virtualNetwork, }); + return this.trackCurrent(loader); + } + + // Bundled base modules can't discover their loader via + // `import.meta.loader` (the platform evaluated them, not a Loader), so + // every loader that becomes this service's active one is also published + // as the loader bundled modules fall back to. + private trackCurrent(loader: Loader): Loader { + Loader.setForBundledModules(loader); return loader; } diff --git a/packages/host/app/services/network.ts b/packages/host/app/services/network.ts index 316c7c7f8fe..93253859aee 100644 --- a/packages/host/app/services/network.ts +++ b/packages/host/app/services/network.ts @@ -13,6 +13,7 @@ import { import config from '@cardstack/host/config/environment'; +import { shimBundledBase } from '../lib/bundled-base'; import { shimExternals } from '../lib/externals'; import { authErrorEventMiddleware } from '../utils/auth-error-guard'; import { scheduleNativeTimeout } from '../utils/render-timer-stub'; @@ -82,7 +83,14 @@ export default class NetworkService extends Service { '@cardstack/base/', resolvedBaseRealmURL.href, ); + // Externals shim first: identity capture replays shims in registration + // order, and base modules re-export values whose declaring modules are + // externals (runtime-common, boxel-ui) — declaring modules must + // register ahead of their re-exporters. shimExternals(virtualNetwork); + // Base-realm modules ship inside the host bundle; any loader on this + // network serves them without fetching from the base realm. + shimBundledBase(virtualNetwork); virtualNetwork.addImportMap('@cardstack/boxel-icons/', (rest) => { return `${config.iconsURL}/@cardstack/boxel-icons/v1/icons/${rest}.js`; }); diff --git a/packages/host/tests/helpers/setup-qunit.js b/packages/host/tests/helpers/setup-qunit.js index fd0a8eaf455..2d2b6b8c4ac 100644 --- a/packages/host/tests/helpers/setup-qunit.js +++ b/packages/host/tests/helpers/setup-qunit.js @@ -22,9 +22,26 @@ export function setupQUnit() { ) { return true; } + // QUnit reports a global error as message + the asset line it surfaced + // at, which for a bundled chunk names neither the failing request nor + // the call site. The stack lands in the failure's browser log instead. + let error = args[3]; + if (error && error.stack) { + console.error(`[global-error] ${error.stack}`); + } return _originalOnError ? _originalOnError(message, ...args) : false; }; + // Same reasoning for a rejection that reaches the window: the reported + // message alone doesn't identify what rejected. Listening (without + // preventDefault) only logs — QUnit still fails the test. + window.addEventListener('unhandledrejection', (event) => { + let reason = event.reason; + console.error( + `[global-rejection] ${(reason && (reason.stack || reason.message)) || String(reason)}`, + ); + }); + QUnit.dump.maxDepth = 20; useTestWaiters(TestWaiters); setup(QUnit.assert); diff --git a/packages/host/vite.config.mjs b/packages/host/vite.config.mjs index e7ce9f4d327..becccf9d709 100644 --- a/packages/host/vite.config.mjs +++ b/packages/host/vite.config.mjs @@ -236,6 +236,11 @@ export default defineConfig(({ mode }) => ({ build: { minify: false, rolldownOptions: { + // Bundled base-realm modules may import directly from an https URL + // (e.g. currency.gts's esm.run import). Leave those imports verbatim + // in the output; the browser fetches them at chunk load, matching how + // the loader-served module behaves. + external: [/^https:\/\//], output: { keepNames: true, ...(mode === 'production' ? { minify: true } : {}), @@ -269,6 +274,15 @@ export default defineConfig(({ mode }) => ({ }, resolve: { alias: [ + // Base-realm modules (bundled via app/lib/bundled-base.ts) import + // host tools as `@cardstack/boxel-host/tools/*` (and the pre-rename + // `commands/*` spelling). At runtime the virtual network shims those + // specifiers to app/tools modules (see app/tools/index.ts); this + // alias gives the bundler the same 1:1 mapping. + { + find: /^@cardstack\/boxel-host\/(?:tools|commands)\//, + replacement: `${__dirname}/app/tools/`, + }, { find: 'path', replacement: require.resolve('path-browserify') }, { find: 'stream', replacement: require.resolve('stream-browserify') }, { find: /^util$/, replacement: require.resolve('util/') }, diff --git a/packages/runtime-common/fetcher.ts b/packages/runtime-common/fetcher.ts index 8f07de69902..e2cf85915f7 100644 --- a/packages/runtime-common/fetcher.ts +++ b/packages/runtime-common/fetcher.ts @@ -45,7 +45,12 @@ export function fetcher( ? urlOrRequest : new Request(urlOrRequest, init); - let token = fetcherWaiter.beginAsync(); + // Labeled so a test that times out on this waiter names the request it + // was waiting for. Requests answered in-page (a test realm, a + // service-worker relay) never reach the global fetch, so the harness' + // in-flight-fetch list can be empty while this waiter is still open — + // the label is then the only record of what stalled. + let token = fetcherWaiter.beginAsync(`${request.method} ${request.url}`); try { return responseWithWaiters(await buildNext(middlewareStack)(request)); } finally { @@ -100,7 +105,11 @@ function responseWithWaiters(response: Response): Response { } if (typeof key === 'string' && asyncMethods.includes(key)) { return async (...args: unknown[]) => { - return waitForPromise(value(...args), `fetcher-body:${key}`); + let url = Reflect.get(target, 'url'); + return waitForPromise( + value(...args), + url ? `fetcher-body:${key} ${url}` : `fetcher-body:${key}`, + ); }; } return value; diff --git a/packages/runtime-common/loader.ts b/packages/runtime-common/loader.ts index 1d186ed9c1c..0b88835299e 100644 --- a/packages/runtime-common/loader.ts +++ b/packages/runtime-common/loader.ts @@ -234,8 +234,36 @@ export class Loader { }, ) { this.fetchImplementation = fetch; - this.resolveImport = - resolveImport ?? ((moduleIdentifier) => moduleIdentifier); + let rawResolveImport = + resolveImport ?? ((moduleIdentifier: string) => moduleIdentifier); + let virtualNetwork = options?.virtualNetwork; + // Fold virtual-alias URL forms (e.g. https://cardstack.com/base/…) onto + // the real URL the network would serve them from, so both spellings of a + // module converge on one module-state entry, one shim lookup key, and one + // captured export identity. `resolveImport` itself only rewrites RRI / + // bare-package prefixes and passes full URLs through, so without this a + // virtual-alias import keys its own separate module state — and card + // serialization requires identities in real-URL form (module refs + // relativize against instance ids, which are real-form). Mapping an + // already-real URL is a no-op, so composed resolvers (cloneLoader wraps + // the parent's) stay idempotent. + this.resolveImport = virtualNetwork + ? (moduleIdentifier: string) => { + let resolved = rawResolveImport(moduleIdentifier); + // mapURL constructs a URL, so only URL-shaped identifiers can be + // folded; anything else (an unmapped prefix form, a relative + // specifier) passes through untouched. + if ( + !resolved.startsWith('http://') && + !resolved.startsWith('https://') + ) { + return resolved; + } + return ( + virtualNetwork.mapURL(resolved, 'virtual-to-real')?.href ?? resolved + ); + } + : rawResolveImport; this.retrySleep = options?.retrySleep; this.virtualNetwork = options?.virtualNetwork; // Module caches are keyed by canonical RRI form (see moduleCacheKey), whose @@ -421,6 +449,24 @@ export class Loader { return undefined; } + // Realm modules that a Loader evaluates discover their loader via + // `import.meta.loader`, which the Loader injects at eval time. Modules + // compiled into the host bundle instead (and registered as loader shims — + // see the host's bundled-base registration) are evaluated by the + // platform's module system, where `import.meta.loader` does not exist. + // The host publishes its active loader here so bundled modules can fall + // back to it; module code reads this only when `import.meta.loader` is + // absent. + static #forBundledModules: Loader | undefined; + + static setForBundledModules(loader: Loader) { + Loader.#forBundledModules = loader; + } + + static forBundledModules(): Loader | undefined { + return Loader.#forBundledModules; + } + async import( moduleIdentifier: string, dependencyTrackingContext?: RuntimeDependencyTrackingContext, @@ -863,9 +909,16 @@ export class Loader { init?: RequestInit, ): Promise => { try { - let shimmedModule = this.moduleShims.get( - this.asRequest(urlOrRequest, init).url, - ); + let shimmedModule = + this.moduleShims.get(this.asRequest(urlOrRequest, init).url) ?? + // Modules shimmed on the virtual network (e.g. base modules + // compiled into the host bundle) are served to every loader + // sharing that network, including loaders constructed outside the + // host's loader service. This is a module-fetch path, so a shim + // registered under a realm URL can't shadow a card-instance GET. + (await this.virtualNetwork?.getShimmedModule( + this.asRequest(urlOrRequest, init).url, + )); if (shimmedModule) { let response = new Response(); (response as any)[Symbol.for('shimmed-module')] = shimmedModule; @@ -960,6 +1013,18 @@ export class Loader { } } + private vnShimIdentitiesCaptured = false; + + private captureVirtualNetworkShimIdentities() { + if (this.vnShimIdentitiesCaptured || !this.virtualNetwork) { + return; + } + this.vnShimIdentitiesCaptured = true; + for (let [id, module] of this.virtualNetwork.syncShimEntries()) { + this.captureIdentitiesOfModuleExports(module, id); + } + } + private captureIdentitiesOfModuleExports( module: any, moduleIdentifier: string, @@ -1047,6 +1112,15 @@ export class Loader { this.setCanonicalModuleURL(moduleIdentifier, canonicalURL); if (loaded.type === 'shimmed') { + // Loader-evaluated modules capture export identities dependency-first + // (a re-exporting module always evaluates after the module that + // declares the export, so first-wins capture lands on the declaring + // module). Shims carry no dependency chain, so a loader whose first + // shim load is a re-exporter would mis-attribute identities. Replay + // the network's whole sync-shim inventory once, in registration + // order (registrars put declaring modules first), before any + // individual shim's capture. + this.captureVirtualNetworkShimIdentities(); this.captureIdentitiesOfModuleExports(loaded.module, moduleIdentifier); this.setModule(moduleIdentifier, { diff --git a/packages/runtime-common/package-shim-handler.ts b/packages/runtime-common/package-shim-handler.ts index 2cccd4c7707..95b58a2fd10 100644 --- a/packages/runtime-common/package-shim-handler.ts +++ b/packages/runtime-common/package-shim-handler.ts @@ -440,10 +440,25 @@ export class PackageShimHandler { return null; }; + // Synchronously-shimmed modules in registration order. Loaders replay + // this inventory through their identity capture so that export + // identities don't depend on which shim a given loader happened to load + // first — loader-evaluated modules got that guarantee from + // dependency-first evaluation (a re-exporting module always evaluated + // after its source), but shims carry no dependency chain. Registration + // order therefore stands in for dependency order; registrars put + // declaring modules before their re-exporters. + private syncModules = new Map(); + + syncShimEntries(): ReadonlyMap { + return this.syncModules; + } + shimModule(moduleIdentifier: string, module: ModuleLike) { moduleIdentifier = this.resolveImport(moduleIdentifier); let key = trimModuleIdentifier(moduleIdentifier); this.moduleIds.set(key, async () => module); + this.syncModules.set(key, module); this.rememberExports(key, module); } @@ -506,6 +521,16 @@ export class PackageShimHandler { } } + // Module lookup for callers outside the fetch pipeline (the Loader's + // module-fetch path asks the virtual network for shims registered here + // before going to the network). Unlike `handle`, this is not restricted + // to the fake packages origin: the caller vouches that the URL is a + // module request, so a shim registered under a realm URL can be served + // without risking shadowing a card-instance GET of the same URL. + async lookupModule(url: string): Promise { + return (await this.getModule(url)) ?? (await this.getModuleByPrefix(url)); + } + private async getModule(url: string): Promise { let key = trimModuleIdentifier(url); let resolver = this.moduleIds.get(key); diff --git a/packages/runtime-common/test-waiters.ts b/packages/runtime-common/test-waiters.ts index 09728bade9f..c287de013d9 100644 --- a/packages/runtime-common/test-waiters.ts +++ b/packages/runtime-common/test-waiters.ts @@ -6,7 +6,9 @@ export interface Waiters { buildWaiter(label: string): { - beginAsync(): unknown; + // `@ember/test-waiters`' signature: an explicit token, then a label that + // its `debugInfo()` reports in place of a captured stack. + beginAsync(token?: unknown, label?: string): unknown; endAsync(token: unknown): void; }; waitForPromise(promise: Promise, label?: string): Promise; @@ -19,7 +21,9 @@ export function useTestWaiters(w: Waiters) { } export interface TestWaiter { - beginAsync(): unknown; + // A label names the pending operation in a stuck-waiter dump (a test that + // times out prints each open token's label, falling back to its stack). + beginAsync(label?: string): unknown; endAsync(token: unknown): void; } @@ -36,8 +40,8 @@ export function buildWaiter(label: string): TestWaiter { return real; }; return { - beginAsync() { - return resolve()?.beginAsync(); + beginAsync(label?: string) { + return resolve()?.beginAsync(undefined, label); }, endAsync(token: unknown) { if (token === undefined) { diff --git a/packages/runtime-common/virtual-network.ts b/packages/runtime-common/virtual-network.ts index b8824163c9d..9a6d62c25ac 100644 --- a/packages/runtime-common/virtual-network.ts +++ b/packages/runtime-common/virtual-network.ts @@ -126,6 +126,22 @@ export class VirtualNetwork { this.packageShimHandler.shimAsyncModule(descriptor); } + // Lets a Loader serve a module shimmed on this network (under any URL, + // not just the fake packages origin) without a network fetch. Only the + // module-fetch path may call this: shims can be registered under realm + // URLs that also serve card instances, and only the caller knows the + // request is for a module rather than an instance document. + getShimmedModule(url: string): Promise { + return this.packageShimHandler.lookupModule(url); + } + + // Registration-ordered inventory of synchronously-shimmed modules, for + // loaders to replay through identity capture (see the note on the + // handler's syncShimEntries). + syncShimEntries(): ReadonlyMap { + return this.packageShimHandler.syncShimEntries(); + } + addURLMapping(from: URL, to: URL) { this.urlMappings.push([from.href, to.href]); // unresolveURL and toRealURLHref chase through urlMappings (the latter via diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc3126ea939..c99b0666b48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -886,8 +886,8 @@ importers: packages/base: dependencies: ember-provide-consume-context: - specifier: ^0.7.0 - version: 0.7.1(@ember/test-helpers@5.4.3(@babel/core@7.29.7))(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5)) + specifier: ^0.8.0 + version: 0.8.0(@ember/test-helpers@5.4.3(@babel/core@7.29.7))(@glimmer/component@2.1.1)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5)) ember-source: specifier: 'catalog:' version: 6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5) @@ -931,6 +931,9 @@ importers: concurrently: specifier: 'catalog:' version: 8.2.2 + date-fns: + specifier: 'catalog:' + version: 2.30.0 ember-cli-htmlbars: specifier: ^6.3.0 version: 6.3.0