diff --git a/CHANGELOG.md b/CHANGELOG.md index 49fca617..cc00b59f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- **@studiometa/ui-mapbox:** autoload map children declared on a `hidden` element ([#655](https://github.com/studiometa/ui/pull/655)) + ## [v2.0.0-alpha.0](https://github.com/studiometa/ui/compare/1.11.1..2.0.0-alpha.0) (2026-09-03) This is the first release of the v2 line. It moves every package onto [`@studiometa/js-toolkit` v4](https://js-toolkit-v4.studiometa.dev/), removes six component families that a newer component already covers, renames three components, merges `LargeText` and `CircularMarquee` into `Marquee`, rewrites `Tabs` on the WAI-ARIA Tabs pattern, redesigns `Cursor` around published CSS hooks, completes the `Carousel` family and gives it an accessibility contract, and changes the shape of every event payload. There is no compatibility layer. diff --git a/packages/docs/guide/autoloading/index.md b/packages/docs/guide/autoloading/index.md index d6264d6d..afceaf94 100644 --- a/packages/docs/guide/autoloading/index.md +++ b/packages/docs/guide/autoloading/index.md @@ -69,14 +69,14 @@ Pin an exact version in production. An exact-version URL is immutable and stays Each manifest entry carries a default mount strategy. Override it per element with `data-mount`. There are six: -| Strategy | The component mounts when… | -| --------------- | ------------------------------------------------------------------------- | -| `eager` | the runtime starts. The default for every `@studiometa/ui` component. | -| `visible` | the element crosses into the viewport, once. | -| `in-view` | the element is in the viewport, and unmounts when it leaves. Reversible. | -| `idle` | the browser is idle. | -| `interaction` | the first `pointerenter`, `pointerdown` or `focusin` on the element. | -| `media:` | the media query matches, and unmounts when it stops matching. Reversible. | +| Strategy | The component mounts when… | +| --------------- | ------------------------------------------------------------------------------------ | +| `eager` | the element exists. No other condition. See [What `eager` means](#what-eager-means). | +| `visible` | the element crosses into the viewport, once. | +| `in-view` | the element is in the viewport, and unmounts when it leaves. Reversible. | +| `idle` | the browser is idle. | +| `interaction` | the first `pointerenter`, `pointerdown` or `focusin` on the element. | +| `media:` | the media query matches, and unmounts when it stops matching. Reversible. | ```html
Mounts on hover, touch, or focus
@@ -86,7 +86,19 @@ Each manifest entry carries a default mount strategy. Override it per element wi The element's `data-mount` always wins over the manifest default. An invalid value reports a `component.invalid-mount-strategy` diagnostic and the entry's default is used. -The `@studiometa/ui` manifest ships every component as `eager`; `@studiometa/ui-mapbox` and `@studiometa/ui-motion` ship theirs as `visible`, so their heavy dependencies stay off the critical path. +### What `eager` means + +`eager` reads like "load at page load". It is not that. The runtime never walks the manifest: it reads the `data-component` tokens declared on the elements of the page, and consults a manifest entry only for a token it finds there. Nothing loads for a token absent from the markup. + +`eager` means the entry has no condition left to wait for beyond that element existing — no viewport crossing, no media query, no interaction. The import then runs on a background scheduler task, off the frame that discovered the element. Every element declaring the same token shares one import. + +Use it for an element that renders nothing. An element carrying `hidden` is never rendered, so it never intersects the viewport and never receives a pointer or focus event: `visible`, `in-view` and `interaction` wait on it for a signal that cannot arrive. + +### Package defaults + +The `@studiometa/ui` manifest ships every component as `eager`. `@studiometa/ui-motion` ships every component as `visible`, so the Motion library stays off the critical path. + +`@studiometa/ui-mapbox` splits. `MapboxMap` and `StoreLocator` render, so they are `visible` and the heavy `mapbox-gl` import waits for a map to approach the viewport. The twelve map children — clusters, cluster items, controls, the geocoder, images, layers, markers, popups and sources — configure the map from markup that renders nothing and is marked `hidden`, so they are `eager`. Their own modules are small; `mapbox-gl` still waits for `MapboxMap`. ## Component discovery @@ -160,7 +172,7 @@ Declare an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Elemen ``` -Load the Mapbox stylesheet yourself, and the geocoder stylesheet only when you use `MapboxGeocoder`. Give each map a valid access token through its `data-option-access-token`, and any other Mapbox `Map` option through `data-option-map-options`. Mapbox components default to the `visible` mount strategy, so the map code loads when a map crosses into the viewport. +Load the Mapbox stylesheet yourself, and the geocoder stylesheet only when you use `MapboxGeocoder`. Give each map a valid access token through its `data-option-access-token`, and any other Mapbox `Map` option through `data-option-map-options`. `MapboxMap` and `StoreLocator` default to the `visible` mount strategy, so the map code loads when a map crosses into the viewport. The map children default to `eager`, because their markup renders nothing and carries `hidden` — see [Package defaults](#package-defaults). You own the `mapbox-gl` module, so its Web Worker is same-origin and a strict Content Security Policy works. When you load `mapbox-gl` from a CDN that builds its worker from a `blob:` URL, allow it: `Content-Security-Policy: worker-src blob:;`. diff --git a/packages/docs/migration-guides/vue-mapbox-gl/index.md b/packages/docs/migration-guides/vue-mapbox-gl/index.md index 576ef55f..5231cbcb 100644 --- a/packages/docs/migration-guides/vue-mapbox-gl/index.md +++ b/packages/docs/migration-guides/vue-mapbox-gl/index.md @@ -44,16 +44,18 @@ registerManifest({ load: () => import('@studiometa/ui-mapbox/MapboxMap'), }, MapboxMarker: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('@studiometa/ui-mapbox/MapboxMarker'), }, MapboxPopup: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('@studiometa/ui-mapbox/MapboxPopup'), }, }); ``` +The map renders, so `visible` keeps `mapbox-gl` off the critical path. The children are wrapped in a `hidden` element, which never intersects the viewport, so they take `eager` — see [Package defaults](/guide/autoloading/#package-defaults). + ## Component mapping Most Vue components have a same-named js-toolkit equivalent, which you author as `data-component` elements nested inside a `MapboxMap` instead of Vue templates. The clustering and store-locator components are the exception — they were re-architected around a new `MapboxClusterItem`, detailed below the table. diff --git a/packages/docs/reference/items/MapboxMap/index.md b/packages/docs/reference/items/MapboxMap/index.md index 8129bc37..d159c1e1 100644 --- a/packages/docs/reference/items/MapboxMap/index.md +++ b/packages/docs/reference/items/MapboxMap/index.md @@ -95,14 +95,16 @@ registerManifest({ load: () => import('@studiometa/ui-mapbox/MapboxMap'), }, MapboxMarker: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('@studiometa/ui-mapbox/MapboxMarker'), }, MapboxPopup: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('@studiometa/ui-mapbox/MapboxPopup'), }, }); ``` +The map renders, so `visible` holds the `mapbox-gl` import back until a map approaches the viewport. A marker or a popup renders nothing and its element carries `hidden`, so it never intersects the viewport: give it `eager`, which waits for nothing beyond the element being on the page. This is what the package manifest already does — see [Package defaults](/guide/autoloading/#package-defaults). + Reach for a different strategy when it fits better — `idle`, `interaction`, `media:` — and override any of them per element with `data-mount`. The [Autoloading](/guide/autoloading/) guide lists all six. diff --git a/packages/tests/autoload/hidden-elements.spec.ts b/packages/tests/autoload/hidden-elements.spec.ts new file mode 100644 index 00000000..03192867 --- /dev/null +++ b/packages/tests/autoload/hidden-elements.spec.ts @@ -0,0 +1,78 @@ +import { Base, getInstance, registerComponent, registerManifest } from '@studiometa/js-toolkit'; +import { mount, settle, waitFor } from '@studiometa/js-toolkit/test'; +import { MapboxMap } from '@studiometa/ui-mapbox'; +import { manifest as mapboxManifest } from '@studiometa/ui-mapbox/manifest'; +import { describe, expect, it } from 'vitest'; +import { mountMap } from '../MapboxMap/harness.js'; + +/** + * The `hidden` attribute is the pattern `@studiometa/ui-mapbox` documents for + * the map children that render nothing. A `hidden` element is never rendered, + * so it never intersects the viewport: a `visible` manifest entry on it waits + * for a signal that can never arrive, and the dynamic `import()` never runs. + * + * The map children therefore declare `eager`, which has no condition left to + * wait for beyond the element existing. + */ + +// `MapboxMap` renders and is the ancestor every child resolves through, so it +// is registered as a class here: the test is about the children's entries, and +// a `visible` root would make the whole map wait on the viewport. +const { MapboxMap: _root, StoreLocator: _storeLocator, ...childEntries } = mapboxManifest; + +registerComponent(MapboxMap); +registerManifest(childEntries); + +/** A component whose only job is to record that its lazy entry was imported. */ +class HiddenProbe extends Base { + static config = { name: 'HiddenProbe' }; +} + +let probeImports = 0; +registerManifest({ + HiddenProbe: { + mountStrategy: 'visible', + load() { + probeImports += 1; + return HiddenProbe; + }, + }, +}); + +describe('autoloading an element carrying the hidden attribute', () => { + it('loads and mounts an eager map child', async () => { + const { mapEl } = await mountMap(` + + `); + const el = mapEl.querySelector('[data-component="MapboxNavigationControl"]')!; + + const instance = await waitFor(() => getInstance(el, 'MapboxNavigationControl')); + + expect(instance.$isMounted).toBe(true); + }); + + it('loads and mounts every hidden child of one map', async () => { + const { mapEl } = await mountMap(` + + + + `); + + for (const token of ['MapboxFullscreenControl', 'MapboxGeolocateControl', 'MapboxMarker']) { + const el = mapEl.querySelector(`[data-component="${token}"]`)!; + const instance = await waitFor(() => getInstance(el, token)); + + expect(instance.$isMounted).toBe(true); + } + }); + + it('never loads a visible entry, because a hidden element cannot intersect', async () => { + const root = await mount(` + + `); + await settle(); + + expect(probeImports).toBe(0); + expect(getInstance(root.firstElementChild as HTMLElement, 'HiddenProbe')).toBeUndefined(); + }); +}); diff --git a/packages/tests/autoload/manifest.spec.ts b/packages/tests/autoload/manifest.spec.ts index b54311ff..bd5655c8 100644 --- a/packages/tests/autoload/manifest.spec.ts +++ b/packages/tests/autoload/manifest.spec.ts @@ -1,4 +1,10 @@ -import { Base, type BaseConstructor, type ComponentManifestEntry } from '@studiometa/js-toolkit'; +import { + Base, + type BaseConstructor, + type ComponentManifest, + type ComponentManifestEntry, + type MountStrategy, +} from '@studiometa/js-toolkit'; import { manifest as uiManifest } from '@studiometa/ui/manifest'; import { manifest as mapboxManifest } from '@studiometa/ui-mapbox/manifest'; import { manifest as motionManifest } from '@studiometa/ui-motion/manifest'; @@ -11,21 +17,65 @@ function isBaseConstructor(value: unknown): value is BaseConstructor { return typeof value === 'function' && value.prototype instanceof Base; } +/** + * The two `@studiometa/ui-mapbox` components that render. They keep the package + * default `visible`; every other entry configures the map from markup that + * renders nothing and is therefore `eager`. + */ +const MAPBOX_VISIBLE_TOKENS: readonly string[] = ['MapboxMap', 'StoreLocator']; + +const MAPBOX_EAGER_TOKENS: readonly string[] = [ + 'MapboxCluster', + 'MapboxClusterItem', + 'MapboxFullscreenControl', + 'MapboxGeocoder', + 'MapboxGeolocateControl', + 'MapboxImage', + 'MapboxImages', + 'MapboxLayer', + 'MapboxMarker', + 'MapboxNavigationControl', + 'MapboxPopup', + 'MapboxSource', +]; + +type ManifestCase = readonly [ + packageName: string, + manifest: ComponentManifest, + exports: Record, + strategyFor: (token: string) => MountStrategy, +]; + +// A package declares one default strategy, and a component overrides it when +// that default cannot answer for it — so the expectation is per token, not per +// package. +const cases: readonly ManifestCase[] = [ + ['@studiometa/ui', uiManifest, uiExports as Record, () => 'eager'], + [ + '@studiometa/ui-mapbox', + mapboxManifest, + mapboxExports as Record, + (token) => (MAPBOX_VISIBLE_TOKENS.includes(token) ? 'visible' : 'eager'), + ], + [ + '@studiometa/ui-motion', + motionManifest, + motionExports as Record, + () => 'visible', + ], +]; + // A generated manifest entry holds only what the registry reads before the // module is loaded: `{ load, mountStrategy }`. Everything else stays in the // authoring catalog the generator reads from. The token is not restated inside // the entry — it *is* the key — which is why the assertions below read the key // and then check that the loaded class agrees with it. -describe.each([ - ['@studiometa/ui', uiManifest, uiExports as Record, 'eager'], - ['@studiometa/ui-mapbox', mapboxManifest, mapboxExports as Record, 'visible'], - ['@studiometa/ui-motion', motionManifest, motionExports as Record, 'visible'], -] as const)('%s ./manifest export', (_packageName, manifest, exports, strategy) => { - it('declares the package mount strategy on every entry', () => { +describe.each(cases)('%s ./manifest export', (_packageName, manifest, exports, strategyFor) => { + it('declares the expected mount strategy on every entry', () => { expect(Object.keys(manifest).length).toBeGreaterThan(0); - for (const entry of Object.values(manifest) as ComponentManifestEntry[]) { - expect(entry.mountStrategy).toBe(strategy); + for (const [token, entry] of Object.entries(manifest) as [string, ComponentManifestEntry][]) { + expect(entry.mountStrategy).toBe(strategyFor(token)); } }); @@ -43,3 +93,20 @@ describe.each([ } }); }); + +// Listing both halves keeps a new component from inheriting a strategy by +// accident: adding one to the catalog fails here until it is classified. +describe('@studiometa/ui-mapbox mount strategies', () => { + it('splits every entry between the rendered roots and the declarative children', () => { + const eager = Object.entries(mapboxManifest) + .filter(([, entry]) => (entry as ComponentManifestEntry).mountStrategy === 'eager') + .map(([token]) => token); + const visible = Object.entries(mapboxManifest) + .filter(([, entry]) => (entry as ComponentManifestEntry).mountStrategy === 'visible') + .map(([token]) => token); + + expect(visible).toEqual([...MAPBOX_VISIBLE_TOKENS]); + expect(eager).toEqual([...MAPBOX_EAGER_TOKENS]); + expect(eager.length + visible.length).toBe(Object.keys(mapboxManifest).length); + }); +}); diff --git a/packages/ui-mapbox/README.md b/packages/ui-mapbox/README.md index a8a9430f..fd5e7229 100644 --- a/packages/ui-mapbox/README.md +++ b/packages/ui-mapbox/README.md @@ -21,20 +21,23 @@ npm install @mapbox/mapbox-gl-geocoder ## Usage -Register each component your page uses: each one registers independently and resolves its parent map on its own. `mapbox-gl` is heavy (~230 kB gzipped), so the recommended default is to lazy-register each component with js-toolkit's `importWhen*` helpers and the per-component subpaths (each subpath's default export is the component class), keeping the dependency out of your main bundle until a map is actually on the page: +Register each component your page uses: each one registers independently and resolves its parent map on its own. ```js -import { registerComponents, importWhenVisible } from '@studiometa/js-toolkit'; +import { registerComponents } from '@studiometa/js-toolkit'; +import { MapboxMap, MapboxMarker, MapboxPopup } from '@studiometa/ui-mapbox'; // Register only the components your page uses; order doesn't matter. -registerComponents( - importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxMap'), 'MapboxMap'), - importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxMarker'), 'MapboxMarker'), - importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxPopup'), 'MapboxPopup'), -); +registerComponents(MapboxMap, MapboxMarker, MapboxPopup); ``` -Other triggers are available too — `importWhenIdle`, `importOnInteraction` and `importOnMediaQuery` — see the [`importWhen*` helper docs](https://js-toolkit.studiometa.dev/api/helpers/importWhenVisible.html). Then author the map declaratively in your markup: +`mapbox-gl` is heavy (~230 kB gzipped), so import the autoload entry instead to keep it out of your main bundle. It registers a lazy entry for every component of the package, and imports a module only when an element on the page declares its token: + +```js +import '@studiometa/ui-mapbox/autoload'; +``` + +Then author the map declaratively in your markup: ```html
``` -If you do not need code-splitting, register eagerly instead — each component is exported by name from the package: - -```js -import { registerComponent } from '@studiometa/js-toolkit'; -import { MapboxMap } from '@studiometa/ui-mapbox'; - -registerComponent(MapboxMap); -``` +`MapboxMarker` renders nothing of its own — it configures the map from its attributes — so its element carries `hidden`. Every map child works that way, and the autoload manifest gives them the `eager` mount strategy for that reason: a `hidden` element is never rendered and never intersects the viewport, so a strategy waiting for a viewport crossing would never load them. `MapboxMap` and `StoreLocator` do render and keep `visible`, which is what holds the `mapbox-gl` import back until a map approaches the viewport. Override any of it per element with `data-mount`. Do not forget to include the `mapbox-gl` stylesheet so the map renders correctly. diff --git a/packages/ui-mapbox/src/catalog.ts b/packages/ui-mapbox/src/catalog.ts index f95262a5..f19bf343 100644 --- a/packages/ui-mapbox/src/catalog.ts +++ b/packages/ui-mapbox/src/catalog.ts @@ -1,22 +1,33 @@ -import type { ComponentCatalog, CuratedComponentMetadata } from '../../../scripts/manifest-types.js'; +import type { + ComponentCatalog, + CuratedComponentMetadata, +} from '../../../scripts/manifest-types.js'; // Mapbox GL and the optional geocoder are external (import-map resolved) and not served by the // CDN, so these components declare neither a CDN-served stylesheet nor a bundled integration // chunk — consumers load the Mapbox JavaScript and CSS from the source their import map points at. +// +// `MapboxMap` and `StoreLocator` render, so the package default `visible` suits them: the import +// waits until the map is about to be seen. Every other component configures the map from markup +// that renders nothing, and the README tells consumers to mark such an element `hidden`. A hidden +// element never intersects the viewport, so a `visible` entry would wait for a signal that can +// never come. They declare `eager` instead: the registry only schedules a token declared in the +// document, so the import happens because the element exists, and it runs on a background +// scheduler task rather than at page load. The heavy `mapbox-gl` import stays behind `MapboxMap`. const components: readonly CuratedComponentMetadata[] = [ - { token: 'MapboxCluster', group: 'mapbox' }, - { token: 'MapboxClusterItem', group: 'mapbox' }, - { token: 'MapboxFullscreenControl', group: 'mapbox' }, - { token: 'MapboxGeocoder', group: 'mapbox' }, - { token: 'MapboxGeolocateControl', group: 'mapbox' }, - { token: 'MapboxImage', group: 'mapbox' }, - { token: 'MapboxImages', group: 'mapbox' }, - { token: 'MapboxLayer', group: 'mapbox' }, + { token: 'MapboxCluster', group: 'mapbox', strategy: 'eager' }, + { token: 'MapboxClusterItem', group: 'mapbox', strategy: 'eager' }, + { token: 'MapboxFullscreenControl', group: 'mapbox', strategy: 'eager' }, + { token: 'MapboxGeocoder', group: 'mapbox', strategy: 'eager' }, + { token: 'MapboxGeolocateControl', group: 'mapbox', strategy: 'eager' }, + { token: 'MapboxImage', group: 'mapbox', strategy: 'eager' }, + { token: 'MapboxImages', group: 'mapbox', strategy: 'eager' }, + { token: 'MapboxLayer', group: 'mapbox', strategy: 'eager' }, { token: 'MapboxMap', group: 'mapbox' }, - { token: 'MapboxMarker', group: 'mapbox' }, - { token: 'MapboxNavigationControl', group: 'mapbox' }, - { token: 'MapboxPopup', group: 'mapbox' }, - { token: 'MapboxSource', group: 'mapbox' }, + { token: 'MapboxMarker', group: 'mapbox', strategy: 'eager' }, + { token: 'MapboxNavigationControl', group: 'mapbox', strategy: 'eager' }, + { token: 'MapboxPopup', group: 'mapbox', strategy: 'eager' }, + { token: 'MapboxSource', group: 'mapbox', strategy: 'eager' }, { token: 'StoreLocator', group: 'mapbox' }, ]; diff --git a/packages/ui-mapbox/src/manifest.ts b/packages/ui-mapbox/src/manifest.ts index 7aa44929..edb4218a 100644 --- a/packages/ui-mapbox/src/manifest.ts +++ b/packages/ui-mapbox/src/manifest.ts @@ -3,41 +3,41 @@ import type { ComponentManifest } from '@studiometa/js-toolkit'; export const manifest: ComponentManifest = { MapboxCluster: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxCluster.js').then(({ MapboxCluster }) => MapboxCluster), }, MapboxClusterItem: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxClusterItem.js').then(({ MapboxClusterItem }) => MapboxClusterItem), }, MapboxFullscreenControl: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxFullscreenControl.js').then( ({ MapboxFullscreenControl }) => MapboxFullscreenControl, ), }, MapboxGeocoder: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxGeocoder.js').then(({ MapboxGeocoder }) => MapboxGeocoder), }, MapboxGeolocateControl: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxGeolocateControl.js').then( ({ MapboxGeolocateControl }) => MapboxGeolocateControl, ), }, MapboxImage: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxImage.js').then(({ MapboxImage }) => MapboxImage), }, MapboxImages: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxImages.js').then(({ MapboxImages }) => MapboxImages), }, MapboxLayer: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxLayer.js').then(({ MapboxLayer }) => MapboxLayer), }, MapboxMap: { @@ -45,22 +45,22 @@ export const manifest: ComponentManifest = { load: () => import('./MapboxMap.js').then(({ MapboxMap }) => MapboxMap), }, MapboxMarker: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxMarker.js').then(({ MapboxMarker }) => MapboxMarker), }, MapboxNavigationControl: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxNavigationControl.js').then( ({ MapboxNavigationControl }) => MapboxNavigationControl, ), }, MapboxPopup: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxPopup.js').then(({ MapboxPopup }) => MapboxPopup), }, MapboxSource: { - mountStrategy: 'visible', + mountStrategy: 'eager', load: () => import('./MapboxSource.js').then(({ MapboxSource }) => MapboxSource), }, StoreLocator: { diff --git a/scripts/generate-manifests.ts b/scripts/generate-manifests.ts index 79130373..815ae764 100644 --- a/scripts/generate-manifests.ts +++ b/scripts/generate-manifests.ts @@ -153,7 +153,9 @@ function serializeComponent( component: CuratedComponentMetadata, packageJson: { exports: Record }, ) { - const { strategy } = catalog; + // A component states its own strategy when the package default does not fit it; the catalog + // strategy covers the rest. + const strategy = component.strategy ?? catalog.strategy; const { token } = component; const subpath = component.subpath ?? token; const exportName = component.exportName ?? token; diff --git a/scripts/manifest-types.ts b/scripts/manifest-types.ts index c8d3791a..bbf76bc6 100644 --- a/scripts/manifest-types.ts +++ b/scripts/manifest-types.ts @@ -11,7 +11,8 @@ import type { MountStrategy } from '@studiometa/js-toolkit'; /** * Curated authoring metadata for a single component, consumed by the manifest generator. The - * `subpath` and `exportName` default to the `token` when omitted. + * `subpath` and `exportName` default to the `token` when omitted, and `strategy` defaults to the + * one the catalog declares for the whole package. */ export interface CuratedComponentMetadata { token: string; @@ -21,11 +22,13 @@ export interface CuratedComponentMetadata { integrations?: readonly string[]; subpath?: string; exportName?: string; + strategy?: MountStrategy; } /** * The authoring catalog for a component package: the list of its components, the default strategy - * they share, and the abstract exports that are intentionally excluded from the manifest. + * a component inherits when it declares none of its own, and the abstract exports that are + * intentionally excluded from the manifest. */ export interface ComponentCatalog { packageName: string;