@edynamix/mf-isolation 0.0.23 → 0.0.25

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
@@ -178,6 +178,38 @@ Provide the runtime `MF_OVERLAY_ROOT` value through the existing platform/bootst
178
178
 
179
179
  The Angular entry point requires `@angular/core`, `@angular/common`, and `@angular/cdk` 19.x.
180
180
 
181
+ ## Sharing Angular between modules
182
+
183
+ Without sharing, every hosted module ships and starts its own copy of Angular. With `createMfAngularShared()` from `@edynamix/mf-isolation/vite`, modules on the same versions load Angular, the CDK, Material and RxJS once per page:
184
+
185
+ ```ts
186
+ import { federation } from '@module-federation/vite';
187
+ import { createMfAngularShared } from '@edynamix/mf-isolation/vite';
188
+ import packageJson from './package.json' with { type: 'json' };
189
+
190
+ federation({
191
+ name: 'autopoint',
192
+ filename: 'remoteEntry.js',
193
+ exposes: { './lifecycle': './src/lifecycle.ts' },
194
+ shared: createMfAngularShared(packageJson.dependencies),
195
+ });
196
+ ```
197
+
198
+ It shares every script entry point of the module's `@angular/*` dependencies (read from each installed package's `exports`), plus `rxjs` and `rxjs/operators`, at the module's own version. Entry points that stylesheets also resolve are left out: the `@angular/material` and `@angular/cdk` roots answer `@use '@angular/material'`, and Module Federation would answer the stylesheet with a script. `@angular/compiler` and `@angular/platform-browser-dynamic` are not shared: only just-in-time compilation needs them, and sharing them would put the compiler into every module's build. The versions must be exact (`19.2.25`, not `^19.2.25`). A module on a different version keeps its own copy instead of mixing two Angular versions on one page. Pass `{ root }` when the Vite config does not run from the module's package directory.
199
+
200
+ A shared Angular means one Angular platform per page. A module bootstrapped with `createApplication()` reuses that platform already. A module that bootstraps an NgModule replaces `platformBrowserDynamic(providers)` with `createHostedAngularPlatform(providers)`:
201
+
202
+ ```ts
203
+ import { createHostedAngularPlatform } from '@edynamix/mf-isolation/angular-platform';
204
+
205
+ const platform = createHostedAngularPlatform([{ provide: APP_BASE_HREF, useValue: baseHref }]);
206
+ const moduleRef = await platform.bootstrapModule(AppModule);
207
+ // on unmount
208
+ platform.destroy();
209
+ ```
210
+
211
+ Its providers reach only the module's own NgModules, and `destroy()` destroys only them; the shared platform and the other modules keep running. Calling `platformBrowserDynamic(providers)` instead would hand the module another module's platform without its providers, and its `destroy()` would stop every module on the page. The entry point requires `@angular/platform-browser-dynamic` 19.x.
212
+
181
213
  ## Host-owned Zone.js
182
214
 
183
215
  Standalone Angular applications load their own Zone.js runtime as usual:
@@ -207,5 +239,6 @@ export const mount = async (host: HTMLElement, context: IShellContext): Promise<
207
239
  - `@edynamix/mf-isolation`
208
240
  - `@edynamix/mf-isolation/angular`
209
241
  - `@edynamix/mf-isolation/angular-runtime`
242
+ - `@edynamix/mf-isolation/angular-platform`
210
243
  - `@edynamix/mf-isolation/host-contract`
211
244
  - `@edynamix/mf-isolation/postcss`
@@ -0,0 +1,12 @@
1
+ import { PlatformRef, type StaticProvider } from '@angular/core';
2
+ /**
3
+ * Replaces `platformBrowserDynamic(providers)` in a hosted module that bootstraps an NgModule.
4
+ *
5
+ * When the modules on a page share one Angular, the page has one Angular platform: a second
6
+ * `platformBrowserDynamic(providers)` call returns the first module's platform and drops its own providers, and a
7
+ * module's `platform.destroy()` would destroy every other module. This returns a platform for the module alone. The
8
+ * module's providers reach only its own NgModules, `destroy()` destroys only them, and the shared platform (created
9
+ * here, or reused from a module that started first, including one bootstrapped with `createApplication`) stays up.
10
+ */
11
+ export declare const createHostedAngularPlatform: (providers?: StaticProvider[]) => PlatformRef;
12
+ //# sourceMappingURL=angular-platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"angular-platform.d.ts","sourceRoot":"","sources":["../src/angular-platform.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2C,WAAW,EAAE,KAAK,cAAc,EAAa,MAAM,eAAe,CAAC;AAsDrH;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,GAAI,YAAW,cAAc,EAAO,KAAG,WAG9E,CAAC","sourcesContent":["import { getPlatform, Injector, type NgModuleRef, PlatformRef, type StaticProvider, type Type } from '@angular/core';\nimport { platformBrowserDynamic } from '@angular/platform-browser-dynamic';\n\n// PlatformRef takes its injector through an internal constructor, which the public typings leave out.\nconst PlatformRefWithInjector = PlatformRef as unknown as new (injector: Injector) => PlatformRef;\n\ntype TBootstrapModuleOptions = Parameters<PlatformRef['bootstrapModule']>[1];\n\n// One module's platform on top of the page's single Angular platform: its NgModules see the module's own providers,\n// and destroying it destroys only those NgModules, never the shared platform the other modules run on.\nclass HostedModulePlatformRef extends PlatformRefWithInjector {\n private readonly hostedModules = new Set<NgModuleRef<unknown>>();\n private readonly hostedDestroyListeners: Array<() => void> = [];\n private hostedDestroyed = false;\n\n override async bootstrapModule<M>(moduleType: Type<M>, compilerOptions?: TBootstrapModuleOptions): Promise<NgModuleRef<M>> {\n if (this.hostedDestroyed) {\n throw new Error('The hosted Angular module platform has been destroyed.');\n }\n\n const moduleRef = await super.bootstrapModule(moduleType, compilerOptions);\n if (this.hostedDestroyed) {\n moduleRef.destroy();\n throw new Error('The hosted Angular module platform was destroyed while the module was bootstrapping.');\n }\n\n this.hostedModules.add(moduleRef);\n moduleRef.onDestroy(() => this.hostedModules.delete(moduleRef));\n return moduleRef;\n }\n\n override onDestroy(callback: () => void): void {\n this.hostedDestroyListeners.push(callback);\n }\n\n override get destroyed(): boolean {\n return this.hostedDestroyed;\n }\n\n override destroy(): void {\n if (this.hostedDestroyed) {\n return;\n }\n\n this.hostedDestroyed = true;\n for (const moduleRef of [...this.hostedModules]) {\n moduleRef.destroy();\n }\n for (const listener of this.hostedDestroyListeners.splice(0)) {\n listener();\n }\n }\n}\n\n/**\n * Replaces `platformBrowserDynamic(providers)` in a hosted module that bootstraps an NgModule.\n *\n * When the modules on a page share one Angular, the page has one Angular platform: a second\n * `platformBrowserDynamic(providers)` call returns the first module's platform and drops its own providers, and a\n * module's `platform.destroy()` would destroy every other module. This returns a platform for the module alone. The\n * module's providers reach only its own NgModules, `destroy()` destroys only them, and the shared platform (created\n * here, or reused from a module that started first, including one bootstrapped with `createApplication`) stays up.\n */\nexport const createHostedAngularPlatform = (providers: StaticProvider[] = []): PlatformRef => {\n const sharedPlatform = getPlatform() ?? platformBrowserDynamic();\n return new HostedModulePlatformRef(Injector.create({ providers, parent: sharedPlatform.injector, name: 'Hosted Angular module' }));\n};\n"]}
@@ -0,0 +1,56 @@
1
+ import { getPlatform, Injector, PlatformRef } from '@angular/core';
2
+ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3
+ // PlatformRef takes its injector through an internal constructor, which the public typings leave out.
4
+ const PlatformRefWithInjector = PlatformRef;
5
+ // One module's platform on top of the page's single Angular platform: its NgModules see the module's own providers,
6
+ // and destroying it destroys only those NgModules, never the shared platform the other modules run on.
7
+ class HostedModulePlatformRef extends PlatformRefWithInjector {
8
+ hostedModules = new Set();
9
+ hostedDestroyListeners = [];
10
+ hostedDestroyed = false;
11
+ async bootstrapModule(moduleType, compilerOptions) {
12
+ if (this.hostedDestroyed) {
13
+ throw new Error('The hosted Angular module platform has been destroyed.');
14
+ }
15
+ const moduleRef = await super.bootstrapModule(moduleType, compilerOptions);
16
+ if (this.hostedDestroyed) {
17
+ moduleRef.destroy();
18
+ throw new Error('The hosted Angular module platform was destroyed while the module was bootstrapping.');
19
+ }
20
+ this.hostedModules.add(moduleRef);
21
+ moduleRef.onDestroy(() => this.hostedModules.delete(moduleRef));
22
+ return moduleRef;
23
+ }
24
+ onDestroy(callback) {
25
+ this.hostedDestroyListeners.push(callback);
26
+ }
27
+ get destroyed() {
28
+ return this.hostedDestroyed;
29
+ }
30
+ destroy() {
31
+ if (this.hostedDestroyed) {
32
+ return;
33
+ }
34
+ this.hostedDestroyed = true;
35
+ for (const moduleRef of [...this.hostedModules]) {
36
+ moduleRef.destroy();
37
+ }
38
+ for (const listener of this.hostedDestroyListeners.splice(0)) {
39
+ listener();
40
+ }
41
+ }
42
+ }
43
+ /**
44
+ * Replaces `platformBrowserDynamic(providers)` in a hosted module that bootstraps an NgModule.
45
+ *
46
+ * When the modules on a page share one Angular, the page has one Angular platform: a second
47
+ * `platformBrowserDynamic(providers)` call returns the first module's platform and drops its own providers, and a
48
+ * module's `platform.destroy()` would destroy every other module. This returns a platform for the module alone. The
49
+ * module's providers reach only its own NgModules, `destroy()` destroys only them, and the shared platform (created
50
+ * here, or reused from a module that started first, including one bootstrapped with `createApplication`) stays up.
51
+ */
52
+ export const createHostedAngularPlatform = (providers = []) => {
53
+ const sharedPlatform = getPlatform() ?? platformBrowserDynamic();
54
+ return new HostedModulePlatformRef(Injector.create({ providers, parent: sharedPlatform.injector, name: 'Hosted Angular module' }));
55
+ };
56
+ //# sourceMappingURL=angular-platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"angular-platform.js","sourceRoot":"","sources":["../src/angular-platform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAoB,WAAW,EAAkC,MAAM,eAAe,CAAC;AACrH,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAE3E,sGAAsG;AACtG,MAAM,uBAAuB,GAAG,WAAiE,CAAC;AAIlG,oHAAoH;AACpH,uGAAuG;AACvG,MAAM,uBAAwB,SAAQ,uBAAuB;IAC1C,aAAa,GAAG,IAAI,GAAG,EAAwB,CAAC;IAChD,sBAAsB,GAAsB,EAAE,CAAC;IACxD,eAAe,GAAG,KAAK,CAAC;IAEvB,KAAK,CAAC,eAAe,CAAI,UAAmB,EAAE,eAAyC;QAC9F,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC5E,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,eAAe,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;QAC3E,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,SAAS,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,sFAAsF,CAAC,CAAC;QAC1G,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAClC,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;QAChE,OAAO,SAAS,CAAC;IACnB,CAAC;IAEQ,SAAS,CAAC,QAAoB;QACrC,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAED,IAAa,SAAS;QACpB,OAAO,IAAI,CAAC,eAAe,CAAC;IAC9B,CAAC;IAEQ,OAAO;QACd,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,KAAK,MAAM,SAAS,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;YAChD,SAAS,CAAC,OAAO,EAAE,CAAC;QACtB,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,QAAQ,EAAE,CAAC;QACb,CAAC;IACH,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,YAA8B,EAAE,EAAe,EAAE;IAC3F,MAAM,cAAc,GAAG,WAAW,EAAE,IAAI,sBAAsB,EAAE,CAAC;IACjE,OAAO,IAAI,uBAAuB,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,CAAC,QAAQ,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC,CAAC,CAAC;AACrI,CAAC,CAAC","sourcesContent":["import { getPlatform, Injector, type NgModuleRef, PlatformRef, type StaticProvider, type Type } from '@angular/core';\nimport { platformBrowserDynamic } from '@angular/platform-browser-dynamic';\n\n// PlatformRef takes its injector through an internal constructor, which the public typings leave out.\nconst PlatformRefWithInjector = PlatformRef as unknown as new (injector: Injector) => PlatformRef;\n\ntype TBootstrapModuleOptions = Parameters<PlatformRef['bootstrapModule']>[1];\n\n// One module's platform on top of the page's single Angular platform: its NgModules see the module's own providers,\n// and destroying it destroys only those NgModules, never the shared platform the other modules run on.\nclass HostedModulePlatformRef extends PlatformRefWithInjector {\n private readonly hostedModules = new Set<NgModuleRef<unknown>>();\n private readonly hostedDestroyListeners: Array<() => void> = [];\n private hostedDestroyed = false;\n\n override async bootstrapModule<M>(moduleType: Type<M>, compilerOptions?: TBootstrapModuleOptions): Promise<NgModuleRef<M>> {\n if (this.hostedDestroyed) {\n throw new Error('The hosted Angular module platform has been destroyed.');\n }\n\n const moduleRef = await super.bootstrapModule(moduleType, compilerOptions);\n if (this.hostedDestroyed) {\n moduleRef.destroy();\n throw new Error('The hosted Angular module platform was destroyed while the module was bootstrapping.');\n }\n\n this.hostedModules.add(moduleRef);\n moduleRef.onDestroy(() => this.hostedModules.delete(moduleRef));\n return moduleRef;\n }\n\n override onDestroy(callback: () => void): void {\n this.hostedDestroyListeners.push(callback);\n }\n\n override get destroyed(): boolean {\n return this.hostedDestroyed;\n }\n\n override destroy(): void {\n if (this.hostedDestroyed) {\n return;\n }\n\n this.hostedDestroyed = true;\n for (const moduleRef of [...this.hostedModules]) {\n moduleRef.destroy();\n }\n for (const listener of this.hostedDestroyListeners.splice(0)) {\n listener();\n }\n }\n}\n\n/**\n * Replaces `platformBrowserDynamic(providers)` in a hosted module that bootstraps an NgModule.\n *\n * When the modules on a page share one Angular, the page has one Angular platform: a second\n * `platformBrowserDynamic(providers)` call returns the first module's platform and drops its own providers, and a\n * module's `platform.destroy()` would destroy every other module. This returns a platform for the module alone. The\n * module's providers reach only its own NgModules, `destroy()` destroys only them, and the shared platform (created\n * here, or reused from a module that started first, including one bootstrapped with `createApplication`) stays up.\n */\nexport const createHostedAngularPlatform = (providers: StaticProvider[] = []): PlatformRef => {\n const sharedPlatform = getPlatform() ?? platformBrowserDynamic();\n return new HostedModulePlatformRef(Injector.create({ providers, parent: sharedPlatform.injector, name: 'Hosted Angular module' }));\n};\n"]}
package/dist/vite.d.ts CHANGED
@@ -4,6 +4,20 @@ export interface IMfNavigationPluginOptions {
4
4
  fileName?: string;
5
5
  navigation: IMfNavigationDefinition;
6
6
  }
7
+ export interface IMfSharedDependency {
8
+ requiredVersion: string;
9
+ singleton: false;
10
+ }
11
+ export interface IMfAngularSharedOptions {
12
+ root?: string;
13
+ }
14
+ /**
15
+ * The Module Federation `shared` configuration of a hosted Angular module: every script entry point of the `@angular/*`
16
+ * packages among the module's dependencies, and RxJS, at the module's own exact versions. Modules on the same versions
17
+ * load these once per page and share them; a module on another version keeps its own copy instead of mixing two
18
+ * Angular versions.
19
+ */
20
+ export declare const createMfAngularShared: (dependencies: Readonly<Record<string, string>>, { root }?: IMfAngularSharedOptions) => Record<string, IMfSharedDependency>;
7
21
  export declare const MF_ASSET_MANIFEST_CONTRACT_VERSION: 1;
8
22
  export interface IMfAssetManifest {
9
23
  assets: readonly string[];
@@ -1 +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,iBAAiB,CAAC;AAQ/G,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,uBAAuB,CAAC;CACrC;AAED,eAAO,MAAM,kCAAkC,EAAG,CAAU,CAAC;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,eAAe,EAAE,OAAO,kCAAkC,CAAC;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAsBD,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;AAmEF,eAAO,MAAM,wBAAwB,GAAI,UAAU,gBAAgB,KAAG,MAAkD,CAAC;AAEzH,eAAO,MAAM,qBAAqB,GAAI,wBAAkD,6BAA6B,KAAG,MAqCvH,CAAC","sourcesContent":["import { createHash } from 'node:crypto';\nimport type { Plugin } from 'vite';\nimport { defineMfNavigation, type IMfNavigationDefinition, type IMfNavigationManifest } from './navigation.js';\n\nconst DEFAULT_FILE_NAME = 'navigation.json';\nconst DEFAULT_ASSET_FILE_NAME = 'module-assets.json';\nconst FILE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\\.json$/;\nconst MODULE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\nconst CONTENT_HASHED_ASSET_PATTERN = /(?:^|\\/)[^/]+-[a-zA-Z0-9_-]{8,}\\.[^/]+$/;\n\nexport interface IMfNavigationPluginOptions {\n fileName?: string;\n navigation: IMfNavigationDefinition;\n}\n\nexport const MF_ASSET_MANIFEST_CONTRACT_VERSION = 1 as const;\n\nexport interface IMfAssetManifest {\n assets: readonly string[];\n contractVersion: typeof MF_ASSET_MANIFEST_CONTRACT_VERSION;\n moduleId: string;\n revision: string;\n}\n\nexport interface IMfAssetManifestPluginOptions {\n fileName?: string;\n moduleId: string;\n}\n\ninterface IMfOutputAsset {\n fileName: string;\n source: string | Uint8Array;\n type: 'asset';\n}\n\ninterface IMfOutputChunk {\n code: string;\n fileName: string;\n imports: readonly string[];\n modules: Readonly<Record<string, unknown>>;\n referencedFiles?: readonly string[];\n type: 'chunk';\n viteMetadata?: { importedAssets?: ReadonlySet<string>; importedCss?: ReadonlySet<string> };\n}\n\ntype TMfOutputBundle = Readonly<Record<string, IMfOutputAsset | IMfOutputChunk>>;\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\nconst isLifecycleModule = (moduleId: string): boolean => /(?:^|\\/)src\\/lifecycle(?:-impl)?\\.[cm]?[jt]sx?$/.test(moduleId.replaceAll('\\\\', '/'));\n\nconst addChunkDependencies = (bundle: TMfOutputBundle, fileName: string, assets: Set<string>): void => {\n if (assets.has(fileName)) {\n return;\n }\n\n const output = bundle[fileName];\n if (output?.type !== 'chunk') {\n return;\n }\n\n assets.add(fileName);\n for (const importedFileName of output.imports) {\n addChunkDependencies(bundle, importedFileName, assets);\n }\n\n const metadata = output.viteMetadata;\n for (const referencedFileName of [...(metadata?.importedAssets ?? []), ...(metadata?.importedCss ?? []), ...(output.referencedFiles ?? [])]) {\n if (bundle[referencedFileName]) {\n assets.add(referencedFileName);\n }\n }\n};\n\nconst outputBytes = (output: IMfOutputAsset | IMfOutputChunk): string | Uint8Array => (output.type === 'chunk' ? output.code : output.source);\n\nconst createMfAssetManifest = (moduleId: string, bundle: TMfOutputBundle): Readonly<IMfAssetManifest> => {\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('Microfrontend asset moduleId must use lowercase letters, numbers, and hyphens.');\n }\n\n const lifecycleChunks = Object.values(bundle).filter(\n (output): output is IMfOutputChunk => output.type === 'chunk' && Object.keys(output.modules).some(isLifecycleModule),\n );\n if (lifecycleChunks.length === 0) {\n throw new Error(`Microfrontend ${moduleId} did not emit a lifecycle chunk.`);\n }\n\n const discoveredAssets = new Set<string>();\n for (const chunk of lifecycleChunks) {\n addChunkDependencies(bundle, chunk.fileName, discoveredAssets);\n }\n const assets = [...discoveredAssets].filter((fileName) => CONTENT_HASHED_ASSET_PATTERN.test(fileName)).sort();\n if (assets.length === 0) {\n throw new Error(`Microfrontend ${moduleId} did not emit content-hashed lifecycle assets.`);\n }\n\n const revisionHash = createHash('sha256');\n for (const fileName of assets) {\n const output = bundle[fileName];\n if (!output) {\n throw new Error(`Microfrontend ${moduleId} asset ${fileName} is missing from the output bundle.`);\n }\n revisionHash.update(fileName).update('\\0').update(outputBytes(output)).update('\\0');\n }\n\n return Object.freeze({\n assets: Object.freeze(assets),\n contractVersion: MF_ASSET_MANIFEST_CONTRACT_VERSION,\n moduleId,\n revision: revisionHash.digest('hex'),\n });\n};\n\nexport const serializeMfAssetManifest = (manifest: IMfAssetManifest): string => `${JSON.stringify(manifest, null, 2)}\\n`;\n\nexport const mfAssetManifestPlugin = ({ fileName = DEFAULT_ASSET_FILE_NAME, moduleId }: IMfAssetManifestPluginOptions): Plugin => {\n if (!FILE_NAME_PATTERN.test(fileName)) {\n throw new Error('Microfrontend asset fileName must be a safe JSON file name.');\n }\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('Microfrontend asset moduleId must use lowercase letters, numbers, and hyphens.');\n }\n\n const developmentManifest = serializeMfAssetManifest({\n assets: [],\n contractVersion: MF_ASSET_MANIFEST_CONTRACT_VERSION,\n moduleId,\n revision: 'development',\n });\n const publicPath = `/${fileName}`;\n\n return {\n name: 'edynamix-mf-assets',\n configureServer(server) {\n server.middlewares.use((request, response, next) => {\n if (request.url?.split('?', 1)[0] !== publicPath) {\n next();\n return;\n }\n\n response.statusCode = 200;\n response.setHeader('Access-Control-Allow-Origin', '*');\n response.setHeader('Cache-Control', 'no-cache');\n response.setHeader('Content-Type', 'application/json; charset=utf-8');\n response.end(developmentManifest);\n });\n },\n generateBundle(_options, bundle) {\n const source = serializeMfAssetManifest(createMfAssetManifest(moduleId, bundle as unknown as TMfOutputBundle));\n this.emitFile({ fileName, source, type: 'asset' });\n },\n };\n};\n"]}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAAsB,KAAK,uBAAuB,EAAE,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAQ/G,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,uBAAuB,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,KAAK,CAAC;CAClB;AAgBD,MAAM,WAAW,uBAAuB;IAEtC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA2CD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,cAAc,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC9C,WAA0B,uBAA4B,KACrD,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAepC,CAAC;AAEF,eAAO,MAAM,kCAAkC,EAAG,CAAU,CAAC;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,eAAe,EAAE,OAAO,kCAAkC,CAAC;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAsBD,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;AAmEF,eAAO,MAAM,wBAAwB,GAAI,UAAU,gBAAgB,KAAG,MAAkD,CAAC;AAEzH,eAAO,MAAM,qBAAqB,GAAI,wBAAkD,6BAA6B,KAAG,MAqCvH,CAAC","sourcesContent":["import { createHash } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\nimport type { Plugin } from 'vite';\nimport { defineMfNavigation, type IMfNavigationDefinition, type IMfNavigationManifest } from './navigation.js';\n\nconst DEFAULT_FILE_NAME = 'navigation.json';\nconst DEFAULT_ASSET_FILE_NAME = 'module-assets.json';\nconst FILE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\\.json$/;\nconst MODULE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\nconst CONTENT_HASHED_ASSET_PATTERN = /(?:^|\\/)[^/]+-[a-zA-Z0-9_-]{8,}\\.[^/]+$/;\n\nexport interface IMfNavigationPluginOptions {\n fileName?: string;\n navigation: IMfNavigationDefinition;\n}\n\nexport interface IMfSharedDependency {\n requiredVersion: string;\n singleton: false;\n}\n\n// The Angular packages are shared entry point by entry point: each one is a flat ESM file importing the others by\n// package name. An entry point stylesheets also resolve (the `sass` root of @angular/material and @angular/cdk, which\n// `@use '@angular/material'` reaches) is left out, because Module Federation would answer the stylesheet with a script.\n// RxJS subpaths reach its internals by relative path: only its two public entry points are shared, each complete, so a\n// page never mixes RxJS internals from two modules' builds.\nconst SHARED_ANGULAR_SCOPE = '@angular/';\n// Only just-in-time compilation needs these, and the modules are compiled ahead of time. Shared, they would put the\n// compiler (with its ShadowCss placeholders) into every module's build; the modules that bootstrap through\n// platformBrowserDynamic keep their own copy, as before.\nconst UNSHARED_ANGULAR_PACKAGES = new Set(['@angular/compiler', '@angular/platform-browser-dynamic']);\nconst SHARED_RXJS_ENTRY_POINTS = ['rxjs', 'rxjs/operators'] as const;\nconst EXACT_VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$/;\nconst SCRIPT_ENTRY_CONDITIONS = 'default,types';\n\nexport interface IMfAngularSharedOptions {\n // The directory whose node_modules holds the module's packages; the Vite config's directory by default.\n root?: string;\n}\n\n// The import and re-export statements of a flat ESM file, which start their line; comments mentioning imports do not.\nconst BARE_IMPORT_PATTERN = /^(?:import|export)\\b[^;'\"]*?(?:\\bfrom\\s*)?['\"]([^'\"./][^'\"]*)['\"]/gm;\n\nconst bareImportPackage = (specifier: string): string =>\n specifier\n .split('/')\n .slice(0, specifier.startsWith('@') ? 2 : 1)\n .join('/');\n\n// Module Federation builds a local copy of every shared entry point, used or not, so an entry point is shared only\n// when every package it imports is installed (`@angular/common/upgrade` needs `@angular/upgrade`, which modules do not\n// install, and `@angular/platform-browser/animations` needs `@angular/animations`).\nconst angularScriptEntryPoints = (packageName: string, root: string): string[] => {\n const resolvePackage = createRequire(join(root, 'package.json'));\n const packageJsonPath = resolvePackage.resolve(`${packageName}/package.json`);\n const isInstalled = (dependency: string): boolean => {\n try {\n resolvePackage.resolve(`${dependency}/package.json`);\n return true;\n } catch {\n return false;\n }\n };\n const { exports } = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { exports?: Record<string, unknown> };\n return Object.entries(exports ?? {})\n .filter(([subpath, target]) => {\n if (\n subpath.includes('*') ||\n subpath.split('/').includes('testing') ||\n typeof target !== 'object' ||\n target === null ||\n Object.keys(target).sort().join(',') !== SCRIPT_ENTRY_CONDITIONS\n ) {\n return false;\n }\n const source = readFileSync(join(dirname(packageJsonPath), (target as { default: string }).default), 'utf8');\n return [...source.matchAll(BARE_IMPORT_PATTERN)].every(([, specifier]) => specifier === undefined || isInstalled(bareImportPackage(specifier)));\n })\n .map(([subpath]) => (subpath === '.' ? packageName : `${packageName}/${subpath.slice(2)}`));\n};\n\n/**\n * The Module Federation `shared` configuration of a hosted Angular module: every script entry point of the `@angular/*`\n * packages among the module's dependencies, and RxJS, at the module's own exact versions. Modules on the same versions\n * load these once per page and share them; a module on another version keeps its own copy instead of mixing two\n * Angular versions.\n */\nexport const createMfAngularShared = (\n dependencies: Readonly<Record<string, string>>,\n { root = process.cwd() }: IMfAngularSharedOptions = {},\n): Record<string, IMfSharedDependency> => {\n const shared: Record<string, IMfSharedDependency> = {};\n for (const [name, version] of Object.entries(dependencies).sort(([left], [right]) => left.localeCompare(right))) {\n const isAngular = name.startsWith(SHARED_ANGULAR_SCOPE) && !UNSHARED_ANGULAR_PACKAGES.has(name);\n if (!isAngular && name !== 'rxjs') {\n continue;\n }\n if (!EXACT_VERSION_PATTERN.test(version)) {\n throw new Error(`Shared dependency ${name} must be pinned to an exact version, not \"${version}\".`);\n }\n for (const entryPoint of isAngular ? angularScriptEntryPoints(name, root) : SHARED_RXJS_ENTRY_POINTS) {\n shared[entryPoint] = { requiredVersion: version, singleton: false };\n }\n }\n return shared;\n};\n\nexport const MF_ASSET_MANIFEST_CONTRACT_VERSION = 1 as const;\n\nexport interface IMfAssetManifest {\n assets: readonly string[];\n contractVersion: typeof MF_ASSET_MANIFEST_CONTRACT_VERSION;\n moduleId: string;\n revision: string;\n}\n\nexport interface IMfAssetManifestPluginOptions {\n fileName?: string;\n moduleId: string;\n}\n\ninterface IMfOutputAsset {\n fileName: string;\n source: string | Uint8Array;\n type: 'asset';\n}\n\ninterface IMfOutputChunk {\n code: string;\n fileName: string;\n imports: readonly string[];\n modules: Readonly<Record<string, unknown>>;\n referencedFiles?: readonly string[];\n type: 'chunk';\n viteMetadata?: { importedAssets?: ReadonlySet<string>; importedCss?: ReadonlySet<string> };\n}\n\ntype TMfOutputBundle = Readonly<Record<string, IMfOutputAsset | IMfOutputChunk>>;\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\nconst isLifecycleModule = (moduleId: string): boolean => /(?:^|\\/)src\\/lifecycle(?:-impl)?\\.[cm]?[jt]sx?$/.test(moduleId.replaceAll('\\\\', '/'));\n\nconst addChunkDependencies = (bundle: TMfOutputBundle, fileName: string, assets: Set<string>): void => {\n if (assets.has(fileName)) {\n return;\n }\n\n const output = bundle[fileName];\n if (output?.type !== 'chunk') {\n return;\n }\n\n assets.add(fileName);\n for (const importedFileName of output.imports) {\n addChunkDependencies(bundle, importedFileName, assets);\n }\n\n const metadata = output.viteMetadata;\n for (const referencedFileName of [...(metadata?.importedAssets ?? []), ...(metadata?.importedCss ?? []), ...(output.referencedFiles ?? [])]) {\n if (bundle[referencedFileName]) {\n assets.add(referencedFileName);\n }\n }\n};\n\nconst outputBytes = (output: IMfOutputAsset | IMfOutputChunk): string | Uint8Array => (output.type === 'chunk' ? output.code : output.source);\n\nconst createMfAssetManifest = (moduleId: string, bundle: TMfOutputBundle): Readonly<IMfAssetManifest> => {\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('Microfrontend asset moduleId must use lowercase letters, numbers, and hyphens.');\n }\n\n const lifecycleChunks = Object.values(bundle).filter(\n (output): output is IMfOutputChunk => output.type === 'chunk' && Object.keys(output.modules).some(isLifecycleModule),\n );\n if (lifecycleChunks.length === 0) {\n throw new Error(`Microfrontend ${moduleId} did not emit a lifecycle chunk.`);\n }\n\n const discoveredAssets = new Set<string>();\n for (const chunk of lifecycleChunks) {\n addChunkDependencies(bundle, chunk.fileName, discoveredAssets);\n }\n const assets = [...discoveredAssets].filter((fileName) => CONTENT_HASHED_ASSET_PATTERN.test(fileName)).sort();\n if (assets.length === 0) {\n throw new Error(`Microfrontend ${moduleId} did not emit content-hashed lifecycle assets.`);\n }\n\n const revisionHash = createHash('sha256');\n for (const fileName of assets) {\n const output = bundle[fileName];\n if (!output) {\n throw new Error(`Microfrontend ${moduleId} asset ${fileName} is missing from the output bundle.`);\n }\n revisionHash.update(fileName).update('\\0').update(outputBytes(output)).update('\\0');\n }\n\n return Object.freeze({\n assets: Object.freeze(assets),\n contractVersion: MF_ASSET_MANIFEST_CONTRACT_VERSION,\n moduleId,\n revision: revisionHash.digest('hex'),\n });\n};\n\nexport const serializeMfAssetManifest = (manifest: IMfAssetManifest): string => `${JSON.stringify(manifest, null, 2)}\\n`;\n\nexport const mfAssetManifestPlugin = ({ fileName = DEFAULT_ASSET_FILE_NAME, moduleId }: IMfAssetManifestPluginOptions): Plugin => {\n if (!FILE_NAME_PATTERN.test(fileName)) {\n throw new Error('Microfrontend asset fileName must be a safe JSON file name.');\n }\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('Microfrontend asset moduleId must use lowercase letters, numbers, and hyphens.');\n }\n\n const developmentManifest = serializeMfAssetManifest({\n assets: [],\n contractVersion: MF_ASSET_MANIFEST_CONTRACT_VERSION,\n moduleId,\n revision: 'development',\n });\n const publicPath = `/${fileName}`;\n\n return {\n name: 'edynamix-mf-assets',\n configureServer(server) {\n server.middlewares.use((request, response, next) => {\n if (request.url?.split('?', 1)[0] !== publicPath) {\n next();\n return;\n }\n\n response.statusCode = 200;\n response.setHeader('Access-Control-Allow-Origin', '*');\n response.setHeader('Cache-Control', 'no-cache');\n response.setHeader('Content-Type', 'application/json; charset=utf-8');\n response.end(developmentManifest);\n });\n },\n generateBundle(_options, bundle) {\n const source = serializeMfAssetManifest(createMfAssetManifest(moduleId, bundle as unknown as TMfOutputBundle));\n this.emitFile({ fileName, source, type: 'asset' });\n },\n };\n};\n"]}
package/dist/vite.js CHANGED
@@ -1,10 +1,84 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join } from 'node:path';
2
5
  import { defineMfNavigation } from './navigation.js';
3
6
  const DEFAULT_FILE_NAME = 'navigation.json';
4
7
  const DEFAULT_ASSET_FILE_NAME = 'module-assets.json';
5
8
  const FILE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\.json$/;
6
9
  const MODULE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
7
10
  const CONTENT_HASHED_ASSET_PATTERN = /(?:^|\/)[^/]+-[a-zA-Z0-9_-]{8,}\.[^/]+$/;
11
+ // The Angular packages are shared entry point by entry point: each one is a flat ESM file importing the others by
12
+ // package name. An entry point stylesheets also resolve (the `sass` root of @angular/material and @angular/cdk, which
13
+ // `@use '@angular/material'` reaches) is left out, because Module Federation would answer the stylesheet with a script.
14
+ // RxJS subpaths reach its internals by relative path: only its two public entry points are shared, each complete, so a
15
+ // page never mixes RxJS internals from two modules' builds.
16
+ const SHARED_ANGULAR_SCOPE = '@angular/';
17
+ // Only just-in-time compilation needs these, and the modules are compiled ahead of time. Shared, they would put the
18
+ // compiler (with its ShadowCss placeholders) into every module's build; the modules that bootstrap through
19
+ // platformBrowserDynamic keep their own copy, as before.
20
+ const UNSHARED_ANGULAR_PACKAGES = new Set(['@angular/compiler', '@angular/platform-browser-dynamic']);
21
+ const SHARED_RXJS_ENTRY_POINTS = ['rxjs', 'rxjs/operators'];
22
+ const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
23
+ const SCRIPT_ENTRY_CONDITIONS = 'default,types';
24
+ // The import and re-export statements of a flat ESM file, which start their line; comments mentioning imports do not.
25
+ const BARE_IMPORT_PATTERN = /^(?:import|export)\b[^;'"]*?(?:\bfrom\s*)?['"]([^'"./][^'"]*)['"]/gm;
26
+ const bareImportPackage = (specifier) => specifier
27
+ .split('/')
28
+ .slice(0, specifier.startsWith('@') ? 2 : 1)
29
+ .join('/');
30
+ // Module Federation builds a local copy of every shared entry point, used or not, so an entry point is shared only
31
+ // when every package it imports is installed (`@angular/common/upgrade` needs `@angular/upgrade`, which modules do not
32
+ // install, and `@angular/platform-browser/animations` needs `@angular/animations`).
33
+ const angularScriptEntryPoints = (packageName, root) => {
34
+ const resolvePackage = createRequire(join(root, 'package.json'));
35
+ const packageJsonPath = resolvePackage.resolve(`${packageName}/package.json`);
36
+ const isInstalled = (dependency) => {
37
+ try {
38
+ resolvePackage.resolve(`${dependency}/package.json`);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ };
45
+ const { exports } = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
46
+ return Object.entries(exports ?? {})
47
+ .filter(([subpath, target]) => {
48
+ if (subpath.includes('*') ||
49
+ subpath.split('/').includes('testing') ||
50
+ typeof target !== 'object' ||
51
+ target === null ||
52
+ Object.keys(target).sort().join(',') !== SCRIPT_ENTRY_CONDITIONS) {
53
+ return false;
54
+ }
55
+ const source = readFileSync(join(dirname(packageJsonPath), target.default), 'utf8');
56
+ return [...source.matchAll(BARE_IMPORT_PATTERN)].every(([, specifier]) => specifier === undefined || isInstalled(bareImportPackage(specifier)));
57
+ })
58
+ .map(([subpath]) => (subpath === '.' ? packageName : `${packageName}/${subpath.slice(2)}`));
59
+ };
60
+ /**
61
+ * The Module Federation `shared` configuration of a hosted Angular module: every script entry point of the `@angular/*`
62
+ * packages among the module's dependencies, and RxJS, at the module's own exact versions. Modules on the same versions
63
+ * load these once per page and share them; a module on another version keeps its own copy instead of mixing two
64
+ * Angular versions.
65
+ */
66
+ export const createMfAngularShared = (dependencies, { root = process.cwd() } = {}) => {
67
+ const shared = {};
68
+ for (const [name, version] of Object.entries(dependencies).sort(([left], [right]) => left.localeCompare(right))) {
69
+ const isAngular = name.startsWith(SHARED_ANGULAR_SCOPE) && !UNSHARED_ANGULAR_PACKAGES.has(name);
70
+ if (!isAngular && name !== 'rxjs') {
71
+ continue;
72
+ }
73
+ if (!EXACT_VERSION_PATTERN.test(version)) {
74
+ throw new Error(`Shared dependency ${name} must be pinned to an exact version, not "${version}".`);
75
+ }
76
+ for (const entryPoint of isAngular ? angularScriptEntryPoints(name, root) : SHARED_RXJS_ENTRY_POINTS) {
77
+ shared[entryPoint] = { requiredVersion: version, singleton: false };
78
+ }
79
+ }
80
+ return shared;
81
+ };
8
82
  export const MF_ASSET_MANIFEST_CONTRACT_VERSION = 1;
9
83
  const serializeDefinition = (definition) => JSON.stringify(definition);
10
84
  export const createMfNavigationManifest = (navigation) => {
package/dist/vite.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport type { Plugin } from 'vite';\nimport { defineMfNavigation, type IMfNavigationDefinition, type IMfNavigationManifest } from './navigation.js';\n\nconst DEFAULT_FILE_NAME = 'navigation.json';\nconst DEFAULT_ASSET_FILE_NAME = 'module-assets.json';\nconst FILE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\\.json$/;\nconst MODULE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\nconst CONTENT_HASHED_ASSET_PATTERN = /(?:^|\\/)[^/]+-[a-zA-Z0-9_-]{8,}\\.[^/]+$/;\n\nexport interface IMfNavigationPluginOptions {\n fileName?: string;\n navigation: IMfNavigationDefinition;\n}\n\nexport const MF_ASSET_MANIFEST_CONTRACT_VERSION = 1 as const;\n\nexport interface IMfAssetManifest {\n assets: readonly string[];\n contractVersion: typeof MF_ASSET_MANIFEST_CONTRACT_VERSION;\n moduleId: string;\n revision: string;\n}\n\nexport interface IMfAssetManifestPluginOptions {\n fileName?: string;\n moduleId: string;\n}\n\ninterface IMfOutputAsset {\n fileName: string;\n source: string | Uint8Array;\n type: 'asset';\n}\n\ninterface IMfOutputChunk {\n code: string;\n fileName: string;\n imports: readonly string[];\n modules: Readonly<Record<string, unknown>>;\n referencedFiles?: readonly string[];\n type: 'chunk';\n viteMetadata?: { importedAssets?: ReadonlySet<string>; importedCss?: ReadonlySet<string> };\n}\n\ntype TMfOutputBundle = Readonly<Record<string, IMfOutputAsset | IMfOutputChunk>>;\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\nconst isLifecycleModule = (moduleId: string): boolean => /(?:^|\\/)src\\/lifecycle(?:-impl)?\\.[cm]?[jt]sx?$/.test(moduleId.replaceAll('\\\\', '/'));\n\nconst addChunkDependencies = (bundle: TMfOutputBundle, fileName: string, assets: Set<string>): void => {\n if (assets.has(fileName)) {\n return;\n }\n\n const output = bundle[fileName];\n if (output?.type !== 'chunk') {\n return;\n }\n\n assets.add(fileName);\n for (const importedFileName of output.imports) {\n addChunkDependencies(bundle, importedFileName, assets);\n }\n\n const metadata = output.viteMetadata;\n for (const referencedFileName of [...(metadata?.importedAssets ?? []), ...(metadata?.importedCss ?? []), ...(output.referencedFiles ?? [])]) {\n if (bundle[referencedFileName]) {\n assets.add(referencedFileName);\n }\n }\n};\n\nconst outputBytes = (output: IMfOutputAsset | IMfOutputChunk): string | Uint8Array => (output.type === 'chunk' ? output.code : output.source);\n\nconst createMfAssetManifest = (moduleId: string, bundle: TMfOutputBundle): Readonly<IMfAssetManifest> => {\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('Microfrontend asset moduleId must use lowercase letters, numbers, and hyphens.');\n }\n\n const lifecycleChunks = Object.values(bundle).filter(\n (output): output is IMfOutputChunk => output.type === 'chunk' && Object.keys(output.modules).some(isLifecycleModule),\n );\n if (lifecycleChunks.length === 0) {\n throw new Error(`Microfrontend ${moduleId} did not emit a lifecycle chunk.`);\n }\n\n const discoveredAssets = new Set<string>();\n for (const chunk of lifecycleChunks) {\n addChunkDependencies(bundle, chunk.fileName, discoveredAssets);\n }\n const assets = [...discoveredAssets].filter((fileName) => CONTENT_HASHED_ASSET_PATTERN.test(fileName)).sort();\n if (assets.length === 0) {\n throw new Error(`Microfrontend ${moduleId} did not emit content-hashed lifecycle assets.`);\n }\n\n const revisionHash = createHash('sha256');\n for (const fileName of assets) {\n const output = bundle[fileName];\n if (!output) {\n throw new Error(`Microfrontend ${moduleId} asset ${fileName} is missing from the output bundle.`);\n }\n revisionHash.update(fileName).update('\\0').update(outputBytes(output)).update('\\0');\n }\n\n return Object.freeze({\n assets: Object.freeze(assets),\n contractVersion: MF_ASSET_MANIFEST_CONTRACT_VERSION,\n moduleId,\n revision: revisionHash.digest('hex'),\n });\n};\n\nexport const serializeMfAssetManifest = (manifest: IMfAssetManifest): string => `${JSON.stringify(manifest, null, 2)}\\n`;\n\nexport const mfAssetManifestPlugin = ({ fileName = DEFAULT_ASSET_FILE_NAME, moduleId }: IMfAssetManifestPluginOptions): Plugin => {\n if (!FILE_NAME_PATTERN.test(fileName)) {\n throw new Error('Microfrontend asset fileName must be a safe JSON file name.');\n }\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('Microfrontend asset moduleId must use lowercase letters, numbers, and hyphens.');\n }\n\n const developmentManifest = serializeMfAssetManifest({\n assets: [],\n contractVersion: MF_ASSET_MANIFEST_CONTRACT_VERSION,\n moduleId,\n revision: 'development',\n });\n const publicPath = `/${fileName}`;\n\n return {\n name: 'edynamix-mf-assets',\n configureServer(server) {\n server.middlewares.use((request, response, next) => {\n if (request.url?.split('?', 1)[0] !== publicPath) {\n next();\n return;\n }\n\n response.statusCode = 200;\n response.setHeader('Access-Control-Allow-Origin', '*');\n response.setHeader('Cache-Control', 'no-cache');\n response.setHeader('Content-Type', 'application/json; charset=utf-8');\n response.end(developmentManifest);\n });\n },\n generateBundle(_options, bundle) {\n const source = serializeMfAssetManifest(createMfAssetManifest(moduleId, bundle as unknown as TMfOutputBundle));\n this.emitFile({ fileName, source, type: 'asset' });\n },\n };\n};\n"]}
1
+ {"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\nimport type { Plugin } from 'vite';\nimport { defineMfNavigation, type IMfNavigationDefinition, type IMfNavigationManifest } from './navigation.js';\n\nconst DEFAULT_FILE_NAME = 'navigation.json';\nconst DEFAULT_ASSET_FILE_NAME = 'module-assets.json';\nconst FILE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\\.json$/;\nconst MODULE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\nconst CONTENT_HASHED_ASSET_PATTERN = /(?:^|\\/)[^/]+-[a-zA-Z0-9_-]{8,}\\.[^/]+$/;\n\nexport interface IMfNavigationPluginOptions {\n fileName?: string;\n navigation: IMfNavigationDefinition;\n}\n\nexport interface IMfSharedDependency {\n requiredVersion: string;\n singleton: false;\n}\n\n// The Angular packages are shared entry point by entry point: each one is a flat ESM file importing the others by\n// package name. An entry point stylesheets also resolve (the `sass` root of @angular/material and @angular/cdk, which\n// `@use '@angular/material'` reaches) is left out, because Module Federation would answer the stylesheet with a script.\n// RxJS subpaths reach its internals by relative path: only its two public entry points are shared, each complete, so a\n// page never mixes RxJS internals from two modules' builds.\nconst SHARED_ANGULAR_SCOPE = '@angular/';\n// Only just-in-time compilation needs these, and the modules are compiled ahead of time. Shared, they would put the\n// compiler (with its ShadowCss placeholders) into every module's build; the modules that bootstrap through\n// platformBrowserDynamic keep their own copy, as before.\nconst UNSHARED_ANGULAR_PACKAGES = new Set(['@angular/compiler', '@angular/platform-browser-dynamic']);\nconst SHARED_RXJS_ENTRY_POINTS = ['rxjs', 'rxjs/operators'] as const;\nconst EXACT_VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$/;\nconst SCRIPT_ENTRY_CONDITIONS = 'default,types';\n\nexport interface IMfAngularSharedOptions {\n // The directory whose node_modules holds the module's packages; the Vite config's directory by default.\n root?: string;\n}\n\n// The import and re-export statements of a flat ESM file, which start their line; comments mentioning imports do not.\nconst BARE_IMPORT_PATTERN = /^(?:import|export)\\b[^;'\"]*?(?:\\bfrom\\s*)?['\"]([^'\"./][^'\"]*)['\"]/gm;\n\nconst bareImportPackage = (specifier: string): string =>\n specifier\n .split('/')\n .slice(0, specifier.startsWith('@') ? 2 : 1)\n .join('/');\n\n// Module Federation builds a local copy of every shared entry point, used or not, so an entry point is shared only\n// when every package it imports is installed (`@angular/common/upgrade` needs `@angular/upgrade`, which modules do not\n// install, and `@angular/platform-browser/animations` needs `@angular/animations`).\nconst angularScriptEntryPoints = (packageName: string, root: string): string[] => {\n const resolvePackage = createRequire(join(root, 'package.json'));\n const packageJsonPath = resolvePackage.resolve(`${packageName}/package.json`);\n const isInstalled = (dependency: string): boolean => {\n try {\n resolvePackage.resolve(`${dependency}/package.json`);\n return true;\n } catch {\n return false;\n }\n };\n const { exports } = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { exports?: Record<string, unknown> };\n return Object.entries(exports ?? {})\n .filter(([subpath, target]) => {\n if (\n subpath.includes('*') ||\n subpath.split('/').includes('testing') ||\n typeof target !== 'object' ||\n target === null ||\n Object.keys(target).sort().join(',') !== SCRIPT_ENTRY_CONDITIONS\n ) {\n return false;\n }\n const source = readFileSync(join(dirname(packageJsonPath), (target as { default: string }).default), 'utf8');\n return [...source.matchAll(BARE_IMPORT_PATTERN)].every(([, specifier]) => specifier === undefined || isInstalled(bareImportPackage(specifier)));\n })\n .map(([subpath]) => (subpath === '.' ? packageName : `${packageName}/${subpath.slice(2)}`));\n};\n\n/**\n * The Module Federation `shared` configuration of a hosted Angular module: every script entry point of the `@angular/*`\n * packages among the module's dependencies, and RxJS, at the module's own exact versions. Modules on the same versions\n * load these once per page and share them; a module on another version keeps its own copy instead of mixing two\n * Angular versions.\n */\nexport const createMfAngularShared = (\n dependencies: Readonly<Record<string, string>>,\n { root = process.cwd() }: IMfAngularSharedOptions = {},\n): Record<string, IMfSharedDependency> => {\n const shared: Record<string, IMfSharedDependency> = {};\n for (const [name, version] of Object.entries(dependencies).sort(([left], [right]) => left.localeCompare(right))) {\n const isAngular = name.startsWith(SHARED_ANGULAR_SCOPE) && !UNSHARED_ANGULAR_PACKAGES.has(name);\n if (!isAngular && name !== 'rxjs') {\n continue;\n }\n if (!EXACT_VERSION_PATTERN.test(version)) {\n throw new Error(`Shared dependency ${name} must be pinned to an exact version, not \"${version}\".`);\n }\n for (const entryPoint of isAngular ? angularScriptEntryPoints(name, root) : SHARED_RXJS_ENTRY_POINTS) {\n shared[entryPoint] = { requiredVersion: version, singleton: false };\n }\n }\n return shared;\n};\n\nexport const MF_ASSET_MANIFEST_CONTRACT_VERSION = 1 as const;\n\nexport interface IMfAssetManifest {\n assets: readonly string[];\n contractVersion: typeof MF_ASSET_MANIFEST_CONTRACT_VERSION;\n moduleId: string;\n revision: string;\n}\n\nexport interface IMfAssetManifestPluginOptions {\n fileName?: string;\n moduleId: string;\n}\n\ninterface IMfOutputAsset {\n fileName: string;\n source: string | Uint8Array;\n type: 'asset';\n}\n\ninterface IMfOutputChunk {\n code: string;\n fileName: string;\n imports: readonly string[];\n modules: Readonly<Record<string, unknown>>;\n referencedFiles?: readonly string[];\n type: 'chunk';\n viteMetadata?: { importedAssets?: ReadonlySet<string>; importedCss?: ReadonlySet<string> };\n}\n\ntype TMfOutputBundle = Readonly<Record<string, IMfOutputAsset | IMfOutputChunk>>;\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\nconst isLifecycleModule = (moduleId: string): boolean => /(?:^|\\/)src\\/lifecycle(?:-impl)?\\.[cm]?[jt]sx?$/.test(moduleId.replaceAll('\\\\', '/'));\n\nconst addChunkDependencies = (bundle: TMfOutputBundle, fileName: string, assets: Set<string>): void => {\n if (assets.has(fileName)) {\n return;\n }\n\n const output = bundle[fileName];\n if (output?.type !== 'chunk') {\n return;\n }\n\n assets.add(fileName);\n for (const importedFileName of output.imports) {\n addChunkDependencies(bundle, importedFileName, assets);\n }\n\n const metadata = output.viteMetadata;\n for (const referencedFileName of [...(metadata?.importedAssets ?? []), ...(metadata?.importedCss ?? []), ...(output.referencedFiles ?? [])]) {\n if (bundle[referencedFileName]) {\n assets.add(referencedFileName);\n }\n }\n};\n\nconst outputBytes = (output: IMfOutputAsset | IMfOutputChunk): string | Uint8Array => (output.type === 'chunk' ? output.code : output.source);\n\nconst createMfAssetManifest = (moduleId: string, bundle: TMfOutputBundle): Readonly<IMfAssetManifest> => {\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('Microfrontend asset moduleId must use lowercase letters, numbers, and hyphens.');\n }\n\n const lifecycleChunks = Object.values(bundle).filter(\n (output): output is IMfOutputChunk => output.type === 'chunk' && Object.keys(output.modules).some(isLifecycleModule),\n );\n if (lifecycleChunks.length === 0) {\n throw new Error(`Microfrontend ${moduleId} did not emit a lifecycle chunk.`);\n }\n\n const discoveredAssets = new Set<string>();\n for (const chunk of lifecycleChunks) {\n addChunkDependencies(bundle, chunk.fileName, discoveredAssets);\n }\n const assets = [...discoveredAssets].filter((fileName) => CONTENT_HASHED_ASSET_PATTERN.test(fileName)).sort();\n if (assets.length === 0) {\n throw new Error(`Microfrontend ${moduleId} did not emit content-hashed lifecycle assets.`);\n }\n\n const revisionHash = createHash('sha256');\n for (const fileName of assets) {\n const output = bundle[fileName];\n if (!output) {\n throw new Error(`Microfrontend ${moduleId} asset ${fileName} is missing from the output bundle.`);\n }\n revisionHash.update(fileName).update('\\0').update(outputBytes(output)).update('\\0');\n }\n\n return Object.freeze({\n assets: Object.freeze(assets),\n contractVersion: MF_ASSET_MANIFEST_CONTRACT_VERSION,\n moduleId,\n revision: revisionHash.digest('hex'),\n });\n};\n\nexport const serializeMfAssetManifest = (manifest: IMfAssetManifest): string => `${JSON.stringify(manifest, null, 2)}\\n`;\n\nexport const mfAssetManifestPlugin = ({ fileName = DEFAULT_ASSET_FILE_NAME, moduleId }: IMfAssetManifestPluginOptions): Plugin => {\n if (!FILE_NAME_PATTERN.test(fileName)) {\n throw new Error('Microfrontend asset fileName must be a safe JSON file name.');\n }\n if (!MODULE_ID_PATTERN.test(moduleId)) {\n throw new Error('Microfrontend asset moduleId must use lowercase letters, numbers, and hyphens.');\n }\n\n const developmentManifest = serializeMfAssetManifest({\n assets: [],\n contractVersion: MF_ASSET_MANIFEST_CONTRACT_VERSION,\n moduleId,\n revision: 'development',\n });\n const publicPath = `/${fileName}`;\n\n return {\n name: 'edynamix-mf-assets',\n configureServer(server) {\n server.middlewares.use((request, response, next) => {\n if (request.url?.split('?', 1)[0] !== publicPath) {\n next();\n return;\n }\n\n response.statusCode = 200;\n response.setHeader('Access-Control-Allow-Origin', '*');\n response.setHeader('Cache-Control', 'no-cache');\n response.setHeader('Content-Type', 'application/json; charset=utf-8');\n response.end(developmentManifest);\n });\n },\n generateBundle(_options, bundle) {\n const source = serializeMfAssetManifest(createMfAssetManifest(moduleId, bundle as unknown as TMfOutputBundle));\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.23",
3
+ "version": "0.0.25",
4
4
  "description": "CSS and overlay isolation helpers for eDynamix microfrontends.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -29,6 +29,10 @@
29
29
  "types": "./dist/angular-runtime.d.ts",
30
30
  "import": "./dist/angular-runtime.js"
31
31
  },
32
+ "./angular-platform": {
33
+ "types": "./dist/angular-platform.d.ts",
34
+ "import": "./dist/angular-platform.js"
35
+ },
32
36
  "./postcss": {
33
37
  "types": "./dist/postcss.d.ts",
34
38
  "import": "./dist/postcss.js"
@@ -100,6 +104,7 @@
100
104
  "@angular/cdk": "^19.0.0",
101
105
  "@angular/common": "^19.0.0",
102
106
  "@angular/core": "^19.0.0",
107
+ "@angular/platform-browser-dynamic": "^19.0.0",
103
108
  "postcss": "^8.5.16",
104
109
  "vite": "^8.0.0"
105
110
  },
@@ -113,6 +118,9 @@
113
118
  "@angular/core": {
114
119
  "optional": true
115
120
  },
121
+ "@angular/platform-browser-dynamic": {
122
+ "optional": true
123
+ },
116
124
  "postcss": {
117
125
  "optional": true
118
126
  },