@edynamix/mf-isolation 0.0.17 → 0.0.18

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.
package/README.md CHANGED
@@ -60,6 +60,21 @@ The Angular-aware plugin uses `:host-context([data-edyn-mf="<scope-id>"])` for E
60
60
 
61
61
  Run this plugin through Vite/PostCSS before Angular AOT compilation. Do not combine it with `scopeRemoteCss` in the same Vite configuration.
62
62
 
63
+ ## Host contract
64
+
65
+ The host passes an `IMfShellContext` to a module's `mount` function and expects an `IMfModuleLifecycle` export. Check the host version with `isSupportedMfHostContract`: it accepts `MF_MINIMUM_HOST_CONTRACT_VERSION` and every later version, so additive contract changes do not need a lockstep release of the host and all modules.
66
+
67
+ ```ts
68
+ import { type IMfShellContext, isSupportedMfHostContract, MF_MINIMUM_HOST_CONTRACT_VERSION } from '@edynamix/mf-isolation/host-contract';
69
+
70
+ export const mount = (element: HTMLElement, shellContext: IMfShellContext): void => {
71
+ if (!isSupportedMfHostContract(shellContext)) {
72
+ throw new Error(`This module requires host contract version ${MF_MINIMUM_HOST_CONTRACT_VERSION} or later.`);
73
+ }
74
+ // ...
75
+ };
76
+ ```
77
+
63
78
  ## Navigation manifest
64
79
 
65
80
  Declare module-owned navigation as validated data and emit `navigation.json` from the same Vite build:
@@ -176,4 +191,5 @@ export const mount = async (host: HTMLElement, context: IShellContext): Promise<
176
191
  - `@edynamix/mf-isolation`
177
192
  - `@edynamix/mf-isolation/angular`
178
193
  - `@edynamix/mf-isolation/angular-runtime`
194
+ - `@edynamix/mf-isolation/host-contract`
179
195
  - `@edynamix/mf-isolation/postcss`
@@ -0,0 +1,26 @@
1
+ export declare const MF_HOST_CONTRACT_VERSION: 1;
2
+ export declare const MF_MINIMUM_HOST_CONTRACT_VERSION = 1;
3
+ export type TMfThemeTokens = Record<string, string>;
4
+ export interface IMfModuleNavigationContext {
5
+ readonly groupId: number;
6
+ readonly dealerIds: readonly number[];
7
+ readonly dealerId?: number;
8
+ }
9
+ export interface IMfShellContext {
10
+ contractVersion: number;
11
+ dealer: string;
12
+ theme: TMfThemeTokens;
13
+ overlayRoot: HTMLElement;
14
+ styleRoot: ShadowRoot;
15
+ locale: string;
16
+ navigationContext?: IMfModuleNavigationContext;
17
+ refreshSession?: () => Promise<boolean>;
18
+ navigate: (path: string) => void;
19
+ }
20
+ export interface IMfModuleLifecycle<TShellContext extends IMfShellContext = IMfShellContext> {
21
+ preload: () => void | Promise<void>;
22
+ mount: (element: HTMLElement, context: TShellContext) => void | Promise<void>;
23
+ unmount: (element: HTMLElement) => void | Promise<void>;
24
+ }
25
+ export declare const isSupportedMfHostContract: (context: unknown) => context is IMfShellContext;
26
+ //# sourceMappingURL=host-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-contract.d.ts","sourceRoot":"","sources":["../src/host-contract.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,wBAAwB,EAAG,CAAU,CAAC;AACnD,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAElD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEpD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,cAAc,CAAC;IACtB,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,EAAE,UAAU,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,0BAA0B,CAAC;IAC/C,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB,CAAC,aAAa,SAAS,eAAe,GAAG,eAAe;IACzF,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,KAAK,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,OAAO,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED,eAAO,MAAM,yBAAyB,GAAI,SAAS,OAAO,KAAG,OAAO,IAAI,eAOvE,CAAC","sourcesContent":["// The contract between the eDynamix host and a hosted module. Remotes accept any host at or above the minimum\n// version, so additive changes ship without a lockstep release of the host and every module.\nexport const MF_HOST_CONTRACT_VERSION = 1 as const;\nexport const MF_MINIMUM_HOST_CONTRACT_VERSION = 1;\n\nexport type TMfThemeTokens = Record<string, string>;\n\nexport interface IMfModuleNavigationContext {\n readonly groupId: number;\n readonly dealerIds: readonly number[];\n readonly dealerId?: number;\n}\n\nexport interface IMfShellContext {\n contractVersion: number;\n dealer: string;\n theme: TMfThemeTokens;\n overlayRoot: HTMLElement;\n styleRoot: ShadowRoot;\n locale: string;\n navigationContext?: IMfModuleNavigationContext;\n refreshSession?: () => Promise<boolean>;\n navigate: (path: string) => void;\n}\n\nexport interface IMfModuleLifecycle<TShellContext extends IMfShellContext = IMfShellContext> {\n preload: () => void | Promise<void>;\n mount: (element: HTMLElement, context: TShellContext) => void | Promise<void>;\n unmount: (element: HTMLElement) => void | Promise<void>;\n}\n\nexport const isSupportedMfHostContract = (context: unknown): context is IMfShellContext => {\n if (typeof context !== 'object' || context === null || !('contractVersion' in context)) {\n return false;\n }\n\n const { contractVersion } = context;\n return typeof contractVersion === 'number' && Number.isSafeInteger(contractVersion) && contractVersion >= MF_MINIMUM_HOST_CONTRACT_VERSION;\n};\n"]}
@@ -0,0 +1,12 @@
1
+ // The contract between the eDynamix host and a hosted module. Remotes accept any host at or above the minimum
2
+ // version, so additive changes ship without a lockstep release of the host and every module.
3
+ export const MF_HOST_CONTRACT_VERSION = 1;
4
+ export const MF_MINIMUM_HOST_CONTRACT_VERSION = 1;
5
+ export const isSupportedMfHostContract = (context) => {
6
+ if (typeof context !== 'object' || context === null || !('contractVersion' in context)) {
7
+ return false;
8
+ }
9
+ const { contractVersion } = context;
10
+ return typeof contractVersion === 'number' && Number.isSafeInteger(contractVersion) && contractVersion >= MF_MINIMUM_HOST_CONTRACT_VERSION;
11
+ };
12
+ //# sourceMappingURL=host-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-contract.js","sourceRoot":"","sources":["../src/host-contract.ts"],"names":[],"mappings":"AAAA,8GAA8G;AAC9G,6FAA6F;AAC7F,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAU,CAAC;AACnD,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAAC,CAAC;AA4BlD,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,OAAgB,EAA8B,EAAE;IACxF,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,iBAAiB,IAAI,OAAO,CAAC,EAAE,CAAC;QACvF,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC;IACpC,OAAO,OAAO,eAAe,KAAK,QAAQ,IAAI,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,IAAI,eAAe,IAAI,gCAAgC,CAAC;AAC7I,CAAC,CAAC","sourcesContent":["// The contract between the eDynamix host and a hosted module. Remotes accept any host at or above the minimum\n// version, so additive changes ship without a lockstep release of the host and every module.\nexport const MF_HOST_CONTRACT_VERSION = 1 as const;\nexport const MF_MINIMUM_HOST_CONTRACT_VERSION = 1;\n\nexport type TMfThemeTokens = Record<string, string>;\n\nexport interface IMfModuleNavigationContext {\n readonly groupId: number;\n readonly dealerIds: readonly number[];\n readonly dealerId?: number;\n}\n\nexport interface IMfShellContext {\n contractVersion: number;\n dealer: string;\n theme: TMfThemeTokens;\n overlayRoot: HTMLElement;\n styleRoot: ShadowRoot;\n locale: string;\n navigationContext?: IMfModuleNavigationContext;\n refreshSession?: () => Promise<boolean>;\n navigate: (path: string) => void;\n}\n\nexport interface IMfModuleLifecycle<TShellContext extends IMfShellContext = IMfShellContext> {\n preload: () => void | Promise<void>;\n mount: (element: HTMLElement, context: TShellContext) => void | Promise<void>;\n unmount: (element: HTMLElement) => void | Promise<void>;\n}\n\nexport const isSupportedMfHostContract = (context: unknown): context is IMfShellContext => {\n if (typeof context !== 'object' || context === null || !('contractVersion' in context)) {\n return false;\n }\n\n const { contractVersion } = context;\n return typeof contractVersion === 'number' && Number.isSafeInteger(contractVersion) && contractVersion >= MF_MINIMUM_HOST_CONTRACT_VERSION;\n};\n"]}
@@ -5,7 +5,6 @@ export interface IMfNavigationCounterReference {
5
5
  tone?: TMfNavigationCounterTone;
6
6
  }
7
7
  export interface IMfNavigationItem {
8
- accessKey?: string;
9
8
  children?: readonly IMfNavigationItem[];
10
9
  counters?: readonly IMfNavigationCounterReference[];
11
10
  icon?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,8BAA8B,EAAG,CAAU,CAAC;AAMzD,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEpF,MAAM,WAAW,6BAA6B;IAC5C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,wBAAwB,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACxC,QAAQ,CAAC,EAAE,SAAS,6BAA6B,EAAE,CAAC;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,OAAO,8BAA8B,CAAC;IACvD,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;IACpE,QAAQ,EAAE,MAAM,CAAC;CAClB;AA8GD,eAAO,MAAM,kBAAkB,GAAI,OAAO,uBAAuB,KAAG,QAAQ,CAAC,uBAAuB,CAoCnG,CAAC","sourcesContent":["export const MF_NAVIGATION_CONTRACT_VERSION = 1 as const;\n\nconst IDENTIFIER_PATTERN = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;\nconst MODULE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\nconst RELATIVE_ROUTE_PATTERN = /^(?!\\/)(?!.*(?:^|\\/)\\.\\.?(?:\\/|$))[a-zA-Z0-9][a-zA-Z0-9/_-]*$/;\n\nexport type TMfNavigationCounterTone = 'danger' | 'neutral' | 'success' | 'warning';\n\nexport interface IMfNavigationCounterReference {\n id: string;\n tone?: TMfNavigationCounterTone;\n}\n\nexport interface IMfNavigationItem {\n accessKey?: string;\n children?: readonly IMfNavigationItem[];\n counters?: readonly IMfNavigationCounterReference[];\n icon?: string;\n id: string;\n label: string;\n order: number;\n path?: string;\n}\n\nexport interface IMfNavigationDefinition {\n applicationId: number;\n contractVersion: typeof MF_NAVIGATION_CONTRACT_VERSION;\n items: readonly IMfNavigationItem[];\n label: string;\n landingPath: string;\n moduleId: string;\n}\n\nexport interface IMfNavigationManifest extends IMfNavigationDefinition {\n revision: string;\n}\n\nconst assertPlainObject = (value: unknown, location: string): Record<string, unknown> => {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`${location} must be a plain object.`);\n }\n\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new Error(`${location} must be a plain object.`);\n }\n\n return value as Record<string, unknown>;\n};\n\nconst assertExactKeys = (value: Record<string, unknown>, allowedKeys: readonly string[], location: string): void => {\n const allowed = new Set(allowedKeys);\n const unknownKey = Object.keys(value).find((key) => !allowed.has(key));\n if (unknownKey) {\n throw new Error(`${location} contains unsupported property \"${unknownKey}\".`);\n }\n};\n\nconst assertNonEmptyString = (value: unknown, location: string): string => {\n if (typeof value !== 'string' || value.trim() !== value || value.length === 0) {\n throw new Error(`${location} must be a non-empty trimmed string.`);\n }\n\n return value;\n};\n\nconst assertIdentifier = (value: unknown, location: string): string => {\n const identifier = assertNonEmptyString(value, location);\n if (!IDENTIFIER_PATTERN.test(identifier)) {\n throw new Error(`${location} must be a stable lowercase identifier.`);\n }\n\n return identifier;\n};\n\nconst normalizeCounter = (value: unknown, location: string): IMfNavigationCounterReference => {\n const counter = assertPlainObject(value, location);\n assertExactKeys(counter, ['id', 'tone'], location);\n\n const id = assertIdentifier(counter.id, `${location}.id`);\n const tone = counter.tone;\n if (tone !== undefined && tone !== 'danger' && tone !== 'neutral' && tone !== 'success' && tone !== 'warning') {\n throw new Error(`${location}.tone is not supported.`);\n }\n\n return Object.freeze(tone === undefined ? { id } : { id, tone });\n};\n\nconst normalizeItems = (values: unknown, location: string, seenIds: Set<string>): readonly IMfNavigationItem[] => {\n if (!Array.isArray(values)) {\n throw new Error(`${location} must be an array.`);\n }\n\n const items = values.map((value, index) => {\n const itemLocation = `${location}[${index}]`;\n const item = assertPlainObject(value, itemLocation);\n assertExactKeys(item, ['accessKey', 'children', 'counters', 'icon', 'id', 'label', 'order', 'path'], itemLocation);\n\n const id = assertIdentifier(item.id, `${itemLocation}.id`);\n if (seenIds.has(id)) {\n throw new Error(`${itemLocation}.id must be unique within the navigation tree.`);\n }\n seenIds.add(id);\n\n const label = assertNonEmptyString(item.label, `${itemLocation}.label`);\n if (!Number.isSafeInteger(item.order) || (item.order as number) < 0) {\n throw new Error(`${itemLocation}.order must be a non-negative safe integer.`);\n }\n\n const path = item.path;\n if (path !== undefined && (typeof path !== 'string' || !RELATIVE_ROUTE_PATTERN.test(path))) {\n throw new Error(`${itemLocation}.path must be an origin-safe module-relative route.`);\n }\n\n const icon = item.icon === undefined ? undefined : assertIdentifier(item.icon, `${itemLocation}.icon`);\n const accessKey = item.accessKey === undefined ? undefined : assertIdentifier(item.accessKey, `${itemLocation}.accessKey`);\n\n if (item.counters !== undefined && !Array.isArray(item.counters)) {\n throw new Error(`${itemLocation}.counters must be an array.`);\n }\n const counters = item.counters?.map((counter, counterIndex) => normalizeCounter(counter, `${itemLocation}.counters[${counterIndex}]`));\n if (counters && new Set(counters.map((counter) => counter.id)).size !== counters.length) {\n throw new Error(`${itemLocation}.counters contains duplicate counter ids.`);\n }\n\n const children = item.children === undefined ? undefined : normalizeItems(item.children, `${itemLocation}.children`, seenIds);\n if (path === undefined && (!children || children.length === 0)) {\n throw new Error(`${itemLocation} must define a path or at least one child.`);\n }\n\n return Object.freeze({\n ...(accessKey === undefined ? {} : { accessKey }),\n ...(children === undefined ? {} : { children }),\n ...(counters === undefined ? {} : { counters }),\n ...(icon === undefined ? {} : { icon }),\n id,\n label,\n order: item.order as number,\n ...(path === undefined ? {} : { path }),\n });\n });\n\n return Object.freeze(items.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id)));\n};\n\nexport const defineMfNavigation = (value: IMfNavigationDefinition): Readonly<IMfNavigationDefinition> => {\n const definition = assertPlainObject(value, 'navigation');\n assertExactKeys(definition, ['applicationId', 'contractVersion', 'items', 'label', 'landingPath', 'moduleId'], 'navigation');\n\n if (definition.contractVersion !== MF_NAVIGATION_CONTRACT_VERSION) {\n throw new Error(`navigation.contractVersion must be ${MF_NAVIGATION_CONTRACT_VERSION}.`);\n }\n\n if (!Number.isSafeInteger(definition.applicationId) || (definition.applicationId as number) <= 0) {\n throw new Error('navigation.applicationId must be a positive safe integer.');\n }\n\n const moduleId = assertNonEmptyString(definition.moduleId, 'navigation.moduleId');\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('navigation.moduleId must be a lowercase module identifier.');\n }\n\n const label = assertNonEmptyString(definition.label, 'navigation.label');\n const landingPath = definition.landingPath;\n if (typeof landingPath !== 'string' || !RELATIVE_ROUTE_PATTERN.test(landingPath)) {\n throw new Error('navigation.landingPath must be an origin-safe module-relative route.');\n }\n\n const items = normalizeItems(definition.items, 'navigation.items', new Set());\n if (items.length === 0) {\n throw new Error('navigation.items must contain at least one item.');\n }\n\n return Object.freeze({\n applicationId: definition.applicationId as number,\n contractVersion: MF_NAVIGATION_CONTRACT_VERSION,\n items,\n label,\n landingPath,\n moduleId,\n });\n};\n"]}
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,8BAA8B,EAAG,CAAU,CAAC;AAMzD,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEpF,MAAM,WAAW,6BAA6B;IAC5C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,wBAAwB,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACxC,QAAQ,CAAC,EAAE,SAAS,6BAA6B,EAAE,CAAC;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,OAAO,8BAA8B,CAAC;IACvD,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;IACpE,QAAQ,EAAE,MAAM,CAAC;CAClB;AA4GD,eAAO,MAAM,kBAAkB,GAAI,OAAO,uBAAuB,KAAG,QAAQ,CAAC,uBAAuB,CAoCnG,CAAC","sourcesContent":["export const MF_NAVIGATION_CONTRACT_VERSION = 1 as const;\n\nconst IDENTIFIER_PATTERN = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;\nconst MODULE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\nconst RELATIVE_ROUTE_PATTERN = /^(?!\\/)(?!.*(?:^|\\/)\\.\\.?(?:\\/|$))[a-zA-Z0-9][a-zA-Z0-9/_-]*$/;\n\nexport type TMfNavigationCounterTone = 'danger' | 'neutral' | 'success' | 'warning';\n\nexport interface IMfNavigationCounterReference {\n id: string;\n tone?: TMfNavigationCounterTone;\n}\n\nexport interface IMfNavigationItem {\n children?: readonly IMfNavigationItem[];\n counters?: readonly IMfNavigationCounterReference[];\n icon?: string;\n id: string;\n label: string;\n order: number;\n path?: string;\n}\n\nexport interface IMfNavigationDefinition {\n applicationId: number;\n contractVersion: typeof MF_NAVIGATION_CONTRACT_VERSION;\n items: readonly IMfNavigationItem[];\n label: string;\n landingPath: string;\n moduleId: string;\n}\n\nexport interface IMfNavigationManifest extends IMfNavigationDefinition {\n revision: string;\n}\n\nconst assertPlainObject = (value: unknown, location: string): Record<string, unknown> => {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`${location} must be a plain object.`);\n }\n\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new Error(`${location} must be a plain object.`);\n }\n\n return value as Record<string, unknown>;\n};\n\nconst assertExactKeys = (value: Record<string, unknown>, allowedKeys: readonly string[], location: string): void => {\n const allowed = new Set(allowedKeys);\n const unknownKey = Object.keys(value).find((key) => !allowed.has(key));\n if (unknownKey) {\n throw new Error(`${location} contains unsupported property \"${unknownKey}\".`);\n }\n};\n\nconst assertNonEmptyString = (value: unknown, location: string): string => {\n if (typeof value !== 'string' || value.trim() !== value || value.length === 0) {\n throw new Error(`${location} must be a non-empty trimmed string.`);\n }\n\n return value;\n};\n\nconst assertIdentifier = (value: unknown, location: string): string => {\n const identifier = assertNonEmptyString(value, location);\n if (!IDENTIFIER_PATTERN.test(identifier)) {\n throw new Error(`${location} must be a stable lowercase identifier.`);\n }\n\n return identifier;\n};\n\nconst normalizeCounter = (value: unknown, location: string): IMfNavigationCounterReference => {\n const counter = assertPlainObject(value, location);\n assertExactKeys(counter, ['id', 'tone'], location);\n\n const id = assertIdentifier(counter.id, `${location}.id`);\n const tone = counter.tone;\n if (tone !== undefined && tone !== 'danger' && tone !== 'neutral' && tone !== 'success' && tone !== 'warning') {\n throw new Error(`${location}.tone is not supported.`);\n }\n\n return Object.freeze(tone === undefined ? { id } : { id, tone });\n};\n\nconst normalizeItems = (values: unknown, location: string, seenIds: Set<string>): readonly IMfNavigationItem[] => {\n if (!Array.isArray(values)) {\n throw new Error(`${location} must be an array.`);\n }\n\n const items = values.map((value, index) => {\n const itemLocation = `${location}[${index}]`;\n const item = assertPlainObject(value, itemLocation);\n assertExactKeys(item, ['children', 'counters', 'icon', 'id', 'label', 'order', 'path'], itemLocation);\n\n const id = assertIdentifier(item.id, `${itemLocation}.id`);\n if (seenIds.has(id)) {\n throw new Error(`${itemLocation}.id must be unique within the navigation tree.`);\n }\n seenIds.add(id);\n\n const label = assertNonEmptyString(item.label, `${itemLocation}.label`);\n if (!Number.isSafeInteger(item.order) || (item.order as number) < 0) {\n throw new Error(`${itemLocation}.order must be a non-negative safe integer.`);\n }\n\n const path = item.path;\n if (path !== undefined && (typeof path !== 'string' || !RELATIVE_ROUTE_PATTERN.test(path))) {\n throw new Error(`${itemLocation}.path must be an origin-safe module-relative route.`);\n }\n\n const icon = item.icon === undefined ? undefined : assertIdentifier(item.icon, `${itemLocation}.icon`);\n\n if (item.counters !== undefined && !Array.isArray(item.counters)) {\n throw new Error(`${itemLocation}.counters must be an array.`);\n }\n const counters = item.counters?.map((counter, counterIndex) => normalizeCounter(counter, `${itemLocation}.counters[${counterIndex}]`));\n if (counters && new Set(counters.map((counter) => counter.id)).size !== counters.length) {\n throw new Error(`${itemLocation}.counters contains duplicate counter ids.`);\n }\n\n const children = item.children === undefined ? undefined : normalizeItems(item.children, `${itemLocation}.children`, seenIds);\n if (path === undefined && (!children || children.length === 0)) {\n throw new Error(`${itemLocation} must define a path or at least one child.`);\n }\n\n return Object.freeze({\n ...(children === undefined ? {} : { children }),\n ...(counters === undefined ? {} : { counters }),\n ...(icon === undefined ? {} : { icon }),\n id,\n label,\n order: item.order as number,\n ...(path === undefined ? {} : { path }),\n });\n });\n\n return Object.freeze(items.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id)));\n};\n\nexport const defineMfNavigation = (value: IMfNavigationDefinition): Readonly<IMfNavigationDefinition> => {\n const definition = assertPlainObject(value, 'navigation');\n assertExactKeys(definition, ['applicationId', 'contractVersion', 'items', 'label', 'landingPath', 'moduleId'], 'navigation');\n\n if (definition.contractVersion !== MF_NAVIGATION_CONTRACT_VERSION) {\n throw new Error(`navigation.contractVersion must be ${MF_NAVIGATION_CONTRACT_VERSION}.`);\n }\n\n if (!Number.isSafeInteger(definition.applicationId) || (definition.applicationId as number) <= 0) {\n throw new Error('navigation.applicationId must be a positive safe integer.');\n }\n\n const moduleId = assertNonEmptyString(definition.moduleId, 'navigation.moduleId');\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('navigation.moduleId must be a lowercase module identifier.');\n }\n\n const label = assertNonEmptyString(definition.label, 'navigation.label');\n const landingPath = definition.landingPath;\n if (typeof landingPath !== 'string' || !RELATIVE_ROUTE_PATTERN.test(landingPath)) {\n throw new Error('navigation.landingPath must be an origin-safe module-relative route.');\n }\n\n const items = normalizeItems(definition.items, 'navigation.items', new Set());\n if (items.length === 0) {\n throw new Error('navigation.items must contain at least one item.');\n }\n\n return Object.freeze({\n applicationId: definition.applicationId as number,\n contractVersion: MF_NAVIGATION_CONTRACT_VERSION,\n items,\n label,\n landingPath,\n moduleId,\n });\n};\n"]}
@@ -49,7 +49,7 @@ const normalizeItems = (values, location, seenIds) => {
49
49
  const items = values.map((value, index) => {
50
50
  const itemLocation = `${location}[${index}]`;
51
51
  const item = assertPlainObject(value, itemLocation);
52
- assertExactKeys(item, ['accessKey', 'children', 'counters', 'icon', 'id', 'label', 'order', 'path'], itemLocation);
52
+ assertExactKeys(item, ['children', 'counters', 'icon', 'id', 'label', 'order', 'path'], itemLocation);
53
53
  const id = assertIdentifier(item.id, `${itemLocation}.id`);
54
54
  if (seenIds.has(id)) {
55
55
  throw new Error(`${itemLocation}.id must be unique within the navigation tree.`);
@@ -64,7 +64,6 @@ const normalizeItems = (values, location, seenIds) => {
64
64
  throw new Error(`${itemLocation}.path must be an origin-safe module-relative route.`);
65
65
  }
66
66
  const icon = item.icon === undefined ? undefined : assertIdentifier(item.icon, `${itemLocation}.icon`);
67
- const accessKey = item.accessKey === undefined ? undefined : assertIdentifier(item.accessKey, `${itemLocation}.accessKey`);
68
67
  if (item.counters !== undefined && !Array.isArray(item.counters)) {
69
68
  throw new Error(`${itemLocation}.counters must be an array.`);
70
69
  }
@@ -77,7 +76,6 @@ const normalizeItems = (values, location, seenIds) => {
77
76
  throw new Error(`${itemLocation} must define a path or at least one child.`);
78
77
  }
79
78
  return Object.freeze({
80
- ...(accessKey === undefined ? {} : { accessKey }),
81
79
  ...(children === undefined ? {} : { children }),
82
80
  ...(counters === undefined ? {} : { counters }),
83
81
  ...(icon === undefined ? {} : { icon }),
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"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","sourcesContent":["export const MF_NAVIGATION_CONTRACT_VERSION = 1 as const;\n\nconst IDENTIFIER_PATTERN = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;\nconst MODULE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\nconst RELATIVE_ROUTE_PATTERN = /^(?!\\/)(?!.*(?:^|\\/)\\.\\.?(?:\\/|$))[a-zA-Z0-9][a-zA-Z0-9/_-]*$/;\n\nexport type TMfNavigationCounterTone = 'danger' | 'neutral' | 'success' | 'warning';\n\nexport interface IMfNavigationCounterReference {\n id: string;\n tone?: TMfNavigationCounterTone;\n}\n\nexport interface IMfNavigationItem {\n accessKey?: string;\n children?: readonly IMfNavigationItem[];\n counters?: readonly IMfNavigationCounterReference[];\n icon?: string;\n id: string;\n label: string;\n order: number;\n path?: string;\n}\n\nexport interface IMfNavigationDefinition {\n applicationId: number;\n contractVersion: typeof MF_NAVIGATION_CONTRACT_VERSION;\n items: readonly IMfNavigationItem[];\n label: string;\n landingPath: string;\n moduleId: string;\n}\n\nexport interface IMfNavigationManifest extends IMfNavigationDefinition {\n revision: string;\n}\n\nconst assertPlainObject = (value: unknown, location: string): Record<string, unknown> => {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`${location} must be a plain object.`);\n }\n\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new Error(`${location} must be a plain object.`);\n }\n\n return value as Record<string, unknown>;\n};\n\nconst assertExactKeys = (value: Record<string, unknown>, allowedKeys: readonly string[], location: string): void => {\n const allowed = new Set(allowedKeys);\n const unknownKey = Object.keys(value).find((key) => !allowed.has(key));\n if (unknownKey) {\n throw new Error(`${location} contains unsupported property \"${unknownKey}\".`);\n }\n};\n\nconst assertNonEmptyString = (value: unknown, location: string): string => {\n if (typeof value !== 'string' || value.trim() !== value || value.length === 0) {\n throw new Error(`${location} must be a non-empty trimmed string.`);\n }\n\n return value;\n};\n\nconst assertIdentifier = (value: unknown, location: string): string => {\n const identifier = assertNonEmptyString(value, location);\n if (!IDENTIFIER_PATTERN.test(identifier)) {\n throw new Error(`${location} must be a stable lowercase identifier.`);\n }\n\n return identifier;\n};\n\nconst normalizeCounter = (value: unknown, location: string): IMfNavigationCounterReference => {\n const counter = assertPlainObject(value, location);\n assertExactKeys(counter, ['id', 'tone'], location);\n\n const id = assertIdentifier(counter.id, `${location}.id`);\n const tone = counter.tone;\n if (tone !== undefined && tone !== 'danger' && tone !== 'neutral' && tone !== 'success' && tone !== 'warning') {\n throw new Error(`${location}.tone is not supported.`);\n }\n\n return Object.freeze(tone === undefined ? { id } : { id, tone });\n};\n\nconst normalizeItems = (values: unknown, location: string, seenIds: Set<string>): readonly IMfNavigationItem[] => {\n if (!Array.isArray(values)) {\n throw new Error(`${location} must be an array.`);\n }\n\n const items = values.map((value, index) => {\n const itemLocation = `${location}[${index}]`;\n const item = assertPlainObject(value, itemLocation);\n assertExactKeys(item, ['accessKey', 'children', 'counters', 'icon', 'id', 'label', 'order', 'path'], itemLocation);\n\n const id = assertIdentifier(item.id, `${itemLocation}.id`);\n if (seenIds.has(id)) {\n throw new Error(`${itemLocation}.id must be unique within the navigation tree.`);\n }\n seenIds.add(id);\n\n const label = assertNonEmptyString(item.label, `${itemLocation}.label`);\n if (!Number.isSafeInteger(item.order) || (item.order as number) < 0) {\n throw new Error(`${itemLocation}.order must be a non-negative safe integer.`);\n }\n\n const path = item.path;\n if (path !== undefined && (typeof path !== 'string' || !RELATIVE_ROUTE_PATTERN.test(path))) {\n throw new Error(`${itemLocation}.path must be an origin-safe module-relative route.`);\n }\n\n const icon = item.icon === undefined ? undefined : assertIdentifier(item.icon, `${itemLocation}.icon`);\n const accessKey = item.accessKey === undefined ? undefined : assertIdentifier(item.accessKey, `${itemLocation}.accessKey`);\n\n if (item.counters !== undefined && !Array.isArray(item.counters)) {\n throw new Error(`${itemLocation}.counters must be an array.`);\n }\n const counters = item.counters?.map((counter, counterIndex) => normalizeCounter(counter, `${itemLocation}.counters[${counterIndex}]`));\n if (counters && new Set(counters.map((counter) => counter.id)).size !== counters.length) {\n throw new Error(`${itemLocation}.counters contains duplicate counter ids.`);\n }\n\n const children = item.children === undefined ? undefined : normalizeItems(item.children, `${itemLocation}.children`, seenIds);\n if (path === undefined && (!children || children.length === 0)) {\n throw new Error(`${itemLocation} must define a path or at least one child.`);\n }\n\n return Object.freeze({\n ...(accessKey === undefined ? {} : { accessKey }),\n ...(children === undefined ? {} : { children }),\n ...(counters === undefined ? {} : { counters }),\n ...(icon === undefined ? {} : { icon }),\n id,\n label,\n order: item.order as number,\n ...(path === undefined ? {} : { path }),\n });\n });\n\n return Object.freeze(items.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id)));\n};\n\nexport const defineMfNavigation = (value: IMfNavigationDefinition): Readonly<IMfNavigationDefinition> => {\n const definition = assertPlainObject(value, 'navigation');\n assertExactKeys(definition, ['applicationId', 'contractVersion', 'items', 'label', 'landingPath', 'moduleId'], 'navigation');\n\n if (definition.contractVersion !== MF_NAVIGATION_CONTRACT_VERSION) {\n throw new Error(`navigation.contractVersion must be ${MF_NAVIGATION_CONTRACT_VERSION}.`);\n }\n\n if (!Number.isSafeInteger(definition.applicationId) || (definition.applicationId as number) <= 0) {\n throw new Error('navigation.applicationId must be a positive safe integer.');\n }\n\n const moduleId = assertNonEmptyString(definition.moduleId, 'navigation.moduleId');\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('navigation.moduleId must be a lowercase module identifier.');\n }\n\n const label = assertNonEmptyString(definition.label, 'navigation.label');\n const landingPath = definition.landingPath;\n if (typeof landingPath !== 'string' || !RELATIVE_ROUTE_PATTERN.test(landingPath)) {\n throw new Error('navigation.landingPath must be an origin-safe module-relative route.');\n }\n\n const items = normalizeItems(definition.items, 'navigation.items', new Set());\n if (items.length === 0) {\n throw new Error('navigation.items must contain at least one item.');\n }\n\n return Object.freeze({\n applicationId: definition.applicationId as number,\n contractVersion: MF_NAVIGATION_CONTRACT_VERSION,\n items,\n label,\n landingPath,\n moduleId,\n });\n};\n"]}
1
+ {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"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","sourcesContent":["export const MF_NAVIGATION_CONTRACT_VERSION = 1 as const;\n\nconst IDENTIFIER_PATTERN = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;\nconst MODULE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\nconst RELATIVE_ROUTE_PATTERN = /^(?!\\/)(?!.*(?:^|\\/)\\.\\.?(?:\\/|$))[a-zA-Z0-9][a-zA-Z0-9/_-]*$/;\n\nexport type TMfNavigationCounterTone = 'danger' | 'neutral' | 'success' | 'warning';\n\nexport interface IMfNavigationCounterReference {\n id: string;\n tone?: TMfNavigationCounterTone;\n}\n\nexport interface IMfNavigationItem {\n children?: readonly IMfNavigationItem[];\n counters?: readonly IMfNavigationCounterReference[];\n icon?: string;\n id: string;\n label: string;\n order: number;\n path?: string;\n}\n\nexport interface IMfNavigationDefinition {\n applicationId: number;\n contractVersion: typeof MF_NAVIGATION_CONTRACT_VERSION;\n items: readonly IMfNavigationItem[];\n label: string;\n landingPath: string;\n moduleId: string;\n}\n\nexport interface IMfNavigationManifest extends IMfNavigationDefinition {\n revision: string;\n}\n\nconst assertPlainObject = (value: unknown, location: string): Record<string, unknown> => {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`${location} must be a plain object.`);\n }\n\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new Error(`${location} must be a plain object.`);\n }\n\n return value as Record<string, unknown>;\n};\n\nconst assertExactKeys = (value: Record<string, unknown>, allowedKeys: readonly string[], location: string): void => {\n const allowed = new Set(allowedKeys);\n const unknownKey = Object.keys(value).find((key) => !allowed.has(key));\n if (unknownKey) {\n throw new Error(`${location} contains unsupported property \"${unknownKey}\".`);\n }\n};\n\nconst assertNonEmptyString = (value: unknown, location: string): string => {\n if (typeof value !== 'string' || value.trim() !== value || value.length === 0) {\n throw new Error(`${location} must be a non-empty trimmed string.`);\n }\n\n return value;\n};\n\nconst assertIdentifier = (value: unknown, location: string): string => {\n const identifier = assertNonEmptyString(value, location);\n if (!IDENTIFIER_PATTERN.test(identifier)) {\n throw new Error(`${location} must be a stable lowercase identifier.`);\n }\n\n return identifier;\n};\n\nconst normalizeCounter = (value: unknown, location: string): IMfNavigationCounterReference => {\n const counter = assertPlainObject(value, location);\n assertExactKeys(counter, ['id', 'tone'], location);\n\n const id = assertIdentifier(counter.id, `${location}.id`);\n const tone = counter.tone;\n if (tone !== undefined && tone !== 'danger' && tone !== 'neutral' && tone !== 'success' && tone !== 'warning') {\n throw new Error(`${location}.tone is not supported.`);\n }\n\n return Object.freeze(tone === undefined ? { id } : { id, tone });\n};\n\nconst normalizeItems = (values: unknown, location: string, seenIds: Set<string>): readonly IMfNavigationItem[] => {\n if (!Array.isArray(values)) {\n throw new Error(`${location} must be an array.`);\n }\n\n const items = values.map((value, index) => {\n const itemLocation = `${location}[${index}]`;\n const item = assertPlainObject(value, itemLocation);\n assertExactKeys(item, ['children', 'counters', 'icon', 'id', 'label', 'order', 'path'], itemLocation);\n\n const id = assertIdentifier(item.id, `${itemLocation}.id`);\n if (seenIds.has(id)) {\n throw new Error(`${itemLocation}.id must be unique within the navigation tree.`);\n }\n seenIds.add(id);\n\n const label = assertNonEmptyString(item.label, `${itemLocation}.label`);\n if (!Number.isSafeInteger(item.order) || (item.order as number) < 0) {\n throw new Error(`${itemLocation}.order must be a non-negative safe integer.`);\n }\n\n const path = item.path;\n if (path !== undefined && (typeof path !== 'string' || !RELATIVE_ROUTE_PATTERN.test(path))) {\n throw new Error(`${itemLocation}.path must be an origin-safe module-relative route.`);\n }\n\n const icon = item.icon === undefined ? undefined : assertIdentifier(item.icon, `${itemLocation}.icon`);\n\n if (item.counters !== undefined && !Array.isArray(item.counters)) {\n throw new Error(`${itemLocation}.counters must be an array.`);\n }\n const counters = item.counters?.map((counter, counterIndex) => normalizeCounter(counter, `${itemLocation}.counters[${counterIndex}]`));\n if (counters && new Set(counters.map((counter) => counter.id)).size !== counters.length) {\n throw new Error(`${itemLocation}.counters contains duplicate counter ids.`);\n }\n\n const children = item.children === undefined ? undefined : normalizeItems(item.children, `${itemLocation}.children`, seenIds);\n if (path === undefined && (!children || children.length === 0)) {\n throw new Error(`${itemLocation} must define a path or at least one child.`);\n }\n\n return Object.freeze({\n ...(children === undefined ? {} : { children }),\n ...(counters === undefined ? {} : { counters }),\n ...(icon === undefined ? {} : { icon }),\n id,\n label,\n order: item.order as number,\n ...(path === undefined ? {} : { path }),\n });\n });\n\n return Object.freeze(items.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id)));\n};\n\nexport const defineMfNavigation = (value: IMfNavigationDefinition): Readonly<IMfNavigationDefinition> => {\n const definition = assertPlainObject(value, 'navigation');\n assertExactKeys(definition, ['applicationId', 'contractVersion', 'items', 'label', 'landingPath', 'moduleId'], 'navigation');\n\n if (definition.contractVersion !== MF_NAVIGATION_CONTRACT_VERSION) {\n throw new Error(`navigation.contractVersion must be ${MF_NAVIGATION_CONTRACT_VERSION}.`);\n }\n\n if (!Number.isSafeInteger(definition.applicationId) || (definition.applicationId as number) <= 0) {\n throw new Error('navigation.applicationId must be a positive safe integer.');\n }\n\n const moduleId = assertNonEmptyString(definition.moduleId, 'navigation.moduleId');\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('navigation.moduleId must be a lowercase module identifier.');\n }\n\n const label = assertNonEmptyString(definition.label, 'navigation.label');\n const landingPath = definition.landingPath;\n if (typeof landingPath !== 'string' || !RELATIVE_ROUTE_PATTERN.test(landingPath)) {\n throw new Error('navigation.landingPath must be an origin-safe module-relative route.');\n }\n\n const items = normalizeItems(definition.items, 'navigation.items', new Set());\n if (items.length === 0) {\n throw new Error('navigation.items must contain at least one item.');\n }\n\n return Object.freeze({\n applicationId: definition.applicationId as number,\n contractVersion: MF_NAVIGATION_CONTRACT_VERSION,\n items,\n label,\n landingPath,\n moduleId,\n });\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edynamix/mf-isolation",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "CSS and overlay isolation helpers for eDynamix microfrontends.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -33,6 +33,10 @@
33
33
  "types": "./dist/postcss.d.ts",
34
34
  "import": "./dist/postcss.js"
35
35
  },
36
+ "./host-contract": {
37
+ "types": "./dist/host-contract.d.ts",
38
+ "import": "./dist/host-contract.js"
39
+ },
36
40
  "./navigation": {
37
41
  "types": "./dist/navigation.d.ts",
38
42
  "import": "./dist/navigation.js"