@softarc/native-federation 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softarc/native-federation",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "dependencies": {
@@ -87,8 +87,14 @@ export async function buildForFederation(config, fedOptions, externals, signal)
87
87
  if (artifactInfo?.chunks) {
88
88
  federationInfo.chunks = { ...(federationInfo.chunks ?? {}), ...artifactInfo?.chunks };
89
89
  }
90
+ if (fedOptions.integrity) {
91
+ federationInfo.integrity = {
92
+ ...(fedOptions.federationCache.integrity ?? {}),
93
+ ...(artifactInfo?.integrity ?? {}),
94
+ };
95
+ }
90
96
  writeFederationInfo(federationInfo, fedOptions);
91
- writeImportMap(fedOptions.federationCache, fedOptions);
97
+ writeImportMap(fedOptions.federationCache, fedOptions, federationInfo.integrity);
92
98
  return federationInfo;
93
99
  }
94
100
  function inferPackageFromSecondary(secondary) {
@@ -123,7 +129,15 @@ async function bundleSeparatePackages(separateBrowser, externals, config, fedOpt
123
129
  if (r.chunks) {
124
130
  chunks = { ...(acc.chunks ?? {}), ...r.chunks };
125
131
  }
126
- return { externals: [...acc.externals, ...r.externals], chunks };
132
+ let integrity = acc.integrity;
133
+ if (r.integrity) {
134
+ integrity = { ...(acc.integrity ?? {}), ...r.integrity };
135
+ }
136
+ return {
137
+ externals: [...acc.externals, ...r.externals],
138
+ chunks,
139
+ integrity,
140
+ };
127
141
  }, { externals: [] });
128
142
  }
129
143
  function splitShared(shared) {
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { integrityForFile } from '../utils/hash-file.js';
3
4
  import { createBuildResultMap, popFromResultMap } from '../utils/build-result-map.js';
4
5
  import { logger } from '../utils/logger.js';
5
6
  import { normalize } from '../utils/normalize.js';
@@ -100,14 +101,26 @@ export async function bundleExposedAndMappings(config, fedOptions, externals, mo
100
101
  .forEach(f => popFromResultMap(resultMap, f));
101
102
  // Process remaining chunks and lazy loaded internal modules
102
103
  let exportedChunks = undefined;
104
+ const chunkPaths = [];
103
105
  if (config.chunks && config.features.denseChunking) {
104
106
  for (const entryFile of entryFiles)
105
107
  rewriteChunkImports(entryFile);
108
+ chunkPaths.push(...Object.values(resultMap));
106
109
  exportedChunks = {
107
- ['mapping-or-exposed']: Object.values(resultMap).map(chunk => path.basename(chunk)),
110
+ ['mapping-or-exposed']: chunkPaths.map(chunk => path.basename(chunk)),
108
111
  };
109
112
  }
110
- return { mappings: sharedResult, exposes: exposedResult, chunks: exportedChunks };
113
+ // Must run after rewriteChunkImports so SRI matches the final on-disk bytes.
114
+ let integrity;
115
+ if (fedOptions.integrity) {
116
+ integrity = {};
117
+ for (const filePath of [...entryFiles, ...chunkPaths]) {
118
+ if (!fs.existsSync(filePath))
119
+ continue;
120
+ integrity[path.basename(filePath)] = integrityForFile(filePath);
121
+ }
122
+ }
123
+ return { mappings: sharedResult, exposes: exposedResult, chunks: exportedChunks, integrity };
111
124
  }
112
125
  export function describeExposed(config, options) {
113
126
  const result = [];
@@ -1,5 +1,5 @@
1
1
  import type { NormalizedFederationConfig } from '../domain/config/federation-config.contract.js';
2
- import type { SharedInfo } from '../domain/core/federation-info.contract.js';
2
+ import type { IntegrityMap, SharedInfo } from '../domain/core/federation-info.contract.js';
3
3
  import { type NormalizedFederationOptions } from '../domain/core/federation-options.contract.js';
4
4
  import type { NormalizedExternalConfig } from '../domain/config/external-config.contract.js';
5
5
  export declare function bundleShared(sharedBundles: Record<string, NormalizedExternalConfig>, config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], buildOptions: {
@@ -9,4 +9,5 @@ export declare function bundleShared(sharedBundles: Record<string, NormalizedExt
9
9
  }): Promise<{
10
10
  externals: SharedInfo[];
11
11
  chunks?: Record<string, string[]>;
12
+ integrity?: IntegrityMap;
12
13
  }>;
@@ -9,6 +9,7 @@ import { toChunkImport } from '../domain/core/chunk.js';
9
9
  import { cacheEntry, getChecksum, getFilename } from '../utils/cache-persistence.js';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { getBuildAdapter } from './build-adapter.js';
12
+ import { integrityForFile } from '../utils/hash-file.js';
12
13
  export async function bundleShared(sharedBundles, config, fedOptions, externals, buildOptions) {
13
14
  const checksum = getChecksum(sharedBundles, fedOptions.dev ? '1' : '0');
14
15
  const folder = fedOptions.packageJson
@@ -20,7 +21,15 @@ export async function bundleShared(sharedBundles, config, fedOptions, externals,
20
21
  if (cacheMetadata) {
21
22
  logger.debug(`Checksum of ${buildOptions.bundleName} matched, Skipped artifact bundling`);
22
23
  bundleCache.copyFiles(path.join(fedOptions.workspaceRoot, fedOptions.outputPath));
23
- return { externals: cacheMetadata.externals, chunks: cacheMetadata.chunks };
24
+ let integrity = cacheMetadata.integrity;
25
+ if (fedOptions.integrity && !integrity) {
26
+ integrity = computeIntegrityForFiles(cacheMetadata.files, fedOptions.federationCache.cachePath);
27
+ }
28
+ return {
29
+ externals: cacheMetadata.externals,
30
+ chunks: cacheMetadata.chunks,
31
+ integrity,
32
+ };
24
33
  }
25
34
  }
26
35
  bundleCache.clear();
@@ -111,14 +120,32 @@ export async function bundleShared(sharedBundles, config, fedOptions, externals,
111
120
  else {
112
121
  addChunksToResult(chunks, result);
113
122
  }
123
+ const persistedFiles = bundleResult.map(r => r.fileName.split(path.sep).pop() ?? r.fileName);
124
+ // Must run after rewriteImports so SRI matches the bytes copied to dist.
125
+ const integrity = fedOptions.integrity
126
+ ? computeIntegrityForFiles(persistedFiles, fedOptions.federationCache.cachePath)
127
+ : undefined;
114
128
  bundleCache.persist({
115
129
  checksum,
116
130
  externals: result,
117
- files: bundleResult.map(r => r.fileName.split(path.sep).pop() ?? r.fileName),
131
+ files: persistedFiles,
118
132
  chunks: exportedChunks,
133
+ integrity,
119
134
  });
120
135
  bundleCache.copyFiles(path.join(fedOptions.workspaceRoot, fedOptions.outputPath));
121
- return { externals: result, chunks: exportedChunks };
136
+ return { externals: result, chunks: exportedChunks, integrity };
137
+ }
138
+ function computeIntegrityForFiles(files, baseDir) {
139
+ const integrity = {};
140
+ for (const file of files) {
141
+ if (file.endsWith('.map'))
142
+ continue;
143
+ const fullPath = path.join(baseDir, file);
144
+ if (!fs.existsSync(fullPath))
145
+ continue;
146
+ integrity[path.basename(file)] = integrityForFile(fullPath);
147
+ }
148
+ return integrity;
122
149
  }
123
150
  function rewriteImports(cachedFiles, cachePath) {
124
151
  const newSourceFiles = cachedFiles.filter(cf => isSourceFile(cf));
@@ -1,8 +1,9 @@
1
1
  import type { FederationCache } from '../domain/core/federation-cache.contract.js';
2
- import type { ChunkInfo, SharedInfo } from '../domain/core/federation-info.contract.js';
2
+ import type { ChunkInfo, IntegrityMap, SharedInfo } from '../domain/core/federation-info.contract.js';
3
3
  export declare function createFederationCache(cachePath: string): FederationCache<undefined>;
4
4
  export declare function createFederationCache<TBundlerCache>(cachePath: string, bundlerCache: TBundlerCache): FederationCache<TBundlerCache>;
5
- export declare function addExternalsToCache(cache: FederationCache, { externals, chunks }: {
5
+ export declare function addExternalsToCache(cache: FederationCache, { externals, chunks, integrity, }: {
6
6
  externals: SharedInfo[];
7
7
  chunks?: ChunkInfo;
8
+ integrity?: IntegrityMap;
8
9
  }): void;
@@ -1,11 +1,16 @@
1
1
  export function createFederationCache(cachePath, bundlerCache) {
2
2
  return { externals: [], cachePath, bundlerCache };
3
3
  }
4
- export function addExternalsToCache(cache, { externals, chunks }) {
4
+ export function addExternalsToCache(cache, { externals, chunks, integrity, }) {
5
5
  cache.externals.push(...externals);
6
6
  if (chunks) {
7
7
  if (!cache.chunks)
8
8
  cache.chunks = {};
9
9
  cache.chunks = { ...cache.chunks, ...chunks };
10
10
  }
11
+ if (integrity) {
12
+ if (!cache.integrity)
13
+ cache.integrity = {};
14
+ cache.integrity = { ...cache.integrity, ...integrity };
15
+ }
11
16
  }
@@ -31,7 +31,13 @@ export async function rebuildForFederation(config, fedOptions, externals, modifi
31
31
  if (artifactInfo?.chunks) {
32
32
  federationInfo.chunks = { ...(federationInfo.chunks ?? {}), ...artifactInfo?.chunks };
33
33
  }
34
+ if (fedOptions.integrity) {
35
+ federationInfo.integrity = {
36
+ ...(federationCache.integrity ?? {}),
37
+ ...(artifactInfo?.integrity ?? {}),
38
+ };
39
+ }
34
40
  writeFederationInfo(federationInfo, fedOptions);
35
- writeImportMap(federationCache, fedOptions);
41
+ writeImportMap(federationCache, fedOptions, federationInfo.integrity);
36
42
  return federationInfo;
37
43
  }
@@ -1,6 +1,6 @@
1
- import type { ChunkInfo, SharedInfo } from '../domain/core/federation-info.contract.js';
1
+ import type { ChunkInfo, IntegrityMap, SharedInfo } from '../domain/core/federation-info.contract.js';
2
2
  import type { FederationOptions } from '../domain/core/federation-options.contract.js';
3
3
  export declare function writeImportMap(sharedInfo: {
4
4
  externals: SharedInfo[];
5
5
  chunks?: ChunkInfo;
6
- }, fedOption: FederationOptions): void;
6
+ }, fedOption: FederationOptions, fileIntegrity?: IntegrityMap): void;
@@ -1,7 +1,7 @@
1
1
  import * as path from 'path';
2
2
  import * as fs from 'fs';
3
3
  import { toChunkImport } from '../domain/core/chunk.js';
4
- export function writeImportMap(sharedInfo, fedOption) {
4
+ export function writeImportMap(sharedInfo, fedOption, fileIntegrity) {
5
5
  const imports = sharedInfo.externals.reduce((acc, cur) => {
6
6
  return {
7
7
  ...acc,
@@ -17,6 +17,17 @@ export function writeImportMap(sharedInfo, fedOption) {
17
17
  });
18
18
  }
19
19
  const importMap = { imports };
20
+ if (fileIntegrity) {
21
+ const integrity = {};
22
+ for (const url of Object.values(imports)) {
23
+ const sri = fileIntegrity[url];
24
+ if (sri)
25
+ integrity[url] = sri;
26
+ }
27
+ if (Object.keys(integrity).length > 0) {
28
+ importMap.integrity = integrity;
29
+ }
30
+ }
20
31
  const importMapPath = path.join(fedOption.workspaceRoot, fedOption.outputPath, 'importmap.json');
21
32
  fs.writeFileSync(importMapPath, JSON.stringify(importMap, null, 2));
22
33
  }
@@ -1,7 +1,8 @@
1
- import type { ChunkInfo, SharedInfo } from './federation-info.contract.js';
1
+ import type { ChunkInfo, IntegrityMap, SharedInfo } from './federation-info.contract.js';
2
2
  export type FederationCache<TBundlerCache = unknown> = {
3
3
  externals: SharedInfo[];
4
4
  chunks?: ChunkInfo;
5
+ integrity?: IntegrityMap;
5
6
  bundlerCache: TBundlerCache;
6
7
  cachePath: string;
7
8
  };
@@ -3,6 +3,7 @@ export interface FederationInfo {
3
3
  exposes: ExposesInfo[];
4
4
  shared: SharedInfo[];
5
5
  chunks?: Record<string, string[]>;
6
+ integrity?: IntegrityMap;
6
7
  buildNotificationsEndpoint?: string;
7
8
  }
8
9
  export type SharedInfo = {
@@ -19,6 +20,7 @@ export type SharedInfo = {
19
20
  };
20
21
  };
21
22
  export type ChunkInfo = Record<string, string[]>;
23
+ export type IntegrityMap = Record<string, string>;
22
24
  export interface ExposesInfo {
23
25
  key: string;
24
26
  outFileName: string;
@@ -30,4 +32,5 @@ export interface ArtifactInfo {
30
32
  mappings: SharedInfo[];
31
33
  exposes: ExposesInfo[];
32
34
  chunks?: ChunkInfo;
35
+ integrity?: IntegrityMap;
33
36
  }
@@ -13,6 +13,7 @@ export interface FederationOptions {
13
13
  packageJson?: string;
14
14
  entryPoints?: string[];
15
15
  buildNotifications?: BuildNotificationOptions;
16
+ integrity?: boolean;
16
17
  }
17
18
  export interface NormalizedFederationOptions<TBundlerCache = unknown> extends FederationOptions {
18
19
  federationCache: FederationCache<TBundlerCache>;
@@ -1,4 +1,4 @@
1
- export type { SharedInfo, FederationInfo, ExposesInfo, ArtifactInfo, ChunkInfo, } from './federation-info.contract.js';
1
+ export type { SharedInfo, FederationInfo, ExposesInfo, ArtifactInfo, ChunkInfo, IntegrityMap, } from './federation-info.contract.js';
2
2
  export { type BuildNotificationOptions, BuildNotificationType, } from './build-notification-options.contract.js';
3
3
  export type { FederationOptions, NormalizedFederationOptions, } from './federation-options.contract.js';
4
4
  export type { EntryPoint, NFBuildAdapterOptions, NFBuildAdapter, NFBuildAdapterResult, NFBuildAdapterContext, } from './build-adapter.contract.js';
@@ -1,21 +1,19 @@
1
1
  import type { NormalizedExternalConfig } from '../domain/config/external-config.contract.js';
2
- import type { ChunkInfo, SharedInfo } from '../domain/core/federation-info.contract.js';
2
+ import type { ChunkInfo, IntegrityMap, SharedInfo } from '../domain/core/federation-info.contract.js';
3
3
  export declare const getDefaultCachePath: (workspaceRoot: string) => string;
4
4
  export declare const getFilename: (title: string, dev?: boolean) => string;
5
5
  export declare const getChecksum: (shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0") => string;
6
+ type CacheMetadata = {
7
+ checksum: string;
8
+ externals: SharedInfo[];
9
+ chunks?: ChunkInfo;
10
+ integrity?: IntegrityMap;
11
+ files: string[];
12
+ };
6
13
  export declare const cacheEntry: (pathToCache: string, fileName: string) => {
7
- getMetadata: (checksum: string) => {
8
- checksum: string;
9
- externals: SharedInfo[];
10
- chunks?: ChunkInfo;
11
- files: string[];
12
- } | undefined;
13
- persist: (payload: {
14
- checksum: string;
15
- externals: SharedInfo[];
16
- chunks?: ChunkInfo;
17
- files: string[];
18
- }) => void;
14
+ getMetadata: (checksum: string) => CacheMetadata | undefined;
15
+ persist: (payload: CacheMetadata) => void;
19
16
  copyFiles: (fullOutputPath: string) => void;
20
17
  clear: () => void;
21
18
  };
19
+ export {};
@@ -1 +1,3 @@
1
1
  export declare function hashFile(fileName: string): string;
2
+ export type SriAlgorithm = 'sha256' | 'sha384' | 'sha512';
3
+ export declare function integrityForFile(fileName: string, algorithm?: SriAlgorithm): string;
@@ -6,3 +6,8 @@ export function hashFile(fileName) {
6
6
  hashSum.update(fileBuffer);
7
7
  return hashSum.digest('hex');
8
8
  }
9
+ export function integrityForFile(fileName, algorithm = 'sha384') {
10
+ const fileBuffer = fs.readFileSync(fileName);
11
+ const hash = crypto.createHash(algorithm).update(fileBuffer).digest('base64');
12
+ return `${algorithm}-${hash}`;
13
+ }