@softarc/native-federation 4.0.1 → 4.1.1

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.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "dependencies": {
@@ -15,7 +15,7 @@ export function withNativeFederation(config) {
15
15
  skip,
16
16
  externals: config.externals ?? [],
17
17
  features: {
18
- mappingVersion: config.features?.mappingVersion ?? false,
18
+ mappingVersion: config.features?.mappingVersion ?? true,
19
19
  ignoreUnusedDeps: config.features?.ignoreUnusedDeps ?? true,
20
20
  denseChunking: config.features?.denseChunking ?? false,
21
21
  },
@@ -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) {
@@ -4,3 +4,4 @@ import { type NormalizedFederationOptions } from '../domain/core/federation-opti
4
4
  export declare function bundleExposedAndMappings(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], modifiedFiles?: string[], signal?: AbortSignal): Promise<ArtifactInfo>;
5
5
  export declare function describeExposed(config: NormalizedFederationConfig, options: NormalizedFederationOptions): Array<ExposesInfo>;
6
6
  export declare function describeSharedMappings(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions): Array<SharedInfo>;
7
+ export declare function getMappingVersion(fileName: string, workspaceRoot: string): string;
@@ -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';
@@ -64,19 +65,7 @@ export async function bundleExposedAndMappings(config, fedOptions, externals, mo
64
65
  // Pick shared-mappings
65
66
  for (const item of shared) {
66
67
  const distEntryFile = popFromResultMap(resultMap, item.outName);
67
- sharedResult.push({
68
- packageName: item.key,
69
- outFileName: path.basename(distEntryFile),
70
- requiredVersion: '',
71
- singleton: true,
72
- strictVersion: false,
73
- version: config.features.mappingVersion ? getMappingVersion(item.fileName) : '',
74
- dev: !fedOptions.dev
75
- ? undefined
76
- : {
77
- entryPoint: normalize(path.normalize(item.fileName)),
78
- },
79
- });
68
+ sharedResult.push(toSharedMappingInfo(item.fileName, item.key, path.basename(distEntryFile), config, fedOptions));
80
69
  entryFiles.push(distEntryFile);
81
70
  }
82
71
  const exposedResult = [];
@@ -100,14 +89,26 @@ export async function bundleExposedAndMappings(config, fedOptions, externals, mo
100
89
  .forEach(f => popFromResultMap(resultMap, f));
101
90
  // Process remaining chunks and lazy loaded internal modules
102
91
  let exportedChunks = undefined;
92
+ const chunkPaths = [];
103
93
  if (config.chunks && config.features.denseChunking) {
104
94
  for (const entryFile of entryFiles)
105
95
  rewriteChunkImports(entryFile);
96
+ chunkPaths.push(...Object.values(resultMap));
106
97
  exportedChunks = {
107
- ['mapping-or-exposed']: Object.values(resultMap).map(chunk => path.basename(chunk)),
98
+ ['mapping-or-exposed']: chunkPaths.map(chunk => path.basename(chunk)),
108
99
  };
109
100
  }
110
- return { mappings: sharedResult, exposes: exposedResult, chunks: exportedChunks };
101
+ // Must run after rewriteChunkImports so SRI matches the final on-disk bytes.
102
+ let integrity;
103
+ if (fedOptions.integrity) {
104
+ integrity = {};
105
+ for (const filePath of [...entryFiles, ...chunkPaths]) {
106
+ if (!fs.existsSync(filePath))
107
+ continue;
108
+ integrity[path.basename(filePath)] = integrityForFile(filePath);
109
+ }
110
+ }
111
+ return { mappings: sharedResult, exposes: exposedResult, chunks: exportedChunks, integrity };
111
112
  }
112
113
  export function describeExposed(config, options) {
113
114
  const result = [];
@@ -128,30 +129,46 @@ export function describeExposed(config, options) {
128
129
  export function describeSharedMappings(config, fedOptions) {
129
130
  const result = [];
130
131
  for (const [mappedPath, mappedImport] of Object.entries(config.sharedMappings)) {
131
- result.push({
132
- packageName: mappedImport,
133
- outFileName: '',
134
- requiredVersion: '',
135
- singleton: true,
136
- strictVersion: false,
137
- version: config.features.mappingVersion ? getMappingVersion(mappedPath) : '',
138
- dev: !fedOptions.dev
139
- ? undefined
140
- : {
141
- entryPoint: normalize(path.normalize(mappedPath)),
142
- },
143
- });
132
+ result.push(toSharedMappingInfo(mappedPath, mappedImport, '', config, fedOptions));
144
133
  }
145
134
  return result;
146
135
  }
147
- function getMappingVersion(fileName) {
148
- const entryFileDir = path.dirname(fileName);
149
- const cand1 = path.join(entryFileDir, 'package.json');
150
- const cand2 = path.join(path.dirname(entryFileDir), 'package.json');
151
- const packageJsonPath = [cand1, cand2].find(cand => fs.existsSync(cand));
152
- if (packageJsonPath) {
153
- const json = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
154
- return json.version ?? '';
136
+ function toSharedMappingInfo(mappedPath, mappedImport, outFileName, config, fedOptions) {
137
+ const mappingVersion = config.features.mappingVersion
138
+ ? getMappingVersion(mappedPath, fedOptions.workspaceRoot)
139
+ : '';
140
+ return {
141
+ packageName: mappedImport,
142
+ outFileName,
143
+ requiredVersion: mappingVersion.length > 0 ? '~' + mappingVersion : '',
144
+ singleton: true,
145
+ strictVersion: config.features.mappingVersion,
146
+ version: mappingVersion,
147
+ dev: !fedOptions.dev
148
+ ? undefined
149
+ : {
150
+ entryPoint: normalize(path.normalize(mappedPath)),
151
+ },
152
+ };
153
+ }
154
+ export function getMappingVersion(fileName, workspaceRoot) {
155
+ const resolvedRoot = path.resolve(workspaceRoot);
156
+ let dir = path.dirname(path.resolve(fileName));
157
+ while (true) {
158
+ const candidate = path.join(dir, 'package.json');
159
+ try {
160
+ const json = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
161
+ if (typeof json.version === 'string' && json.version)
162
+ return json.version;
163
+ }
164
+ catch (err) {
165
+ if (err.code !== 'ENOENT') {
166
+ logger.warn(`[getMappingVersion] Failed to parse ${candidate}: ${err.message}`);
167
+ }
168
+ }
169
+ const parent = path.dirname(dir);
170
+ if (dir === resolvedRoot || parent === dir)
171
+ return '';
172
+ dir = parent;
155
173
  }
156
- return '';
157
174
  }
@@ -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
+ }