@pie-players/pie-players-shared 0.3.75 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +88 -77
  2. package/dist/components/vendor/nds/README.md +4 -5
  3. package/dist/config/profile.d.ts +1 -1
  4. package/dist/config/profile.js +1 -1
  5. package/dist/i18n/index.d.ts +1 -1
  6. package/dist/i18n/index.js +1 -1
  7. package/dist/i18n/language-tags.d.ts +9 -1
  8. package/dist/i18n/language-tags.js +11 -1
  9. package/dist/i18n/messages/en-US.d.ts +33 -5
  10. package/dist/i18n/messages/en-US.js +33 -5
  11. package/dist/i18n/messages/nl-NL.d.ts +33 -5
  12. package/dist/i18n/messages/nl-NL.js +33 -5
  13. package/dist/i18n/provider.d.ts +14 -7
  14. package/dist/i18n/provider.js +26 -29
  15. package/dist/index.js +0 -1
  16. package/dist/instrumentation/providers/BaseInstrumentationProvider.js +0 -14
  17. package/dist/loader-config.d.ts +0 -2
  18. package/dist/loader-config.js +0 -2
  19. package/dist/loaders/element-loader.js +13 -6
  20. package/dist/loaders/esm-adapter.js +48 -37
  21. package/dist/loaders/iife-adapter.d.ts +3 -3
  22. package/dist/loaders/iife-adapter.js +36 -35
  23. package/dist/media/index.d.ts +4 -6
  24. package/dist/media/index.js +6 -8
  25. package/dist/object/index.d.ts +8 -3
  26. package/dist/object/index.js +8 -6
  27. package/dist/pie/asset-handler.d.ts +0 -17
  28. package/dist/pie/asset-handler.js +1 -1
  29. package/dist/pie/authoring.js +5 -7
  30. package/dist/pie/elements-rendered.d.ts +0 -2
  31. package/dist/pie/elements-rendered.js +1 -1
  32. package/dist/pie/initialization.d.ts +13 -14
  33. package/dist/pie/initialization.js +61 -75
  34. package/dist/pie/item-session-contract.js +7 -9
  35. package/dist/pie/resource-monitor.d.ts +34 -32
  36. package/dist/pie/resource-monitor.js +152 -231
  37. package/dist/pie/stages.d.ts +4 -3
  38. package/dist/pie/stages.js +4 -3
  39. package/dist/pie/updates.js +5 -4
  40. package/dist/pie/utils.d.ts +0 -2
  41. package/dist/pie/utils.js +0 -2
  42. package/dist/security/sanitize-item-markup.d.ts +4 -4
  43. package/dist/security/sanitize-item-markup.js +4 -4
  44. package/dist/security/sanitize-svg-icon.js +2 -2
  45. package/dist/security/wrap-overwide-images.d.ts +1 -1
  46. package/dist/security/wrap-overwide-images.js +1 -1
  47. package/dist/timed-media/media-element-source.js +1 -2
  48. package/dist/types/index.d.ts +62 -52
  49. package/dist/types/index.js +2 -2
  50. package/dist/ui/attribute-coercion.d.ts +5 -0
  51. package/dist/ui/attribute-coercion.js +6 -4
  52. package/dist/ui/content-styles.d.ts +6 -10
  53. package/dist/ui/content-styles.js +8 -13
  54. package/dist/ui/first-focusable.d.ts +0 -1
  55. package/dist/ui/first-focusable.js +0 -1
  56. package/dist/ui/focus-trap.js +2 -3
  57. package/dist/ui/scope-css.d.ts +23 -26
  58. package/dist/ui/scope-css.js +23 -26
  59. package/package.json +3 -2
@@ -43,12 +43,10 @@ export declare function detectBrowserLocale(): string;
43
43
  /**
44
44
  * The bundled provider.
45
45
  *
46
- * Retains the `SimpleI18n` name and the `I18nServiceApi` surface it published
47
- * before adoption. What changed is behaviour that had no caller: catalogs are
48
- * BCP-47 keyed and resolved through RFC 4647 lookup rather than string equality,
49
- * `dir`/`lang` are no longer written to `document.documentElement` (an embedded
50
- * player has no business writing the host page's root), and plural categories
51
- * come from `Intl.PluralRules` alone.
46
+ * Catalogs are BCP-47 keyed and resolved through RFC 4647 lookup, and plural
47
+ * categories come from `Intl.PluralRules`. It never writes `dir` or `lang` to
48
+ * `document.documentElement`: an embedded player does not own the host page's
49
+ * root.
52
50
  */
53
51
  export declare class SimpleI18n implements I18nServiceApi {
54
52
  /** Shared across instances and views: `Intl.PluralRules` is not cheap. */
@@ -138,11 +136,20 @@ export declare class SimpleI18n implements I18nServiceApi {
138
136
  private selectPluralCategory;
139
137
  private notify;
140
138
  }
139
+ /**
140
+ * The graceful default: an English-only provider, shared process-wide.
141
+ *
142
+ * `composition-context.md` requires a resolver to work with no publisher
143
+ * present, and this is that state. A tool mounted bare — in `print-player` or
144
+ * an authoring harness — resolves this and renders English rather than
145
+ * leaking raw message keys onto the screen. It carries no locale loader, so it
146
+ * pulls no catalog beyond the 5 KB English one already in the bundle.
147
+ */
141
148
  export declare function getDefaultI18n(): I18nServiceApi;
142
149
  /**
143
150
  * Resolve a component's interface-locale provider from a published context.
144
151
  *
145
- * Returns a **fresh** object on every call, and that is the whole point. A
152
+ * Returns a **fresh** object on every call. A
146
153
  * provider is mutable: `setLocale` swaps its catalog in place, so its identity is
147
154
  * unchanged before and after a locale load. A reactive `$derived` reading the
148
155
  * provider directly therefore never invalidates, and a label rendered while the
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @module @pie-players/pie-players-shared/i18n/provider
11
11
  */
12
- import { findBestLanguageMatch, normalizeLanguageTag, } from "./language-tags.js";
12
+ import { findBestLanguageMatch, normalizeLanguageTag, toLanguageTag, } from "./language-tags.js";
13
13
  import enUS from "./messages/en-US.js";
14
14
  /** The locale every fallback chain terminates at. */
15
15
  export const DEFAULT_LOCALE = "en-US";
@@ -31,7 +31,6 @@ export function dynamicMessageKey(key) {
31
31
  * Consulted only where `Intl.Locale.prototype.textInfo` is unavailable — it is
32
32
  * the authoritative CLDR answer but shipped late in Safari, and an assessment
33
33
  * laid out left-to-right for an Arabic reader is not a graceful degradation.
34
- * Replaces a four-entry list that omitted every subtag below.
35
34
  */
36
35
  const RTL_LANGUAGES = new Set([
37
36
  "ar", // Arabic
@@ -101,12 +100,10 @@ export function detectBrowserLocale() {
101
100
  /**
102
101
  * The bundled provider.
103
102
  *
104
- * Retains the `SimpleI18n` name and the `I18nServiceApi` surface it published
105
- * before adoption. What changed is behaviour that had no caller: catalogs are
106
- * BCP-47 keyed and resolved through RFC 4647 lookup rather than string equality,
107
- * `dir`/`lang` are no longer written to `document.documentElement` (an embedded
108
- * player has no business writing the host page's root), and plural categories
109
- * come from `Intl.PluralRules` alone.
103
+ * Catalogs are BCP-47 keyed and resolved through RFC 4647 lookup, and plural
104
+ * categories come from `Intl.PluralRules`. It never writes `dir` or `lang` to
105
+ * `document.documentElement`: an embedded player does not own the host page's
106
+ * root.
110
107
  */
111
108
  export class SimpleI18n {
112
109
  /** Shared across instances and views: `Intl.PluralRules` is not cheap. */
@@ -121,7 +118,7 @@ export class SimpleI18n {
121
118
  config;
122
119
  constructor(config = {}) {
123
120
  this.config = { ...config };
124
- this.fallbackLocale = normalizeTag(config.fallbackLocale) || DEFAULT_LOCALE;
121
+ this.fallbackLocale = toLanguageTag(config.fallbackLocale) || DEFAULT_LOCALE;
125
122
  this.catalogs = new Map([[DEFAULT_LOCALE, enUS]]);
126
123
  this.customMessages = { ...(config.customMessages ?? {}) };
127
124
  this.loading = new Map();
@@ -160,11 +157,15 @@ export class SimpleI18n {
160
157
  // A locale we ship no catalog for is legitimate: the host may have
161
158
  // supplied its own messages for it. Honour the tag and let the
162
159
  // fallback chain cover whatever those messages omit.
163
- this.locale = normalizeTag(locale) || DEFAULT_LOCALE;
160
+ this.locale = toLanguageTag(locale) || DEFAULT_LOCALE;
164
161
  this.notify();
165
162
  return;
166
163
  }
167
- if (!this.catalogs.has(resolved)) {
164
+ // The loader is asked only for tags it declares. A tag that resolved through
165
+ // host messages alone has nothing to load, and is honoured like the
166
+ // unresolved case above.
167
+ if (!this.catalogs.has(resolved) &&
168
+ this.config.availableLocales?.includes(resolved)) {
168
169
  const inFlight = this.loading.get(resolved);
169
170
  if (inFlight) {
170
171
  await inFlight;
@@ -243,7 +244,7 @@ export class SimpleI18n {
243
244
  * a catalog.
244
245
  */
245
246
  addCustomMessages(locale, messages) {
246
- const tag = normalizeTag(locale);
247
+ const tag = toLanguageTag(locale);
247
248
  this.customMessages[tag] = deepMerge(this.customMessages[tag] ?? {}, messages);
248
249
  this.notify();
249
250
  }
@@ -256,7 +257,7 @@ export class SimpleI18n {
256
257
  * locales: the locale is per-view, the catalogs are per-provider.
257
258
  */
258
259
  withLocale(locale) {
259
- const resolved = this.resolveLocaleTag(locale) ?? normalizeTag(locale);
260
+ const resolved = this.resolveLocaleTag(locale) ?? toLanguageTag(locale);
260
261
  if (resolved === this.locale)
261
262
  return this;
262
263
  const view = new SimpleI18n({ ...this.config, locale: resolved });
@@ -320,9 +321,9 @@ export class SimpleI18n {
320
321
  this.catalogs.set(locale, await load(locale));
321
322
  }
322
323
  catch (error) {
323
- // A failed load is not fatal: the fallback chain still resolves every
324
- // key to English. Rethrowing here would take down a player over a
325
- // missing chunk.
324
+ // Rethrown with the locale named: `setLocale` rejects before switching
325
+ // (it assigns `this.locale` only after the load), so the previous locale
326
+ // stays active.
326
327
  throw new Error(`Failed to load i18n catalog for locale: ${locale}`, {
327
328
  cause: error,
328
329
  });
@@ -350,9 +351,9 @@ export class SimpleI18n {
350
351
  const node = this.lookupNode(key);
351
352
  if (typeof node === "string")
352
353
  return node;
353
- // A key landing on a namespace (`t("common")`) or on a plural group is a
354
- // miss, not a hit: returning the object would put one where the caller
355
- // expects a string, and interpolating it throws.
354
+ // A plural group resolves to its `other` form. A namespace (`t("common")`)
355
+ // is a miss: returning the object would put one where the caller expects
356
+ // a string, and interpolating it throws.
356
357
  if (node !== undefined && isPluralGroup(node))
357
358
  return node.other;
358
359
  return undefined;
@@ -375,9 +376,6 @@ export class SimpleI18n {
375
376
  listener();
376
377
  }
377
378
  }
378
- function normalizeTag(locale) {
379
- return locale ? locale.trim().replace(/_/g, "-") : "";
380
- }
381
379
  function readPath(source, key) {
382
380
  if (!source)
383
381
  return undefined;
@@ -415,17 +413,16 @@ function deepMerge(target, source) {
415
413
  }
416
414
  return result;
417
415
  }
416
+ let defaultProvider;
418
417
  /**
419
418
  * The graceful default: an English-only provider, shared process-wide.
420
419
  *
421
420
  * `composition-context.md` requires a resolver to work with no publisher
422
- * present, and this is that state. A tool mounted bare — in `print-player`, in
423
- * Studio preview, in an authoring harness — resolves this and renders English
424
- * rather than leaking raw message keys onto the screen. It carries no locale
425
- * loader, so it pulls no catalog beyond the 5 KB English one already in the
426
- * bundle.
421
+ * present, and this is that state. A tool mounted bare — in `print-player` or
422
+ * an authoring harness — resolves this and renders English rather than
423
+ * leaking raw message keys onto the screen. It carries no locale loader, so it
424
+ * pulls no catalog beyond the 5 KB English one already in the bundle.
427
425
  */
428
- let defaultProvider;
429
426
  export function getDefaultI18n() {
430
427
  if (!defaultProvider)
431
428
  defaultProvider = new SimpleI18n();
@@ -434,7 +431,7 @@ export function getDefaultI18n() {
434
431
  /**
435
432
  * Resolve a component's interface-locale provider from a published context.
436
433
  *
437
- * Returns a **fresh** object on every call, and that is the whole point. A
434
+ * Returns a **fresh** object on every call. A
438
435
  * provider is mutable: `setLocale` swaps its catalog in place, so its identity is
439
436
  * unchanged before and after a locale load. A reactive `$derived` reading the
440
437
  * provider directly therefore never invalidates, and a label rendered while the
package/dist/index.js CHANGED
@@ -5,7 +5,6 @@ export * from "./player-strategy.js";
5
5
  export * from "./security/index.js";
6
6
  export * from "./loaders/index.js";
7
7
  export * from "./object/index.js";
8
- // Barrel export for PIE runtime utilities
9
8
  export * from "./pie/index.js";
10
9
  export { editorPostFix } from "./types/index.js";
11
10
  export * from "./ui/attribute-coercion.js";
@@ -37,17 +37,13 @@ export class BaseInstrumentationProvider {
37
37
  * @param attributes Contextual attributes
38
38
  */
39
39
  trackError(error, attributes) {
40
- // Guard: check if provider is ready
41
40
  if (!this.guardedOperation("trackError", error, attributes)) {
42
41
  return;
43
42
  }
44
- // Apply error filter
45
43
  if (!this.applyErrorFilter(error, attributes)) {
46
44
  return;
47
45
  }
48
- // Transform attributes
49
46
  const finalAttributes = this.transformAttributes(attributes);
50
- // Delegate to concrete provider
51
47
  try {
52
48
  this.doTrackError(error, finalAttributes);
53
49
  if (this.config?.debug) {
@@ -73,28 +69,22 @@ export class BaseInstrumentationProvider {
73
69
  * @param attributes Event attributes
74
70
  */
75
71
  trackEvent(eventName, attributes) {
76
- // Guard: check if provider is ready
77
72
  if (!this.guardedOperation("trackEvent", eventName, attributes)) {
78
73
  return;
79
74
  }
80
- // Apply event filter
81
75
  if (!this.applyEventFilter(eventName, attributes)) {
82
76
  return;
83
77
  }
84
- // Apply sampling
85
78
  if (!this.applySampling(eventName)) {
86
79
  return;
87
80
  }
88
- // Transform attributes
89
81
  let finalAttributes = this.transformAttributes(attributes);
90
- // Add timestamp if not present
91
82
  if (!finalAttributes.timestamp) {
92
83
  finalAttributes = {
93
84
  ...finalAttributes,
94
85
  timestamp: new Date().toISOString(),
95
86
  };
96
87
  }
97
- // Delegate to concrete provider
98
88
  try {
99
89
  this.doTrackEvent(eventName, finalAttributes);
100
90
  if (this.config?.debug) {
@@ -134,11 +124,9 @@ export class BaseInstrumentationProvider {
134
124
  * @param attributes Optional user attributes
135
125
  */
136
126
  setUserContext(userId, attributes) {
137
- // Guard: check if provider is ready
138
127
  if (!this.guardedOperation("setUserContext", userId, attributes)) {
139
128
  return;
140
129
  }
141
- // Delegate to concrete provider
142
130
  try {
143
131
  this.doSetUserContext(userId, attributes);
144
132
  if (this.config?.debug) {
@@ -158,11 +146,9 @@ export class BaseInstrumentationProvider {
158
146
  * @param attributes Attributes to set globally
159
147
  */
160
148
  setGlobalAttributes(attributes) {
161
- // Guard: check if provider is ready
162
149
  if (!this.guardedOperation("setGlobalAttributes", attributes)) {
163
150
  return;
164
151
  }
165
- // Delegate to concrete provider
166
152
  try {
167
153
  this.doSetGlobalAttributes(attributes);
168
154
  if (this.config?.debug) {
@@ -1,7 +1,5 @@
1
1
  /**
2
2
  * Shared loader configuration for PIE custom elements.
3
- *
4
- * Mirrors the `LoaderConfig` used by PIEOneer's custom element tags.
5
3
  */
6
4
  import type { InstrumentationProvider } from "./instrumentation/types.js";
7
5
  export type IifeBundleRetryConfig = {
@@ -1,7 +1,5 @@
1
1
  /**
2
2
  * Shared loader configuration for PIE custom elements.
3
- *
4
- * Mirrors the `LoaderConfig` used by PIEOneer's custom element tags.
5
3
  */
6
4
  export const DEFAULT_IIFE_BUNDLE_RETRY_CONFIG = {
7
5
  retryDelayMs: 3000,
@@ -128,6 +128,9 @@ const DEFAULT_LOAD_TIMEOUT_MS = DEFAULT_IIFE_BUNDLE_RETRY_CONFIG.timeoutMs;
128
128
  // Module-scoped in-flight cache. Key: backend signature + sorted elements.
129
129
  // Keeps concurrent identical requests collapsed to one backend call.
130
130
  const inFlightRequests = new Map();
131
+ // An ESM backend keeps page state that later loads build on (the import maps it
132
+ // injected, the shared dependency versions they fixed), so every load with the
133
+ // same config reuses one backend. IIFE backends are created per call.
131
134
  const resolvedEsmBackends = new Map();
132
135
  const resolvedBackendOverrides = new Map();
133
136
  /**
@@ -350,22 +353,26 @@ function backendKeyOf(backend) {
350
353
  }
351
354
  return "unknown";
352
355
  }
356
+ /**
357
+ * Plain data compares by value, functions and other objects by identity, so
358
+ * two configs get one key only when they behave the same.
359
+ */
353
360
  function fingerprintValue(value) {
354
361
  return JSON.stringify(normalizeForFingerprint(value, new Map()));
355
362
  }
356
363
  function normalizeForFingerprint(value, seen) {
357
364
  if (value === null || value === undefined)
358
365
  return value;
359
- const primitiveType = typeof value;
360
- if (primitiveType === "string" ||
361
- primitiveType === "number" ||
362
- primitiveType === "boolean") {
366
+ const valueType = typeof value;
367
+ if (valueType === "string" ||
368
+ valueType === "number" ||
369
+ valueType === "boolean") {
363
370
  return value;
364
371
  }
365
- if (primitiveType === "function") {
372
+ if (valueType === "function") {
366
373
  return { __functionRef: getFunctionRefId(value) };
367
374
  }
368
- if (primitiveType !== "object") {
375
+ if (valueType !== "object") {
369
376
  return String(value);
370
377
  }
371
378
  const objectValue = value;
@@ -22,6 +22,7 @@ import { parsePackageName } from "../pie/utils.js";
22
22
  import { AdapterFailure, } from "./element-loader-types.js";
23
23
  import { isExactSemver } from "./element-package-policy.js";
24
24
  import { forwardMathjaxEvents } from "./mathjax-events.js";
25
+ import { isPlainRecord } from "../object/index.js";
25
26
  export const BUILT_IN_VIEWS = {
26
27
  delivery: { subpath: "", tagSuffix: "" },
27
28
  author: { subpath: "/author", tagSuffix: "-config", fallback: "delivery" },
@@ -123,10 +124,15 @@ export function createEsmBackend(config) {
123
124
  let injectedMap;
124
125
  if (Object.keys(imports).length > 0) {
125
126
  const json = JSON.stringify({ imports }, null, 2);
127
+ // URL resolution maps the shared dependencies and the editor
128
+ // runtime, so it needs import maps too.
126
129
  assertImportMapSupported();
127
130
  injectedMap = injectImportMap(json, context.doc, runtimePlan?.mappedRuntime);
128
131
  importMapObserver?.(json, context.doc);
129
132
  }
133
+ // Recorded before the map settles: a load that starts meanwhile skips
134
+ // these packages, builds on the shared dependency versions this map
135
+ // fixed, and its imports wait for the map in `importModule`.
130
136
  for (const [pkg, variant] of runtimePlan?.served ?? []) {
131
137
  editorRuntimeVariants.set(pkg, variant);
132
138
  }
@@ -152,7 +158,7 @@ export function createEsmBackend(config) {
152
158
  reasons.set(tag, {
153
159
  kind: "define-failed",
154
160
  tag,
155
- cause: err instanceof Error ? err.message : String(err),
161
+ cause: errorMessage(err),
156
162
  });
157
163
  return;
158
164
  }
@@ -189,9 +195,7 @@ export function createEsmBackend(config) {
189
195
  kind: "module-load-failed",
190
196
  tag,
191
197
  specifier: fallbackSpecifier,
192
- cause: fallbackErr instanceof Error
193
- ? fallbackErr.message
194
- : String(fallbackErr),
198
+ cause: errorMessage(fallbackErr),
195
199
  });
196
200
  return;
197
201
  }
@@ -201,7 +205,7 @@ export function createEsmBackend(config) {
201
205
  kind: "module-load-failed",
202
206
  tag,
203
207
  specifier,
204
- cause: err instanceof Error ? err.message : String(err),
208
+ cause: errorMessage(err),
205
209
  });
206
210
  return;
207
211
  }
@@ -225,6 +229,8 @@ export function createEsmBackend(config) {
225
229
  return;
226
230
  }
227
231
  try {
232
+ // `customElements` rejects a constructor already defined under
233
+ // another tag, so each versioned tag gets its own subclass.
228
234
  defineCustomElementSafely(actualTag, class extends ElementClass {
229
235
  }, `element tag for ${packageName}`);
230
236
  }
@@ -232,35 +238,13 @@ export function createEsmBackend(config) {
232
238
  reasons.set(tag, {
233
239
  kind: "define-failed",
234
240
  tag,
235
- cause: err instanceof Error ? err.message : String(err),
241
+ cause: errorMessage(err),
236
242
  });
237
243
  return;
238
244
  }
239
- let controller = null;
240
- if (loadControllers) {
241
- const servedVariant = editorRuntimeVariants.get(packageVersion);
242
- const variantController = servedVariant?.views.controller;
243
- if (servedVariant && variantController) {
244
- try {
245
- const controllerModule = await importModule(cdnProvider.browserViewUrl(packageVersion, variantController), context.doc);
246
- controller = controllerModule?.default ?? controllerModule;
247
- }
248
- catch (err) {
249
- reportEditorRuntimeFallback(packageVersion, servedVariant.declaration, servedVariant.runtime, `its editor-runtime controller failed to load: ${errorMessage(err)}`);
250
- }
251
- }
252
- if (!controller) {
253
- const controllerSpecifier = resolveControllerSpecifier(packageName, packageVersion, moduleResolution, cdnProvider);
254
- try {
255
- const controllerModule = await importModule(controllerSpecifier, context.doc);
256
- controller = controllerModule?.default ?? controllerModule;
257
- }
258
- catch {
259
- // Controllers are best-effort; element registration is what the
260
- // primitive verifies.
261
- }
262
- }
263
- }
245
+ const controller = loadControllers
246
+ ? await importController(packageName, packageVersion, context.doc)
247
+ : null;
264
248
  writeRegistryEntry({
265
249
  package: packageVersion,
266
250
  status: Status.loaded,
@@ -293,6 +277,35 @@ export function createEsmBackend(config) {
293
277
  return undefined;
294
278
  }
295
279
  }
280
+ /**
281
+ * The package's controller: its editor-runtime variant's when one is served
282
+ * and loads, else the package's own. Controllers are best-effort; element
283
+ * registration is what the primitive verifies.
284
+ */
285
+ async function importController(packageName, packageVersion, doc) {
286
+ let controller = null;
287
+ const servedVariant = editorRuntimeVariants.get(packageVersion);
288
+ const variantController = servedVariant?.views.controller;
289
+ if (servedVariant && variantController) {
290
+ try {
291
+ const controllerModule = await importModule(cdnProvider.browserViewUrl(packageVersion, variantController), doc);
292
+ controller = controllerModule?.default ?? controllerModule;
293
+ if (controller)
294
+ return controller;
295
+ }
296
+ catch (err) {
297
+ reportEditorRuntimeFallback(packageVersion, servedVariant.declaration, servedVariant.runtime, `its editor-runtime controller failed to load: ${errorMessage(err)}`);
298
+ }
299
+ }
300
+ const controllerSpecifier = resolveControllerSpecifier(packageName, packageVersion, moduleResolution, cdnProvider);
301
+ try {
302
+ const controllerModule = await importModule(controllerSpecifier, doc);
303
+ return controllerModule?.default ?? controllerModule;
304
+ }
305
+ catch {
306
+ return controller;
307
+ }
308
+ }
296
309
  /**
297
310
  * Imports through the browser, or through es-module-shims once the document
298
311
  * has rejected an import map, after every map a backend added has settled.
@@ -547,7 +560,8 @@ export function mapEsmViewElements(elements, view = "delivery", viewConfig) {
547
560
  ]));
548
561
  }
549
562
  function defaultImporter(specifier) {
550
- // @vite-ignore — dynamic import resolved at runtime.
563
+ // Host bundlers must leave this import alone: the specifier is a CDN URL or
564
+ // import-map specifier known only at runtime.
551
565
  return import(/* webpackIgnore: true */ /* @vite-ignore */ specifier);
552
566
  }
553
567
  /**
@@ -872,9 +886,6 @@ const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
872
886
  /** Slash-separated segments, none starting with a dot. */
873
887
  const BROWSER_VIEW_PATH = /^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$/;
874
888
  const BARE_SPECIFIER = /^(?:@[A-Za-z0-9_-][A-Za-z0-9._-]*\/)?[A-Za-z0-9_-][A-Za-z0-9._-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$/;
875
- function isRecord(value) {
876
- return typeof value === "object" && value !== null && !Array.isArray(value);
877
- }
878
889
  /**
879
890
  * A package's `pie.browserEditorRuntime`: `undefined` when it declares none,
880
891
  * `null` when the declaration is malformed.
@@ -883,7 +894,7 @@ function readEditorRuntimeDeclaration(metadata) {
883
894
  const declared = metadata?.pie?.browserEditorRuntime;
884
895
  if (declared === undefined)
885
896
  return undefined;
886
- if (!isRecord(declared) || !isRecord(declared.views))
897
+ if (!isPlainRecord(declared) || !isPlainRecord(declared.views))
887
898
  return null;
888
899
  const { name, version } = declared;
889
900
  if (typeof name !== "string" || !PACKAGE_NAME.test(name))
@@ -907,7 +918,7 @@ function readEditorRuntimeDeclaration(metadata) {
907
918
  */
908
919
  function readBrowserModules(metadata) {
909
920
  const declared = metadata.pie?.browserModules;
910
- if (!isRecord(declared))
921
+ if (!isPlainRecord(declared))
911
922
  return null;
912
923
  const modules = Object.entries(declared);
913
924
  const valid = modules.length > 0 &&
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * IIFE backend adapter for the ElementLoader primitive.
3
3
  *
4
- * Loads PIE elements from IIFE bundles served by the PIE build service,
4
+ * Loads PIE elements from IIFE bundles served by the bundle host,
5
5
  * then registers them with `customElements`. On any per-tag failure
6
6
  * (missing package, non-constructor element class, define failure),
7
7
  * throws `AdapterFailure` with a structured `reasons` map that the
@@ -38,7 +38,7 @@ export type IifeBundleRetryStatus = {
38
38
  };
39
39
  export type IifeBackendConfig = {
40
40
  kind: "iife";
41
- /** Base URL for the PIE bundle service. */
41
+ /** Base URL of the bundle host. */
42
42
  bundleHost: string;
43
43
  /** Bundle variant to request (player / client-player / editor). */
44
44
  bundleType?: BundleType;
@@ -54,7 +54,7 @@ export type IifeBackendConfig = {
54
54
  /**
55
55
  * Retry policy for transient bundle load failures.
56
56
  *
57
- * When the bundle service is building a bundle on demand, the first
57
+ * When the bundle host is building a bundle on demand, the first
58
58
  * few `<script src>` requests can fail. This policy polls until the
59
59
  * bundle is available or `timeoutMs` elapses. Default matches
60
60
  * `DEFAULT_IIFE_BUNDLE_RETRY_CONFIG`.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * IIFE backend adapter for the ElementLoader primitive.
3
3
  *
4
- * Loads PIE elements from IIFE bundles served by the PIE build service,
4
+ * Loads PIE elements from IIFE bundles served by the bundle host,
5
5
  * then registers them with `customElements`. On any per-tag failure
6
6
  * (missing package, non-constructor element class, define failure),
7
7
  * throws `AdapterFailure` with a structured `reasons` map that the
@@ -76,7 +76,7 @@ export function createIifeBackend(config) {
76
76
  /**
77
77
  * Wrap the single-shot script loader with poll-until-ready retry.
78
78
  *
79
- * The PIE bundle service may return a transient failure while it
79
+ * The bundle host may return a transient failure while it
80
80
  * builds a bundle on demand. Retry with fixed-interval polling until
81
81
  * the bundle is ready or the cumulative deadline elapses. Emits
82
82
  * retry lifecycle events to the host via `onBundleRetryStatus` and to
@@ -111,7 +111,7 @@ export function createIifeBackend(config) {
111
111
  catch (error) {
112
112
  const elapsedMs = Date.now() - startedAt;
113
113
  const remainingMs = timeoutMs - elapsedMs;
114
- const reason = error instanceof Error ? error.message : String(error);
114
+ const reason = errorMessage(error);
115
115
  if (remainingMs <= 0) {
116
116
  const timeoutError = new Error(`IIFE bundle load timed out after ${timeoutMs}ms: ${url}`);
117
117
  config.onBundleRetryStatus?.({
@@ -201,45 +201,33 @@ export function createIifeBackend(config) {
201
201
  return;
202
202
  const conflict = findIifeElementMapConflict(elements);
203
203
  if (conflict) {
204
- const reasons = new Map();
205
- for (const tag of Object.keys(elements)) {
206
- reasons.set(tag, {
207
- kind: "backend-rejected",
208
- tag,
209
- cause: conflict,
210
- });
211
- }
212
- throw new AdapterFailure(reasons);
204
+ throw failureForEveryTag(elements, (tag) => ({
205
+ kind: "backend-rejected",
206
+ tag,
207
+ cause: conflict,
208
+ }));
213
209
  }
214
210
  const bundleUrl = buildBundleUrl(elements, bundleType, config);
215
211
  try {
216
212
  await ensureBundleLoaded(bundleUrl, context.doc);
217
213
  }
218
214
  catch (err) {
219
- const cause = err instanceof Error ? err.message : String(err);
220
- const reasons = new Map();
221
- for (const tag of Object.keys(elements)) {
222
- reasons.set(tag, {
223
- kind: "bundle-load-failed",
224
- tag,
225
- url: bundleUrl,
226
- cause,
227
- });
228
- }
229
- throw new AdapterFailure(reasons);
215
+ const cause = errorMessage(err);
216
+ throw failureForEveryTag(elements, (tag) => ({
217
+ kind: "bundle-load-failed",
218
+ tag,
219
+ url: bundleUrl,
220
+ cause,
221
+ }));
230
222
  }
231
223
  const pieModule = readPieModule();
232
224
  if (!pieModule) {
233
- const reasons = new Map();
234
- for (const tag of Object.keys(elements)) {
235
- reasons.set(tag, {
236
- kind: "bundle-load-failed",
237
- tag,
238
- url: bundleUrl,
239
- cause: "window.pie.default missing after bundle load",
240
- });
241
- }
242
- throw new AdapterFailure(reasons);
225
+ throw failureForEveryTag(elements, (tag) => ({
226
+ kind: "bundle-load-failed",
227
+ tag,
228
+ url: bundleUrl,
229
+ cause: "window.pie.default missing after bundle load",
230
+ }));
243
231
  }
244
232
  const reasons = new Map();
245
233
  const isEditorBundle = bundleType === BundleType.editor;
@@ -282,11 +270,13 @@ export function createIifeBackend(config) {
282
270
  reasons.set(tag, {
283
271
  kind: "define-failed",
284
272
  tag,
285
- cause: err instanceof Error ? err.message : String(err),
273
+ cause: errorMessage(err),
286
274
  });
287
275
  continue;
288
276
  }
289
277
  try {
278
+ // `customElements` rejects a constructor already defined under
279
+ // another tag, so each versioned tag gets its own subclass.
290
280
  defineCustomElementSafely(actualTag, class extends ElementClass {
291
281
  }, `element tag for ${packageName}`);
292
282
  }
@@ -294,7 +284,7 @@ export function createIifeBackend(config) {
294
284
  reasons.set(tag, {
295
285
  kind: "define-failed",
296
286
  tag,
297
- cause: err instanceof Error ? err.message : String(err),
287
+ cause: errorMessage(err),
298
288
  });
299
289
  continue;
300
290
  }
@@ -320,6 +310,17 @@ export function createIifeBackend(config) {
320
310
  };
321
311
  }
322
312
  // ─── Helpers ─────────────────────────────────────────────────────────────────
313
+ /** A failure of the whole request, reported once per tag. */
314
+ function failureForEveryTag(elements, reasonFor) {
315
+ const reasons = new Map();
316
+ for (const tag of Object.keys(elements)) {
317
+ reasons.set(tag, reasonFor(tag));
318
+ }
319
+ return new AdapterFailure(reasons);
320
+ }
321
+ function errorMessage(err) {
322
+ return err instanceof Error ? err.message : String(err);
323
+ }
323
324
  function defaultLoadBundleScript(url, doc) {
324
325
  return new Promise((resolve, reject) => {
325
326
  const script = doc.createElement("script");