@asteby/metacore-runtime-react 31.1.1 → 32.1.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 (43) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/action-modal-dispatcher.d.ts +20 -0
  3. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  4. package/dist/action-modal-dispatcher.js +52 -74
  5. package/dist/addon-fiber.d.ts +57 -0
  6. package/dist/addon-fiber.d.ts.map +1 -0
  7. package/dist/addon-fiber.js +122 -0
  8. package/dist/addon-loader.d.ts +19 -3
  9. package/dist/addon-loader.d.ts.map +1 -1
  10. package/dist/addon-loader.js +36 -37
  11. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  12. package/dist/dialogs/dynamic-record.js +27 -28
  13. package/dist/dynamic-form-schema.d.ts.map +1 -1
  14. package/dist/dynamic-form-schema.js +2 -1
  15. package/dist/index.d.ts +4 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +4 -1
  18. package/dist/server-error.d.ts +17 -8
  19. package/dist/server-error.d.ts.map +1 -1
  20. package/dist/server-error.js +75 -28
  21. package/dist/types.d.ts +4 -5
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/validation-catalog.d.ts +8 -0
  24. package/dist/validation-catalog.d.ts.map +1 -0
  25. package/dist/validation-catalog.js +59 -0
  26. package/dist/validator.d.ts +22 -0
  27. package/dist/validator.d.ts.map +1 -0
  28. package/dist/validator.js +261 -0
  29. package/package.json +3 -3
  30. package/src/__tests__/addon-fiber.test.ts +111 -0
  31. package/src/__tests__/extract-field-errors.test.ts +25 -1
  32. package/src/__tests__/prefill-from-record.test.ts +146 -0
  33. package/src/__tests__/validator.test.ts +70 -0
  34. package/src/action-modal-dispatcher.tsx +57 -72
  35. package/src/addon-fiber.ts +155 -0
  36. package/src/addon-loader.tsx +66 -47
  37. package/src/dialogs/dynamic-record.tsx +25 -28
  38. package/src/dynamic-form-schema.ts +3 -2
  39. package/src/index.ts +24 -0
  40. package/src/server-error.ts +85 -30
  41. package/src/types.ts +9 -12
  42. package/src/validation-catalog.ts +60 -0
  43. package/src/validator.ts +275 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 32.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fa25f99: Laravel-style field validator: localize 422 `{errors:{field:[{code,params}]}}`
8
+ into the operator's language (es/en catalogs, overridable via `validation.*`),
9
+ map keys to translated labels, and pre-flight the same regex/min/max/custom
10
+ rules the kernel enforces — no more generic "validation failed" / `[object Object]`.
11
+
12
+ ## 32.0.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 5a932ed: Addon fiber hot-swap without a page reload: Registry.scope/unbind, Plugin.register
17
+ may return a Disposable, AddonLoader re-registers federation remotes when `?v=`
18
+ changes, and purgeAddonFrontendCache drops only that addon's SW cache (L1).
19
+ Hosts call acknowledgeRunningVersion after a successful swap instead of
20
+ location.reload / unregistering the service worker.
21
+
22
+ ### Patch Changes
23
+
24
+ - 5a932ed: Export PrefillSpec helpers (`isPrefillSpec`, `buildPrefillRows`, `applyPrefillLock`)
25
+ so addons can unit-test `$prefillFromRecord` without reaching into private
26
+ dispatcher internals. Kernel now types the same shape in the v3 schema.
27
+ - Updated dependencies [5a932ed]
28
+ - @asteby/metacore-sdk@3.5.0
29
+
3
30
  ## 31.1.1
4
31
 
5
32
  ### Patch Changes
@@ -1,4 +1,24 @@
1
+ import type { ActionFieldDef } from './types';
1
2
  import { type ActionMetadata, type ActionModalProps } from '@asteby/metacore-sdk';
2
3
  export type { ActionMetadata, ActionModalProps };
4
+ export interface PrefillSpec {
5
+ $prefillFromRecord: string;
6
+ map?: Record<string, string>;
7
+ remaining?: {
8
+ target: string;
9
+ of: string;
10
+ minus?: string;
11
+ };
12
+ /**
13
+ * Item-field keys to lock (render read-only) in the prefilled rows — e.g. the
14
+ * product of a receive-goods line is dictated by the source document, so it
15
+ * is shown as a resolved name but cannot be changed. Editable in the create
16
+ * flow (which carries no prefill spec); only the prefilled action locks them.
17
+ */
18
+ lock?: string[];
19
+ }
20
+ export declare function isPrefillSpec(v: unknown): v is PrefillSpec;
21
+ export declare function applyPrefillLock(field: ActionFieldDef): ActionFieldDef;
22
+ export declare function buildPrefillRows(spec: PrefillSpec, record: any): Array<Record<string, any>>;
3
23
  export declare function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess, }: ActionModalProps): import("react").JSX.Element | null;
4
24
  //# sourceMappingURL=action-modal-dispatcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA4DA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AA0FhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCAoElB"}
1
+ {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA4DA,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAA;AAE9E,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAiBhD,MAAM,WAAW,WAAW;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAM1D;AAsBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAStE;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAkB3F;AAED,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCAoElB"}
@@ -14,7 +14,9 @@ import { useTranslation } from 'react-i18next';
14
14
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, Input, Textarea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from '@asteby/metacore-ui/primitives';
15
15
  import { Loader2 } from 'lucide-react';
16
16
  import { toast } from 'sonner';
17
- import { toastServerError, toastServerSuccess, extractFieldErrors, localizeFieldIssue } from './server-error';
17
+ import { toastServerError, toastServerSuccess, extractFieldErrors, localizeFieldErrorMap } from './server-error';
18
+ import { validateValues, bagHasErrors } from './validator';
19
+ import { validationCatalog } from './validation-catalog';
18
20
  import { useApi } from './api-context';
19
21
  import { DynamicIcon } from './dynamic-icon';
20
22
  import { DynamicLineItems } from './dynamic-line-items';
@@ -28,7 +30,7 @@ import { useMetadataCache } from './metadata-cache';
28
30
  import { ViewValue, objectLabel, relationSiblingValue, isLineItemsField as isRecordLineItemsField, } from './dialogs/dynamic-record';
29
31
  // Canonical registry lives in @asteby/metacore-sdk
30
32
  import { getActionComponent, } from '@asteby/metacore-sdk';
31
- function isPrefillSpec(v) {
33
+ export function isPrefillSpec(v) {
32
34
  return (typeof v === 'object' &&
33
35
  v !== null &&
34
36
  typeof v.$prefillFromRecord === 'string');
@@ -51,7 +53,7 @@ function toNum(v) {
51
53
  // untouched when there is no prefill spec or no lock list (the create flow,
52
54
  // which carries no prefill, stays fully editable). The readonly flag is set on
53
55
  // BOTH itemFields aliases the renderers tolerate.
54
- function applyPrefillLock(field) {
56
+ export function applyPrefillLock(field) {
55
57
  const spec = lineItemsDefault(field);
56
58
  if (!isPrefillSpec(spec) || !spec.lock || spec.lock.length === 0)
57
59
  return field;
@@ -64,7 +66,7 @@ function applyPrefillLock(field) {
64
66
  return { ...field, itemFields: patched, item_fields: patched };
65
67
  }
66
68
  // buildPrefillRows projects record[spec.$prefillFromRecord] into modal rows.
67
- function buildPrefillRows(spec, record) {
69
+ export function buildPrefillRows(spec, record) {
68
70
  const src = record?.[spec.$prefillFromRecord];
69
71
  if (!Array.isArray(src))
70
72
  return [];
@@ -264,34 +266,31 @@ function humanizeKey(k) {
264
266
  /** Localize a failed action submit's per-field `errors` (422 map or `{errors}`
265
267
  * body) into `{ [fieldKey]: localizedMessage }`, using each field's label.
266
268
  * Returns undefined when there is no usable per-field map. */
267
- function localizeActionFieldErrors(err, fields, t) {
269
+ function localizeActionFieldErrors(err, fields, t, language) {
268
270
  const map = extractFieldErrors(err);
269
271
  if (!map)
270
272
  return undefined;
271
- const labelFor = (k) => {
272
- const f = (fields ?? []).find(x => x.key === k);
273
- return f?.label ? t(f.label, { defaultValue: f.label }) : humanizeKey(k);
274
- };
275
- const out = {};
276
- for (const [k, issues] of Object.entries(map))
277
- out[k] = localizeFieldIssue(issues[0], labelFor(k), t);
278
- return out;
273
+ const labels = {};
274
+ for (const f of fields ?? []) {
275
+ labels[f.key] = f.label ? t(f.label, { defaultValue: f.label }) : humanizeKey(f.key);
276
+ }
277
+ return localizeFieldErrorMap(map, t, { labels, language });
279
278
  }
280
279
  /** Toast a failed action: a summary + localized per-field lines when the server
281
280
  * returned a per-field `errors` map, else the standard cause-carrying toast.
282
281
  * Used where inline rendering isn't wired (confirm / multi-step wizard). */
283
- function toastActionError(err, fields, t) {
284
- const localized = localizeActionFieldErrors(err, fields, t);
282
+ function toastActionError(err, fields, t, language) {
283
+ const localized = localizeActionFieldErrors(err, fields, t, language);
285
284
  if (localized) {
286
- toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }), {
285
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(language).failed }), {
287
286
  description: Object.values(localized).join('\n'),
288
287
  });
289
288
  return;
290
289
  }
291
- toastServerError(err, { t });
290
+ toastServerError(err, { t, language });
292
291
  }
293
292
  function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
294
- const { t } = useTranslation();
293
+ const { t, i18n } = useTranslation();
295
294
  const api = useApi();
296
295
  const [executing, setExecuting] = useState(false);
297
296
  // `action.label` is an addon-contributed i18n key; its locale bundle loads
@@ -309,11 +308,11 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
309
308
  onSuccess();
310
309
  }
311
310
  else {
312
- toastActionError({ response: { data: res.data } }, action.fields, t);
311
+ toastActionError({ response: { data: res.data } }, action.fields, t, i18n.language);
313
312
  }
314
313
  }
315
314
  catch (err) {
316
- toastActionError(err, action.fields, t);
315
+ toastActionError(err, action.fields, t, i18n.language);
317
316
  }
318
317
  finally {
319
318
  setExecuting(false);
@@ -322,7 +321,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
322
321
  return (_jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), record ? _jsx(RecordPreview, { model: model, record: record }) : null, _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) }));
323
322
  }
324
323
  function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
325
- const { t } = useTranslation();
324
+ const { t, i18n } = useTranslation();
326
325
  // Addon-contributed labels (action + fields) are i18n keys whose locale
327
326
  // bundle loads asynchronously; translate at render so they don't render raw.
328
327
  // defaultValue keeps an already-localized string unchanged.
@@ -389,39 +388,25 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
389
388
  return next;
390
389
  });
391
390
  };
391
+ const lang = i18n.language;
392
392
  const handleActionError = (err) => {
393
- const localized = localizeActionFieldErrors(err, action.fields, t);
393
+ const localized = localizeActionFieldErrors(err, action.fields, t, lang);
394
394
  if (localized) {
395
395
  setFieldErrors(localized);
396
- toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }));
396
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }));
397
397
  return;
398
398
  }
399
- toastServerError(err, { t });
399
+ toastServerError(err, { t, language: lang });
400
400
  };
401
401
  const execute = async () => {
402
402
  if (action.fields) {
403
- // Client-side required check → mark ALL missing fields inline.
404
- const missing = {};
405
- for (const field of action.fields) {
406
- if (!field.required)
407
- continue;
408
- if (isLineItemsField(field)) {
409
- const rows = formData[field.key];
410
- if (!Array.isArray(rows) || rows.length === 0) {
411
- missing[field.key] = t('validation.line_items_required', {
412
- defaultValue: '{{label}} requiere al menos un renglón',
413
- label: tl(field.label),
414
- });
415
- }
416
- continue;
417
- }
418
- if (!formData[field.key] && formData[field.key] !== false) {
419
- missing[field.key] = localizeFieldIssue({ code: 'required' }, tl(field.label), t);
420
- }
421
- }
422
- if (Object.keys(missing).length) {
423
- setFieldErrors(missing);
424
- toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }));
403
+ const bag = validateValues(action.fields, formData);
404
+ if (bagHasErrors(bag)) {
405
+ const labels = {};
406
+ for (const f of action.fields)
407
+ labels[f.key] = tl(f.label);
408
+ setFieldErrors(localizeFieldErrorMap(bag, t, { labels, language: lang }));
409
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }));
425
410
  return;
426
411
  }
427
412
  }
@@ -486,25 +471,14 @@ function buildFieldDefaults(fields, record) {
486
471
  }
487
472
  return defaults;
488
473
  }
489
- // validateFields returns the first validation error for a set of required
490
- // fields, or null when they all pass. Shared by the wizard (per-step gate) and
491
- // mirrors GenericActionModal's inline checks. `tl` localizes the field label.
492
- function validateFields(fields, formData, tl) {
493
- for (const field of fields) {
494
- if (!field.required)
495
- continue;
496
- if (isLineItemsField(field)) {
497
- const rows = formData[field.key];
498
- if (!Array.isArray(rows) || rows.length === 0) {
499
- return `${tl(field.label)} requiere al menos un renglón`;
500
- }
501
- continue;
502
- }
503
- if (!formData[field.key] && formData[field.key] !== false) {
504
- return `${tl(field.label)} es requerido`;
505
- }
506
- }
507
- return null;
474
+ function wizardStepErrors(fields, formData, t, language) {
475
+ const bag = validateValues(fields, formData);
476
+ if (!bagHasErrors(bag))
477
+ return undefined;
478
+ const labels = {};
479
+ for (const f of fields)
480
+ labels[f.key] = t(f.label, { defaultValue: f.label });
481
+ return localizeFieldErrorMap(bag, t, { labels, language });
508
482
  }
509
483
  // WizardActionModal — the third render-path: a multi-step form. It accumulates
510
484
  // every step's fields into ONE formData object and, on the final step, POSTs all
@@ -514,7 +488,7 @@ function validateFields(fields, formData, tl) {
514
488
  // so line-items, dynamic_select, uploads and dates behave identically to a
515
489
  // single-page action form — no widget is duplicated.
516
490
  function WizardActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
517
- const { t } = useTranslation();
491
+ const { t, i18n } = useTranslation();
518
492
  const tl = (s) => t(s, { defaultValue: s });
519
493
  const api = useApi();
520
494
  const steps = action.steps ?? [];
@@ -537,9 +511,11 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
537
511
  const hasLineItems = useMemo(() => stepFields.some(isLineItemsField), [stepFields]);
538
512
  const widthPx = hasLineItems ? '820px' : undefined;
539
513
  const goNext = () => {
540
- const err = validateFields(stepFields, formData, tl);
541
- if (err) {
542
- toast.error(err);
514
+ const localized = wizardStepErrors(stepFields, formData, t, i18n.language);
515
+ if (localized) {
516
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(i18n.language).failed }), {
517
+ description: Object.values(localized).join('\n'),
518
+ });
543
519
  return;
544
520
  }
545
521
  setStepIndex((i) => Math.min(i + 1, steps.length - 1));
@@ -549,9 +525,11 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
549
525
  // Guard every step's required fields on final submit (a user could reach
550
526
  // the last step with an untouched earlier line-items grid otherwise).
551
527
  for (const s of steps) {
552
- const err = validateFields(s.fields ?? [], formData, tl);
553
- if (err) {
554
- toast.error(err);
528
+ const localized = wizardStepErrors(s.fields ?? [], formData, t, i18n.language);
529
+ if (localized) {
530
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(i18n.language).failed }), {
531
+ description: Object.values(localized).join('\n'),
532
+ });
555
533
  return;
556
534
  }
557
535
  }
@@ -565,11 +543,11 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
565
543
  onSuccess();
566
544
  }
567
545
  else {
568
- toastActionError({ response: { data: res.data } }, steps.flatMap(s => s.fields ?? []), t);
546
+ toastActionError({ response: { data: res.data } }, steps.flatMap(s => s.fields ?? []), t, i18n.language);
569
547
  }
570
548
  }
571
549
  catch (err) {
572
- toastActionError(err, steps.flatMap(s => s.fields ?? []), t);
550
+ toastActionError(err, steps.flatMap(s => s.fields ?? []), t, i18n.language);
573
551
  }
574
552
  finally {
575
553
  setExecuting(false);
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Addon fiber primitives — Cordis-style dispose/reload for federated modules
3
+ * running inside a PWA host. Pure helpers so they unit-test without React
4
+ * or the Module Federation runtime.
5
+ */
6
+ import type { AddonAPI, Disposable, Plugin } from '@asteby/metacore-sdk';
7
+ export declare const PURGE_ADDON_MESSAGE: "PURGE_ADDON";
8
+ export interface PurgeAddonMessage {
9
+ type: typeof PURGE_ADDON_MESSAGE;
10
+ key: string;
11
+ }
12
+ /** Shape of the exposed `./register` (or `./plugin`) module. */
13
+ export interface AddonRegisterModule {
14
+ register?: Plugin['register'];
15
+ dispose?: Plugin['dispose'];
16
+ default?: Plugin['register'] | Plugin;
17
+ }
18
+ export interface ResolvedPlugin {
19
+ register?: Plugin['register'];
20
+ dispose?: Plugin['dispose'];
21
+ }
22
+ /**
23
+ * Accept every historical export shape:
24
+ * - `{ register, dispose? }`
25
+ * - `{ default: { register, dispose? } }` (definePlugin)
26
+ * - `{ default: (api) => ... }` (function module)
27
+ */
28
+ export declare function resolvePluginExports(mod: AddonRegisterModule | null | undefined): ResolvedPlugin;
29
+ export declare function runDispose(dispose?: Disposable | null): Promise<void>;
30
+ export declare function composeDisposables(...fns: Array<Disposable | undefined | null>): Disposable;
31
+ /**
32
+ * True when a Cache API / SW request is a federated frontend asset of `addonKey`.
33
+ * Matches both `/api/addons/<key>/frontend` and `/api/metacore/addons/<key>/frontend`.
34
+ */
35
+ export declare function isAddonFrontendCacheUrl(url: string, addonKey: string): boolean;
36
+ /**
37
+ * Drop cached federation assets for one addon. Primary path is the page-side
38
+ * Cache API (works even before the SW learns `PURGE_ADDON`). Also posts the
39
+ * message so a current SW can drop its own entries.
40
+ *
41
+ * Never unregisters the SW and never deletes other caches — L1, not L2.
42
+ */
43
+ export declare function purgeAddonFrontendCache(addonKey: string): Promise<void>;
44
+ /** Test seam: which remote URL is currently registered per federation scope. */
45
+ export declare const remoteEntryByScope: Map<string, string>;
46
+ export declare function shouldReregisterRemote(scope: string, url: string): boolean;
47
+ export declare function markRemoteRegistered(scope: string, url: string): void;
48
+ /** @internal tests */
49
+ export declare function resetRemoteRegistry(): void;
50
+ /**
51
+ * Safe no-op when `register()` returns void. Used by AddonLoader to treat
52
+ * both historical and fiber-style plugins uniformly.
53
+ */
54
+ export declare function disposableFromRegisterResult(result: void | Disposable): Disposable | undefined;
55
+ /** Type-only re-export so AddonLoader does not import Plugin internals twice. */
56
+ export type { AddonAPI, Disposable };
57
+ //# sourceMappingURL=addon-fiber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addon-fiber.d.ts","sourceRoot":"","sources":["../src/addon-fiber.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAExE,eAAO,MAAM,mBAAmB,EAAG,aAAsB,CAAA;AAEzD,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,OAAO,mBAAmB,CAAA;IAChC,GAAG,EAAE,MAAM,CAAA;CACd;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,MAAM,CAAA;CACxC;AAED,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,mBAAmB,GAAG,IAAI,GAAG,SAAS,GAAG,cAAc,CAqBhG;AAED,wBAAsB,UAAU,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3E;AAED,wBAAgB,kBAAkB,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,UAAU,GAAG,SAAS,GAAG,IAAI,CAAC,GAAG,UAAU,CAO3F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAS9E;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B7E;AAED,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,qBAA4B,CAAA;AAE3D,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAE1E;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAErE;AAED,sBAAsB;AACtB,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CACxC,MAAM,EAAE,IAAI,GAAG,UAAU,GAC1B,UAAU,GAAG,SAAS,CAExB;AAED,iFAAiF;AACjF,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAA"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Addon fiber primitives — Cordis-style dispose/reload for federated modules
3
+ * running inside a PWA host. Pure helpers so they unit-test without React
4
+ * or the Module Federation runtime.
5
+ */
6
+ export const PURGE_ADDON_MESSAGE = 'PURGE_ADDON';
7
+ /**
8
+ * Accept every historical export shape:
9
+ * - `{ register, dispose? }`
10
+ * - `{ default: { register, dispose? } }` (definePlugin)
11
+ * - `{ default: (api) => ... }` (function module)
12
+ */
13
+ export function resolvePluginExports(mod) {
14
+ if (!mod)
15
+ return {};
16
+ const fromDefault = mod.default && typeof mod.default === 'object'
17
+ ? mod.default
18
+ : undefined;
19
+ const registerFn = typeof mod.register === 'function'
20
+ ? mod.register
21
+ : typeof fromDefault?.register === 'function'
22
+ ? fromDefault.register.bind(fromDefault)
23
+ : typeof mod.default === 'function'
24
+ ? mod.default
25
+ : undefined;
26
+ const disposeFn = typeof mod.dispose === 'function'
27
+ ? mod.dispose
28
+ : typeof fromDefault?.dispose === 'function'
29
+ ? fromDefault.dispose.bind(fromDefault)
30
+ : undefined;
31
+ return { register: registerFn, dispose: disposeFn };
32
+ }
33
+ export async function runDispose(dispose) {
34
+ if (typeof dispose !== 'function')
35
+ return;
36
+ try {
37
+ await Promise.resolve(dispose());
38
+ }
39
+ catch {
40
+ /* a failing disposer must not block the next fiber mount */
41
+ }
42
+ }
43
+ export function composeDisposables(...fns) {
44
+ const list = fns.filter((f) => typeof f === 'function');
45
+ return async () => {
46
+ for (let i = list.length - 1; i >= 0; i--) {
47
+ await runDispose(list[i]);
48
+ }
49
+ };
50
+ }
51
+ /**
52
+ * True when a Cache API / SW request is a federated frontend asset of `addonKey`.
53
+ * Matches both `/api/addons/<key>/frontend` and `/api/metacore/addons/<key>/frontend`.
54
+ */
55
+ export function isAddonFrontendCacheUrl(url, addonKey) {
56
+ if (!addonKey || !url)
57
+ return false;
58
+ try {
59
+ const path = new URL(url, 'http://local.invalid').pathname;
60
+ const escaped = addonKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
61
+ return new RegExp(`/api/(metacore/)?addons/${escaped}/frontend(/|\\.js)`).test(path);
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ /**
68
+ * Drop cached federation assets for one addon. Primary path is the page-side
69
+ * Cache API (works even before the SW learns `PURGE_ADDON`). Also posts the
70
+ * message so a current SW can drop its own entries.
71
+ *
72
+ * Never unregisters the SW and never deletes other caches — L1, not L2.
73
+ */
74
+ export async function purgeAddonFrontendCache(addonKey) {
75
+ if (!addonKey)
76
+ return;
77
+ try {
78
+ if (typeof caches !== 'undefined') {
79
+ const names = await caches.keys();
80
+ await Promise.all(names.map(async (name) => {
81
+ if (!name.includes('addon-federation'))
82
+ return;
83
+ const cache = await caches.open(name);
84
+ const requests = await cache.keys();
85
+ await Promise.all(requests
86
+ .filter((req) => isAddonFrontendCacheUrl(req.url, addonKey))
87
+ .map((req) => cache.delete(req)));
88
+ }));
89
+ }
90
+ }
91
+ catch {
92
+ /* private mode / no Cache API */
93
+ }
94
+ try {
95
+ if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
96
+ const controller = navigator.serviceWorker.controller;
97
+ controller?.postMessage({ type: PURGE_ADDON_MESSAGE, key: addonKey });
98
+ }
99
+ }
100
+ catch {
101
+ /* no SW */
102
+ }
103
+ }
104
+ /** Test seam: which remote URL is currently registered per federation scope. */
105
+ export const remoteEntryByScope = new Map();
106
+ export function shouldReregisterRemote(scope, url) {
107
+ return remoteEntryByScope.get(scope) !== url;
108
+ }
109
+ export function markRemoteRegistered(scope, url) {
110
+ remoteEntryByScope.set(scope, url);
111
+ }
112
+ /** @internal tests */
113
+ export function resetRemoteRegistry() {
114
+ remoteEntryByScope.clear();
115
+ }
116
+ /**
117
+ * Safe no-op when `register()` returns void. Used by AddonLoader to treat
118
+ * both historical and fiber-style plugins uniformly.
119
+ */
120
+ export function disposableFromRegisterResult(result) {
121
+ return typeof result === 'function' ? result : undefined;
122
+ }
@@ -1,4 +1,4 @@
1
- import type { AddonAPI, AddonLayout } from '@asteby/metacore-sdk';
1
+ import type { AddonAPI, AddonLayout, Registry } from '@asteby/metacore-sdk';
2
2
  export interface AddonLoaderProps {
3
3
  /** Unique key of the addon — maps to the federation container name. */
4
4
  scope: string;
@@ -8,9 +8,25 @@ export interface AddonLoaderProps {
8
8
  module?: string;
9
9
  /** Host-provided API passed to the addon's register() call. */
10
10
  api: AddonAPI;
11
+ /**
12
+ * Host registry used to {@link Registry.unbind} this addon's contributions
13
+ * on dispose. Optional so legacy hosts keep compiling; without it, fiber
14
+ * remounts leak routes/actions.
15
+ */
16
+ hostRegistry?: Registry;
17
+ /**
18
+ * Addon key for SW cache purge. Defaults to `api.manifest.key`.
19
+ */
20
+ addonKey?: string;
21
+ /**
22
+ * Registry owner passed to {@link Registry.unbind}. Defaults to `addonKey`.
23
+ * Immersive `./plugin` fibers use `${key}::view` so they don't wipe the
24
+ * shell `./register` contributions of the same addon.
25
+ */
26
+ unbindKey?: string;
11
27
  /** Optional rendering while loading. */
12
28
  fallback?: React.ReactNode;
13
- /** Called once the addon has successfully registered. */
29
+ /** Called once the addon has successfully registered (including re-register). */
14
30
  onReady?: () => void;
15
31
  /** Called if loading fails. */
16
32
  onError?: (err: Error) => void;
@@ -28,5 +44,5 @@ export interface AddonLoaderProps {
28
44
  layout?: AddonLayout;
29
45
  children?: React.ReactNode;
30
46
  }
31
- export declare function AddonLoader({ scope, url, module, api, fallback, onReady, onError, layout, children, }: AddonLoaderProps): import("react").JSX.Element;
47
+ export declare function AddonLoader({ scope, url, module, api, hostRegistry, addonKey, unbindKey, fallback, onReady, onError, layout, children, }: AddonLoaderProps): import("react").JSX.Element;
32
48
  //# sourceMappingURL=addon-loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"addon-loader.d.ts","sourceRoot":"","sources":["../src/addon-loader.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAGjE,MAAM,WAAW,gBAAgB;IAC7B,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAA;IACb,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,GAAG,EAAE,QAAQ,CAAA;IACb,wCAAwC;IACxC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;IAC9B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAoFD,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,GAAG,EACH,MAAqB,EACrB,GAAG,EACH,QAAe,EACf,OAAO,EACP,OAAO,EACP,MAAM,EACN,QAAQ,GACX,EAAE,gBAAgB,+BA8ClB"}
1
+ {"version":3,"file":"addon-loader.d.ts","sourceRoot":"","sources":["../src/addon-loader.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAc3E,MAAM,WAAW,gBAAgB;IAC7B,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAA;IACb,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,GAAG,EAAE,QAAQ,CAAA;IACb;;;;OAIG;IACH,YAAY,CAAC,EAAE,QAAQ,CAAA;IACvB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;IAC9B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AA4DD,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,GAAG,EACH,MAAqB,EACrB,GAAG,EACH,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,QAAe,EACf,OAAO,EACP,OAAO,EACP,MAAM,EACN,QAAQ,GACX,EAAE,gBAAgB,+BA6DlB"}