@edynamix/mf-isolation 0.0.7 → 0.0.8

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,41 @@ 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
+ ## Navigation manifest
64
+
65
+ Declare module-owned navigation as validated data and emit `navigation.json` from the same Vite build:
66
+
67
+ ```ts
68
+ import {
69
+ defineMfNavigation,
70
+ MF_NAVIGATION_CONTRACT_VERSION,
71
+ } from '@edynamix/mf-isolation/navigation';
72
+ import { mfNavigationPlugin } from '@edynamix/mf-isolation/vite';
73
+ import { defineConfig } from 'vite';
74
+
75
+ const navigation = defineMfNavigation({
76
+ applicationId: 22,
77
+ contractVersion: MF_NAVIGATION_CONTRACT_VERSION,
78
+ items: [
79
+ {
80
+ id: 'stockmaster.for-sale',
81
+ label: 'For Sale',
82
+ order: 1,
83
+ path: 'usedstock/for-sale',
84
+ },
85
+ ],
86
+ label: 'Stock Master',
87
+ landingPath: 'usedstock',
88
+ moduleId: 'stockmaster',
89
+ });
90
+
91
+ export default defineConfig({
92
+ plugins: [mfNavigationPlugin({ navigation })],
93
+ });
94
+ ```
95
+
96
+ Routes are module-relative and contain no leading slash. The plugin validates the complete tree, emits deterministic JSON with a SHA-256 revision during builds, and serves the same manifest with `Cache-Control: no-cache` in development.
97
+
63
98
  ## Angular CDK overlays
64
99
 
65
100
  The Angular integration targets Angular/CDK 19.x. In a hosted application, install the complete provider array so global and connected overlays share the module boundary's viewport and coordinate system:
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export declare const MF_SCOPE_ATTRIBUTE = "data-edyn-mf";
2
2
  export declare const MF_ROOT_SELECTOR = ":mf-root";
3
+ export * from './navigation';
3
4
  export interface IPrepareMfBoundaryOptions {
4
5
  scopeId: string;
5
6
  theme?: Record<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAI3C,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,KAAG,MAMnD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAEpD,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,SAAS,WAAW,EAAE,oBAAyB,yBAAyB,KAAG,CAAC,MAAM,IAAI,CA2CvH,CAAC","sourcesContent":["export const MF_SCOPE_ATTRIBUTE = 'data-edyn-mf';\nexport const MF_ROOT_SELECTOR = ':mf-root';\n\nconst SCOPE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\n\nexport interface IPrepareMfBoundaryOptions {\n scopeId: string;\n theme?: Record<string, string>;\n}\n\nexport const validateMfScopeId = (scopeId: string): string => {\n if (!SCOPE_ID_PATTERN.test(scopeId)) {\n throw new Error(`Invalid microfrontend scope id: \"${scopeId}\".`);\n }\n\n return scopeId;\n};\n\nexport const getMfScopeSelector = (scopeId: string): string => {\n return `:where([${MF_SCOPE_ATTRIBUTE}=\"${validateMfScopeId(scopeId)}\"])`;\n};\n\nexport const prepareMfBoundary = (element: HTMLElement, { scopeId, theme = {} }: IPrepareMfBoundaryOptions): (() => void) => {\n validateMfScopeId(scopeId);\n\n for (const name of Object.keys(theme)) {\n if (!name.startsWith('--')) {\n throw new Error(`Microfrontend theme token must be a CSS custom property: \"${name}\".`);\n }\n }\n\n const previousScope = element.getAttribute(MF_SCOPE_ATTRIBUTE);\n const hadRootClass = element.classList.contains('edx-root');\n const previousTheme = new Map<string, { priority: string; value: string }>();\n\n element.setAttribute(MF_SCOPE_ATTRIBUTE, scopeId);\n element.classList.add('edx-root');\n\n for (const [name, value] of Object.entries(theme)) {\n previousTheme.set(name, {\n priority: element.style.getPropertyPriority(name),\n value: element.style.getPropertyValue(name),\n });\n element.style.setProperty(name, value);\n }\n\n return () => {\n if (previousScope === null) {\n element.removeAttribute(MF_SCOPE_ATTRIBUTE);\n } else {\n element.setAttribute(MF_SCOPE_ATTRIBUTE, previousScope);\n }\n\n if (!hadRootClass) {\n element.classList.remove('edx-root');\n }\n\n for (const [name, previous] of previousTheme) {\n if (previous.value) {\n element.style.setProperty(name, previous.value, previous.priority);\n } else {\n element.style.removeProperty(name);\n }\n }\n };\n};\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAE3C,cAAc,cAAc,CAAC;AAI7B,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,KAAG,MAMnD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAEpD,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,SAAS,WAAW,EAAE,oBAAyB,yBAAyB,KAAG,CAAC,MAAM,IAAI,CA2CvH,CAAC","sourcesContent":["export const MF_SCOPE_ATTRIBUTE = 'data-edyn-mf';\nexport const MF_ROOT_SELECTOR = ':mf-root';\n\nexport * from './navigation';\n\nconst SCOPE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\n\nexport interface IPrepareMfBoundaryOptions {\n scopeId: string;\n theme?: Record<string, string>;\n}\n\nexport const validateMfScopeId = (scopeId: string): string => {\n if (!SCOPE_ID_PATTERN.test(scopeId)) {\n throw new Error(`Invalid microfrontend scope id: \"${scopeId}\".`);\n }\n\n return scopeId;\n};\n\nexport const getMfScopeSelector = (scopeId: string): string => {\n return `:where([${MF_SCOPE_ATTRIBUTE}=\"${validateMfScopeId(scopeId)}\"])`;\n};\n\nexport const prepareMfBoundary = (element: HTMLElement, { scopeId, theme = {} }: IPrepareMfBoundaryOptions): (() => void) => {\n validateMfScopeId(scopeId);\n\n for (const name of Object.keys(theme)) {\n if (!name.startsWith('--')) {\n throw new Error(`Microfrontend theme token must be a CSS custom property: \"${name}\".`);\n }\n }\n\n const previousScope = element.getAttribute(MF_SCOPE_ATTRIBUTE);\n const hadRootClass = element.classList.contains('edx-root');\n const previousTheme = new Map<string, { priority: string; value: string }>();\n\n element.setAttribute(MF_SCOPE_ATTRIBUTE, scopeId);\n element.classList.add('edx-root');\n\n for (const [name, value] of Object.entries(theme)) {\n previousTheme.set(name, {\n priority: element.style.getPropertyPriority(name),\n value: element.style.getPropertyValue(name),\n });\n element.style.setProperty(name, value);\n }\n\n return () => {\n if (previousScope === null) {\n element.removeAttribute(MF_SCOPE_ATTRIBUTE);\n } else {\n element.setAttribute(MF_SCOPE_ATTRIBUTE, previousScope);\n }\n\n if (!hadRootClass) {\n element.classList.remove('edx-root');\n }\n\n for (const [name, previous] of previousTheme) {\n if (previous.value) {\n element.style.setProperty(name, previous.value, previous.priority);\n } else {\n element.style.removeProperty(name);\n }\n }\n };\n};\n"]}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export const MF_SCOPE_ATTRIBUTE = 'data-edyn-mf';
2
2
  export const MF_ROOT_SELECTOR = ':mf-root';
3
+ export * from './navigation';
3
4
  const SCOPE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
4
5
  export const validateMfScopeId = (scopeId) => {
5
6
  if (!SCOPE_ID_PATTERN.test(scopeId)) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,kBAAkB,GAAG,cAAc,CAAC;AACjD,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAC;AAE3C,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAO7C,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAe,EAAU,EAAE;IAC3D,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,oCAAoC,OAAO,IAAI,CAAC,CAAC;IACnE,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,OAAe,EAAU,EAAE;IAC5D,OAAO,WAAW,kBAAkB,KAAK,iBAAiB,CAAC,OAAO,CAAC,KAAK,CAAC;AAC3E,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAoB,EAAE,EAAE,OAAO,EAAE,KAAK,GAAG,EAAE,EAA6B,EAAgB,EAAE;IAC1H,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAE3B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,6DAA6D,IAAI,IAAI,CAAC,CAAC;QACzF,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,kBAAkB,CAAC,CAAC;IAC/D,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC5D,MAAM,aAAa,GAAG,IAAI,GAAG,EAA+C,CAAC;IAE7E,OAAO,CAAC,YAAY,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;IAClD,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAElC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAClD,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE;YACtB,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,CAAC;YACjD,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC;SAC5C,CAAC,CAAC;QACH,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;IAED,OAAO,GAAG,EAAE;QACV,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;YAC3B,OAAO,CAAC,eAAe,CAAC,kBAAkB,CAAC,CAAC;QAC9C,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,YAAY,CAAC,kBAAkB,EAAE,aAAa,CAAC,CAAC;QAC1D,CAAC;QAED,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvC,CAAC;QAED,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,aAAa,EAAE,CAAC;YAC7C,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;gBACnB,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACrE,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;IACH,CAAC,CAAC;AACJ,CAAC,CAAC","sourcesContent":["export const MF_SCOPE_ATTRIBUTE = 'data-edyn-mf';\nexport const MF_ROOT_SELECTOR = ':mf-root';\n\nconst SCOPE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\n\nexport interface IPrepareMfBoundaryOptions {\n scopeId: string;\n theme?: Record<string, string>;\n}\n\nexport const validateMfScopeId = (scopeId: string): string => {\n if (!SCOPE_ID_PATTERN.test(scopeId)) {\n throw new Error(`Invalid microfrontend scope id: \"${scopeId}\".`);\n }\n\n return scopeId;\n};\n\nexport const getMfScopeSelector = (scopeId: string): string => {\n return `:where([${MF_SCOPE_ATTRIBUTE}=\"${validateMfScopeId(scopeId)}\"])`;\n};\n\nexport const prepareMfBoundary = (element: HTMLElement, { scopeId, theme = {} }: IPrepareMfBoundaryOptions): (() => void) => {\n validateMfScopeId(scopeId);\n\n for (const name of Object.keys(theme)) {\n if (!name.startsWith('--')) {\n throw new Error(`Microfrontend theme token must be a CSS custom property: \"${name}\".`);\n }\n }\n\n const previousScope = element.getAttribute(MF_SCOPE_ATTRIBUTE);\n const hadRootClass = element.classList.contains('edx-root');\n const previousTheme = new Map<string, { priority: string; value: string }>();\n\n element.setAttribute(MF_SCOPE_ATTRIBUTE, scopeId);\n element.classList.add('edx-root');\n\n for (const [name, value] of Object.entries(theme)) {\n previousTheme.set(name, {\n priority: element.style.getPropertyPriority(name),\n value: element.style.getPropertyValue(name),\n });\n element.style.setProperty(name, value);\n }\n\n return () => {\n if (previousScope === null) {\n element.removeAttribute(MF_SCOPE_ATTRIBUTE);\n } else {\n element.setAttribute(MF_SCOPE_ATTRIBUTE, previousScope);\n }\n\n if (!hadRootClass) {\n element.classList.remove('edx-root');\n }\n\n for (const [name, previous] of previousTheme) {\n if (previous.value) {\n element.style.setProperty(name, previous.value, previous.priority);\n } else {\n element.style.removeProperty(name);\n }\n }\n };\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,kBAAkB,GAAG,cAAc,CAAC;AACjD,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAC;AAE3C,cAAc,cAAc,CAAC;AAE7B,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAO7C,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAe,EAAU,EAAE;IAC3D,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,oCAAoC,OAAO,IAAI,CAAC,CAAC;IACnE,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,OAAe,EAAU,EAAE;IAC5D,OAAO,WAAW,kBAAkB,KAAK,iBAAiB,CAAC,OAAO,CAAC,KAAK,CAAC;AAC3E,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAoB,EAAE,EAAE,OAAO,EAAE,KAAK,GAAG,EAAE,EAA6B,EAAgB,EAAE;IAC1H,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAE3B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,6DAA6D,IAAI,IAAI,CAAC,CAAC;QACzF,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,kBAAkB,CAAC,CAAC;IAC/D,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC5D,MAAM,aAAa,GAAG,IAAI,GAAG,EAA+C,CAAC;IAE7E,OAAO,CAAC,YAAY,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;IAClD,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAElC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAClD,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE;YACtB,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,CAAC;YACjD,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC;SAC5C,CAAC,CAAC;QACH,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;IAED,OAAO,GAAG,EAAE;QACV,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;YAC3B,OAAO,CAAC,eAAe,CAAC,kBAAkB,CAAC,CAAC;QAC9C,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,YAAY,CAAC,kBAAkB,EAAE,aAAa,CAAC,CAAC;QAC1D,CAAC;QAED,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvC,CAAC;QAED,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,aAAa,EAAE,CAAC;YAC7C,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;gBACnB,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACrE,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;IACH,CAAC,CAAC;AACJ,CAAC,CAAC","sourcesContent":["export const MF_SCOPE_ATTRIBUTE = 'data-edyn-mf';\nexport const MF_ROOT_SELECTOR = ':mf-root';\n\nexport * from './navigation';\n\nconst SCOPE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\n\nexport interface IPrepareMfBoundaryOptions {\n scopeId: string;\n theme?: Record<string, string>;\n}\n\nexport const validateMfScopeId = (scopeId: string): string => {\n if (!SCOPE_ID_PATTERN.test(scopeId)) {\n throw new Error(`Invalid microfrontend scope id: \"${scopeId}\".`);\n }\n\n return scopeId;\n};\n\nexport const getMfScopeSelector = (scopeId: string): string => {\n return `:where([${MF_SCOPE_ATTRIBUTE}=\"${validateMfScopeId(scopeId)}\"])`;\n};\n\nexport const prepareMfBoundary = (element: HTMLElement, { scopeId, theme = {} }: IPrepareMfBoundaryOptions): (() => void) => {\n validateMfScopeId(scopeId);\n\n for (const name of Object.keys(theme)) {\n if (!name.startsWith('--')) {\n throw new Error(`Microfrontend theme token must be a CSS custom property: \"${name}\".`);\n }\n }\n\n const previousScope = element.getAttribute(MF_SCOPE_ATTRIBUTE);\n const hadRootClass = element.classList.contains('edx-root');\n const previousTheme = new Map<string, { priority: string; value: string }>();\n\n element.setAttribute(MF_SCOPE_ATTRIBUTE, scopeId);\n element.classList.add('edx-root');\n\n for (const [name, value] of Object.entries(theme)) {\n previousTheme.set(name, {\n priority: element.style.getPropertyPriority(name),\n value: element.style.getPropertyValue(name),\n });\n element.style.setProperty(name, value);\n }\n\n return () => {\n if (previousScope === null) {\n element.removeAttribute(MF_SCOPE_ATTRIBUTE);\n } else {\n element.setAttribute(MF_SCOPE_ATTRIBUTE, previousScope);\n }\n\n if (!hadRootClass) {\n element.classList.remove('edx-root');\n }\n\n for (const [name, previous] of previousTheme) {\n if (previous.value) {\n element.style.setProperty(name, previous.value, previous.priority);\n } else {\n element.style.removeProperty(name);\n }\n }\n };\n};\n"]}
@@ -0,0 +1,29 @@
1
+ export declare const MF_NAVIGATION_CONTRACT_VERSION: 1;
2
+ export type TMfNavigationCounterTone = 'danger' | 'neutral' | 'success' | 'warning';
3
+ export interface IMfNavigationCounterReference {
4
+ id: string;
5
+ tone?: TMfNavigationCounterTone;
6
+ }
7
+ export interface IMfNavigationItem {
8
+ accessKey?: string;
9
+ children?: readonly IMfNavigationItem[];
10
+ counters?: readonly IMfNavigationCounterReference[];
11
+ icon?: string;
12
+ id: string;
13
+ label: string;
14
+ order: number;
15
+ path?: string;
16
+ }
17
+ export interface IMfNavigationDefinition {
18
+ applicationId: number;
19
+ contractVersion: typeof MF_NAVIGATION_CONTRACT_VERSION;
20
+ items: readonly IMfNavigationItem[];
21
+ label: string;
22
+ landingPath: string;
23
+ moduleId: string;
24
+ }
25
+ export interface IMfNavigationManifest extends IMfNavigationDefinition {
26
+ revision: string;
27
+ }
28
+ export declare const defineMfNavigation: (value: IMfNavigationDefinition) => Readonly<IMfNavigationDefinition>;
29
+ //# sourceMappingURL=navigation.d.ts.map
@@ -0,0 +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"]}
@@ -0,0 +1,123 @@
1
+ export const MF_NAVIGATION_CONTRACT_VERSION = 1;
2
+ const IDENTIFIER_PATTERN = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
3
+ const MODULE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
4
+ const RELATIVE_ROUTE_PATTERN = /^(?!\/)(?!.*(?:^|\/)\.\.?(?:\/|$))[a-zA-Z0-9][a-zA-Z0-9/_-]*$/;
5
+ const assertPlainObject = (value, location) => {
6
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
7
+ throw new Error(`${location} must be a plain object.`);
8
+ }
9
+ const prototype = Object.getPrototypeOf(value);
10
+ if (prototype !== Object.prototype && prototype !== null) {
11
+ throw new Error(`${location} must be a plain object.`);
12
+ }
13
+ return value;
14
+ };
15
+ const assertExactKeys = (value, allowedKeys, location) => {
16
+ const allowed = new Set(allowedKeys);
17
+ const unknownKey = Object.keys(value).find((key) => !allowed.has(key));
18
+ if (unknownKey) {
19
+ throw new Error(`${location} contains unsupported property "${unknownKey}".`);
20
+ }
21
+ };
22
+ const assertNonEmptyString = (value, location) => {
23
+ if (typeof value !== 'string' || value.trim() !== value || value.length === 0) {
24
+ throw new Error(`${location} must be a non-empty trimmed string.`);
25
+ }
26
+ return value;
27
+ };
28
+ const assertIdentifier = (value, location) => {
29
+ const identifier = assertNonEmptyString(value, location);
30
+ if (!IDENTIFIER_PATTERN.test(identifier)) {
31
+ throw new Error(`${location} must be a stable lowercase identifier.`);
32
+ }
33
+ return identifier;
34
+ };
35
+ const normalizeCounter = (value, location) => {
36
+ const counter = assertPlainObject(value, location);
37
+ assertExactKeys(counter, ['id', 'tone'], location);
38
+ const id = assertIdentifier(counter.id, `${location}.id`);
39
+ const tone = counter.tone;
40
+ if (tone !== undefined && tone !== 'danger' && tone !== 'neutral' && tone !== 'success' && tone !== 'warning') {
41
+ throw new Error(`${location}.tone is not supported.`);
42
+ }
43
+ return Object.freeze(tone === undefined ? { id } : { id, tone });
44
+ };
45
+ const normalizeItems = (values, location, seenIds) => {
46
+ if (!Array.isArray(values)) {
47
+ throw new Error(`${location} must be an array.`);
48
+ }
49
+ const items = values.map((value, index) => {
50
+ const itemLocation = `${location}[${index}]`;
51
+ const item = assertPlainObject(value, itemLocation);
52
+ assertExactKeys(item, ['accessKey', 'children', 'counters', 'icon', 'id', 'label', 'order', 'path'], itemLocation);
53
+ const id = assertIdentifier(item.id, `${itemLocation}.id`);
54
+ if (seenIds.has(id)) {
55
+ throw new Error(`${itemLocation}.id must be unique within the navigation tree.`);
56
+ }
57
+ seenIds.add(id);
58
+ const label = assertNonEmptyString(item.label, `${itemLocation}.label`);
59
+ if (!Number.isSafeInteger(item.order) || item.order < 0) {
60
+ throw new Error(`${itemLocation}.order must be a non-negative safe integer.`);
61
+ }
62
+ const path = item.path;
63
+ if (path !== undefined && (typeof path !== 'string' || !RELATIVE_ROUTE_PATTERN.test(path))) {
64
+ throw new Error(`${itemLocation}.path must be an origin-safe module-relative route.`);
65
+ }
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
+ if (item.counters !== undefined && !Array.isArray(item.counters)) {
69
+ throw new Error(`${itemLocation}.counters must be an array.`);
70
+ }
71
+ const counters = item.counters?.map((counter, counterIndex) => normalizeCounter(counter, `${itemLocation}.counters[${counterIndex}]`));
72
+ if (counters && new Set(counters.map((counter) => counter.id)).size !== counters.length) {
73
+ throw new Error(`${itemLocation}.counters contains duplicate counter ids.`);
74
+ }
75
+ const children = item.children === undefined ? undefined : normalizeItems(item.children, `${itemLocation}.children`, seenIds);
76
+ if (path === undefined && (!children || children.length === 0)) {
77
+ throw new Error(`${itemLocation} must define a path or at least one child.`);
78
+ }
79
+ return Object.freeze({
80
+ ...(accessKey === undefined ? {} : { accessKey }),
81
+ ...(children === undefined ? {} : { children }),
82
+ ...(counters === undefined ? {} : { counters }),
83
+ ...(icon === undefined ? {} : { icon }),
84
+ id,
85
+ label,
86
+ order: item.order,
87
+ ...(path === undefined ? {} : { path }),
88
+ });
89
+ });
90
+ return Object.freeze(items.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id)));
91
+ };
92
+ export const defineMfNavigation = (value) => {
93
+ const definition = assertPlainObject(value, 'navigation');
94
+ assertExactKeys(definition, ['applicationId', 'contractVersion', 'items', 'label', 'landingPath', 'moduleId'], 'navigation');
95
+ if (definition.contractVersion !== MF_NAVIGATION_CONTRACT_VERSION) {
96
+ throw new Error(`navigation.contractVersion must be ${MF_NAVIGATION_CONTRACT_VERSION}.`);
97
+ }
98
+ if (!Number.isSafeInteger(definition.applicationId) || definition.applicationId <= 0) {
99
+ throw new Error('navigation.applicationId must be a positive safe integer.');
100
+ }
101
+ const moduleId = assertNonEmptyString(definition.moduleId, 'navigation.moduleId');
102
+ if (!MODULE_ID_PATTERN.test(moduleId)) {
103
+ throw new Error('navigation.moduleId must be a lowercase module identifier.');
104
+ }
105
+ const label = assertNonEmptyString(definition.label, 'navigation.label');
106
+ const landingPath = definition.landingPath;
107
+ if (typeof landingPath !== 'string' || !RELATIVE_ROUTE_PATTERN.test(landingPath)) {
108
+ throw new Error('navigation.landingPath must be an origin-safe module-relative route.');
109
+ }
110
+ const items = normalizeItems(definition.items, 'navigation.items', new Set());
111
+ if (items.length === 0) {
112
+ throw new Error('navigation.items must contain at least one item.');
113
+ }
114
+ return Object.freeze({
115
+ applicationId: definition.applicationId,
116
+ contractVersion: MF_NAVIGATION_CONTRACT_VERSION,
117
+ items,
118
+ label,
119
+ landingPath,
120
+ moduleId,
121
+ });
122
+ };
123
+ //# sourceMappingURL=navigation.js.map
@@ -0,0 +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"]}
package/dist/vite.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { Plugin } from 'vite';
2
+ import { type IMfNavigationDefinition, type IMfNavigationManifest } from './navigation';
3
+ export interface IMfNavigationPluginOptions {
4
+ fileName?: string;
5
+ navigation: IMfNavigationDefinition;
6
+ }
7
+ export declare const createMfNavigationManifest: (navigation: IMfNavigationDefinition) => Readonly<IMfNavigationManifest>;
8
+ export declare const serializeMfNavigationManifest: (navigation: IMfNavigationDefinition) => string;
9
+ export declare const mfNavigationPlugin: ({ fileName, navigation }: IMfNavigationPluginOptions) => Plugin;
10
+ //# sourceMappingURL=vite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAAsB,KAAK,uBAAuB,EAAE,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAK5G,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,uBAAuB,CAAC;CACrC;AAID,eAAO,MAAM,0BAA0B,GAAI,YAAY,uBAAuB,KAAG,QAAQ,CAAC,qBAAqB,CAa9G,CAAC;AAEF,eAAO,MAAM,6BAA6B,GAAI,YAAY,uBAAuB,KAAG,MAEnF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,0BAA8C,0BAA0B,KAAG,MA4B7G,CAAC","sourcesContent":["import { createHash } from 'node:crypto';\nimport type { Plugin } from 'vite';\nimport { defineMfNavigation, type IMfNavigationDefinition, type IMfNavigationManifest } from './navigation';\n\nconst DEFAULT_FILE_NAME = 'navigation.json';\nconst FILE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\\.json$/;\n\nexport interface IMfNavigationPluginOptions {\n fileName?: string;\n navigation: IMfNavigationDefinition;\n}\n\nconst serializeDefinition = (definition: Readonly<IMfNavigationDefinition>): string => JSON.stringify(definition);\n\nexport const createMfNavigationManifest = (navigation: IMfNavigationDefinition): Readonly<IMfNavigationManifest> => {\n const definition = defineMfNavigation(navigation);\n const revision = createHash('sha256').update(serializeDefinition(definition)).digest('hex');\n\n return Object.freeze({\n applicationId: definition.applicationId,\n contractVersion: definition.contractVersion,\n items: definition.items,\n label: definition.label,\n landingPath: definition.landingPath,\n moduleId: definition.moduleId,\n revision,\n });\n};\n\nexport const serializeMfNavigationManifest = (navigation: IMfNavigationDefinition): string => {\n return `${JSON.stringify(createMfNavigationManifest(navigation), null, 2)}\\n`;\n};\n\nexport const mfNavigationPlugin = ({ fileName = DEFAULT_FILE_NAME, navigation }: IMfNavigationPluginOptions): Plugin => {\n if (!FILE_NAME_PATTERN.test(fileName)) {\n throw new Error('Microfrontend navigation fileName must be a safe JSON file name.');\n }\n\n const source = serializeMfNavigationManifest(navigation);\n const publicPath = `/${fileName}`;\n\n return {\n name: 'edynamix-mf-navigation',\n configureServer(server) {\n server.middlewares.use((request, response, next) => {\n const requestPath = request.url?.split('?', 1)[0];\n if (requestPath !== publicPath) {\n next();\n return;\n }\n\n response.statusCode = 200;\n response.setHeader('Cache-Control', 'no-cache');\n response.setHeader('Content-Type', 'application/json; charset=utf-8');\n response.end(source);\n });\n },\n generateBundle() {\n this.emitFile({ fileName, source, type: 'asset' });\n },\n };\n};\n"]}
package/dist/vite.js ADDED
@@ -0,0 +1,48 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { defineMfNavigation } from './navigation';
3
+ const DEFAULT_FILE_NAME = 'navigation.json';
4
+ const FILE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\.json$/;
5
+ const serializeDefinition = (definition) => JSON.stringify(definition);
6
+ export const createMfNavigationManifest = (navigation) => {
7
+ const definition = defineMfNavigation(navigation);
8
+ const revision = createHash('sha256').update(serializeDefinition(definition)).digest('hex');
9
+ return Object.freeze({
10
+ applicationId: definition.applicationId,
11
+ contractVersion: definition.contractVersion,
12
+ items: definition.items,
13
+ label: definition.label,
14
+ landingPath: definition.landingPath,
15
+ moduleId: definition.moduleId,
16
+ revision,
17
+ });
18
+ };
19
+ export const serializeMfNavigationManifest = (navigation) => {
20
+ return `${JSON.stringify(createMfNavigationManifest(navigation), null, 2)}\n`;
21
+ };
22
+ export const mfNavigationPlugin = ({ fileName = DEFAULT_FILE_NAME, navigation }) => {
23
+ if (!FILE_NAME_PATTERN.test(fileName)) {
24
+ throw new Error('Microfrontend navigation fileName must be a safe JSON file name.');
25
+ }
26
+ const source = serializeMfNavigationManifest(navigation);
27
+ const publicPath = `/${fileName}`;
28
+ return {
29
+ name: 'edynamix-mf-navigation',
30
+ configureServer(server) {
31
+ server.middlewares.use((request, response, next) => {
32
+ const requestPath = request.url?.split('?', 1)[0];
33
+ if (requestPath !== publicPath) {
34
+ next();
35
+ return;
36
+ }
37
+ response.statusCode = 200;
38
+ response.setHeader('Cache-Control', 'no-cache');
39
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
40
+ response.end(source);
41
+ });
42
+ },
43
+ generateBundle() {
44
+ this.emitFile({ fileName, source, type: 'asset' });
45
+ },
46
+ };
47
+ };
48
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,kBAAkB,EAA4D,MAAM,cAAc,CAAC;AAE5G,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;AAC5C,MAAM,iBAAiB,GAAG,oCAAoC,CAAC;AAO/D,MAAM,mBAAmB,GAAG,CAAC,UAA6C,EAAU,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;AAElH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,UAAmC,EAAmC,EAAE;IACjH,MAAM,UAAU,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE5F,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,aAAa,EAAE,UAAU,CAAC,aAAa;QACvC,eAAe,EAAE,UAAU,CAAC,eAAe;QAC3C,KAAK,EAAE,UAAU,CAAC,KAAK;QACvB,KAAK,EAAE,UAAU,CAAC,KAAK;QACvB,WAAW,EAAE,UAAU,CAAC,WAAW;QACnC,QAAQ,EAAE,UAAU,CAAC,QAAQ;QAC7B,QAAQ;KACT,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,UAAmC,EAAU,EAAE;IAC3F,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAChF,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,EAAE,QAAQ,GAAG,iBAAiB,EAAE,UAAU,EAA8B,EAAU,EAAE;IACrH,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;IACtF,CAAC;IAED,MAAM,MAAM,GAAG,6BAA6B,CAAC,UAAU,CAAC,CAAC;IACzD,MAAM,UAAU,GAAG,IAAI,QAAQ,EAAE,CAAC;IAElC,OAAO;QACL,IAAI,EAAE,wBAAwB;QAC9B,eAAe,CAAC,MAAM;YACpB,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;gBACjD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClD,IAAI,WAAW,KAAK,UAAU,EAAE,CAAC;oBAC/B,IAAI,EAAE,CAAC;oBACP,OAAO;gBACT,CAAC;gBAED,QAAQ,CAAC,UAAU,GAAG,GAAG,CAAC;gBAC1B,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;gBAChD,QAAQ,CAAC,SAAS,CAAC,cAAc,EAAE,iCAAiC,CAAC,CAAC;gBACtE,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACvB,CAAC,CAAC,CAAC;QACL,CAAC;QACD,cAAc;YACZ,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACrD,CAAC;KACF,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { createHash } from 'node:crypto';\nimport type { Plugin } from 'vite';\nimport { defineMfNavigation, type IMfNavigationDefinition, type IMfNavigationManifest } from './navigation';\n\nconst DEFAULT_FILE_NAME = 'navigation.json';\nconst FILE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\\.json$/;\n\nexport interface IMfNavigationPluginOptions {\n fileName?: string;\n navigation: IMfNavigationDefinition;\n}\n\nconst serializeDefinition = (definition: Readonly<IMfNavigationDefinition>): string => JSON.stringify(definition);\n\nexport const createMfNavigationManifest = (navigation: IMfNavigationDefinition): Readonly<IMfNavigationManifest> => {\n const definition = defineMfNavigation(navigation);\n const revision = createHash('sha256').update(serializeDefinition(definition)).digest('hex');\n\n return Object.freeze({\n applicationId: definition.applicationId,\n contractVersion: definition.contractVersion,\n items: definition.items,\n label: definition.label,\n landingPath: definition.landingPath,\n moduleId: definition.moduleId,\n revision,\n });\n};\n\nexport const serializeMfNavigationManifest = (navigation: IMfNavigationDefinition): string => {\n return `${JSON.stringify(createMfNavigationManifest(navigation), null, 2)}\\n`;\n};\n\nexport const mfNavigationPlugin = ({ fileName = DEFAULT_FILE_NAME, navigation }: IMfNavigationPluginOptions): Plugin => {\n if (!FILE_NAME_PATTERN.test(fileName)) {\n throw new Error('Microfrontend navigation fileName must be a safe JSON file name.');\n }\n\n const source = serializeMfNavigationManifest(navigation);\n const publicPath = `/${fileName}`;\n\n return {\n name: 'edynamix-mf-navigation',\n configureServer(server) {\n server.middlewares.use((request, response, next) => {\n const requestPath = request.url?.split('?', 1)[0];\n if (requestPath !== publicPath) {\n next();\n return;\n }\n\n response.statusCode = 200;\n response.setHeader('Cache-Control', 'no-cache');\n response.setHeader('Content-Type', 'application/json; charset=utf-8');\n response.end(source);\n });\n },\n generateBundle() {\n this.emitFile({ fileName, source, type: 'asset' });\n },\n };\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edynamix/mf-isolation",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "CSS and overlay isolation helpers for eDynamix microfrontends.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -33,6 +33,14 @@
33
33
  "types": "./dist/postcss.d.ts",
34
34
  "import": "./dist/postcss.js"
35
35
  },
36
+ "./navigation": {
37
+ "types": "./dist/navigation.d.ts",
38
+ "import": "./dist/navigation.js"
39
+ },
40
+ "./vite": {
41
+ "types": "./dist/vite.d.ts",
42
+ "import": "./dist/vite.js"
43
+ },
36
44
  "./package.json": "./package.json"
37
45
  },
38
46
  "scripts": {
@@ -78,7 +86,8 @@
78
86
  "peerDependencies": {
79
87
  "@angular/cdk": "^19.0.0",
80
88
  "@angular/common": "^19.0.0",
81
- "@angular/core": "^19.0.0"
89
+ "@angular/core": "^19.0.0",
90
+ "vite": "^8.0.0"
82
91
  },
83
92
  "peerDependenciesMeta": {
84
93
  "@angular/cdk": {
@@ -89,6 +98,9 @@
89
98
  },
90
99
  "@angular/core": {
91
100
  "optional": true
101
+ },
102
+ "vite": {
103
+ "optional": true
92
104
  }
93
105
  }
94
106
  }