@softarc/native-federation 4.0.0 → 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.0",
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';
@@ -55,7 +56,10 @@ export async function bundleExposedAndMappings(config, fedOptions, externals, mo
55
56
  }
56
57
  throw error;
57
58
  }
58
- const resultMap = createBuildResultMap(result, hash);
59
+ const resultMap = createBuildResultMap(result, hash, [
60
+ ...shared.map(s => s.outName),
61
+ ...exposes.map(e => e.outName),
62
+ ]);
59
63
  const sharedResult = [];
60
64
  const entryFiles = [];
61
65
  // Pick shared-mappings
@@ -97,14 +101,26 @@ export async function bundleExposedAndMappings(config, fedOptions, externals, mo
97
101
  .forEach(f => popFromResultMap(resultMap, f));
98
102
  // Process remaining chunks and lazy loaded internal modules
99
103
  let exportedChunks = undefined;
104
+ const chunkPaths = [];
100
105
  if (config.chunks && config.features.denseChunking) {
101
106
  for (const entryFile of entryFiles)
102
107
  rewriteChunkImports(entryFile);
108
+ chunkPaths.push(...Object.values(resultMap));
103
109
  exportedChunks = {
104
- ['mapping-or-exposed']: Object.values(resultMap).map(chunk => path.basename(chunk)),
110
+ ['mapping-or-exposed']: chunkPaths.map(chunk => path.basename(chunk)),
105
111
  };
106
112
  }
107
- 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 };
108
124
  }
109
125
  export function describeExposed(config, options) {
110
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,4 +1,4 @@
1
1
  import type { NFBuildAdapterResult } from '../domain/core/build-adapter.contract.js';
2
- export declare function createBuildResultMap(buildResult: NFBuildAdapterResult[], isHashed: boolean): Record<string, string>;
2
+ export declare function createBuildResultMap(buildResult: NFBuildAdapterResult[], isHashed: boolean, expectedNames?: string[]): Record<string, string>;
3
3
  export declare function lookupInResultMap(map: Record<string, string>, requestName: string): string;
4
4
  export declare function popFromResultMap(map: Record<string, string>, requestName: string): string;
@@ -1,16 +1,17 @@
1
1
  import path from 'path';
2
- export function createBuildResultMap(buildResult, isHashed) {
2
+ function stripHash(fileName) {
3
+ const start = fileName.lastIndexOf('-');
4
+ const end = fileName.lastIndexOf('.');
5
+ if (start < 0 || end < 0 || start > end)
6
+ return fileName;
7
+ return fileName.substring(0, start) + fileName.substring(end);
8
+ }
9
+ export function createBuildResultMap(buildResult, isHashed, expectedNames = []) {
10
+ const expected = new Set(expectedNames.map(n => path.basename(n)));
3
11
  const map = {};
4
12
  for (const item of buildResult) {
5
13
  const resultName = path.basename(item.fileName);
6
- let requestName = resultName;
7
- if (isHashed) {
8
- const start = resultName.lastIndexOf('-');
9
- const end = resultName.lastIndexOf('.');
10
- const part1 = resultName.substring(0, start);
11
- const part2 = resultName.substring(end);
12
- requestName = part1 + part2;
13
- }
14
+ const requestName = isHashed && expected.has(stripHash(resultName)) ? stripHash(resultName) : resultName;
14
15
  map[requestName] = item.fileName;
15
16
  }
16
17
  return map;
@@ -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
+ }