@softarc/native-federation 4.3.0 → 4.3.2

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.
Files changed (37) hide show
  1. package/README.md +2 -2
  2. package/dist/internal/browser.d.ts +6 -0
  3. package/dist/internal/browser.js +9 -0
  4. package/dist/internal.d.ts +9 -6
  5. package/dist/internal.js +21 -6
  6. package/dist/lib/config/with-native-federation.js +2 -1
  7. package/dist/lib/core/build/build-for-federation.d.ts +7 -0
  8. package/dist/lib/core/build/build-for-federation.js +32 -134
  9. package/dist/lib/core/build/bundle-shared.d.ts +7 -0
  10. package/dist/lib/core/build/bundle-shared.js +38 -7
  11. package/dist/lib/core/build/rebuild-for-federation.d.ts +8 -0
  12. package/dist/lib/core/build/rebuild-for-federation.js +26 -0
  13. package/dist/lib/core/build/resolve-shared-dirs.d.ts +16 -0
  14. package/dist/lib/core/build/resolve-shared-dirs.js +66 -0
  15. package/dist/lib/core/build/shared-bundle-plan.d.ts +22 -0
  16. package/dist/lib/core/build/shared-bundle-plan.js +70 -0
  17. package/dist/lib/core/build/synthesize-cjs-exports.d.ts +12 -0
  18. package/dist/lib/core/build/synthesize-cjs-exports.js +58 -0
  19. package/dist/lib/core/cache/cache-persistence.d.ts +2 -2
  20. package/dist/lib/core/cache/cache-persistence.js +7 -4
  21. package/dist/lib/core/federation-builder.d.ts +2 -1
  22. package/dist/lib/core/federation-builder.js +3 -0
  23. package/dist/lib/core/output/densify-externals.d.ts +1 -0
  24. package/dist/lib/core/output/densify-externals.js +21 -4
  25. package/dist/lib/core/output/write-federation-info.js +1 -1
  26. package/dist/lib/domain/config/federation-config.contract.d.ts +2 -0
  27. package/dist/lib/domain/core/federation-info.contract.d.ts +1 -0
  28. package/dist/lib/domain/utils/file-watcher.contract.d.ts +7 -1
  29. package/dist/lib/domain/utils/io-port.contract.d.ts +14 -3
  30. package/dist/lib/utils/file-watcher.d.ts +1 -1
  31. package/dist/lib/utils/file-watcher.js +23 -6
  32. package/dist/lib/utils/io/node-io-adapter.js +77 -21
  33. package/dist/lib/utils/package/cjs-named-exports.d.ts +21 -0
  34. package/dist/lib/utils/package/cjs-named-exports.js +36 -0
  35. package/dist/lib/utils/package/esm-detection.d.ts +13 -0
  36. package/dist/lib/utils/package/esm-detection.js +29 -0
  37. package/package.json +9 -3
package/README.md CHANGED
@@ -614,7 +614,7 @@ For a zero-build integration, declare your remotes in a manifest and include the
614
614
  <script>
615
615
  window.addEventListener(
616
616
  'mfe-loader-available',
617
- (e) => {
617
+ e => {
618
618
  e.detail.loadRemoteModule('team/mfe1', './Button');
619
619
  e.detail.loadRemoteModule('team/mfe2', './Header');
620
620
  },
@@ -623,7 +623,7 @@ For a zero-build integration, declare your remotes in a manifest and include the
623
623
  </script>
624
624
 
625
625
  <!-- Include the orchestrator -->
626
- <script src="https://unpkg.com/@softarc/native-federation-orchestrator@4.3.0/quickstart.mjs"></script>
626
+ <script src="https://unpkg.com/@softarc/native-federation-orchestrator@4.5.1/quickstart.mjs"></script>
627
627
  ```
628
628
 
629
629
  The `mfe-loader-available` event signals that the orchestrator has fetched the
@@ -0,0 +1,6 @@
1
+ export * from '../lib/utils/errors.js';
2
+ export { densifyExternals, toDenseSharedInfoFormat } from '../lib/core/output/densify-externals.js';
3
+ export { isInSkipList, prepareSkipList } from '../lib/config/default-skip-list.js';
4
+ export type { PathToImport } from '../lib/domain/utils/mapped-path.contract.js';
5
+ export type { NormalizedExternalConfig, NormalizedSharedExternalsConfig, } from '../lib/domain/config/external-config.contract.js';
6
+ export type { NormalizedFederationConfig } from '../lib/domain/config/federation-config.contract.js';
@@ -0,0 +1,9 @@
1
+ export * from "../lib/utils/errors.js";
2
+ import { densifyExternals, toDenseSharedInfoFormat } from "../lib/core/output/densify-externals.js";
3
+ import { isInSkipList, prepareSkipList } from "../lib/config/default-skip-list.js";
4
+ export {
5
+ densifyExternals,
6
+ isInSkipList,
7
+ prepareSkipList,
8
+ toDenseSharedInfoFormat
9
+ };
@@ -1,15 +1,18 @@
1
+ /**
2
+ * Internal API's of the native-federation builder.
3
+ *
4
+ * Might change between minors.
5
+ */
6
+ export * from './internal/browser.js';
1
7
  export * from './lib/core/build/build-result-map.js';
2
8
  export { hashFile } from './lib/utils/hash-file.js';
3
- export * from './lib/utils/errors.js';
4
9
  export { logger, setLogLevel } from './lib/utils/logger.js';
5
- export type { PathToImport } from './lib/domain/utils/mapped-path.contract.js';
6
10
  export { RebuildQueue, type TrackResult } from './lib/core/rebuild-queue.js';
7
11
  export { writeImportMap } from './lib/core/output/write-import-map.js';
8
- export type { NormalizedExternalConfig, NormalizedSharedExternalsConfig, } from './lib/domain/config/external-config.contract.js';
9
- export type { NormalizedFederationConfig } from './lib/domain/config/federation-config.contract.js';
10
12
  export { getDefaultCachePath, getChecksum } from './lib/core/cache/cache-persistence.js';
11
13
  export { isESMExport, type ExportCondition, type ExportEntry, } from './lib/utils/package/package-info.js';
12
- export { isInSkipList, prepareSkipList } from './lib/config/default-skip-list.js';
13
- export { densifyExternals } from './lib/core/output/densify-externals.js';
14
+ export { isCjsCandidate, classifyByExtension, hasEsmSyntax, type ModuleFormat, } from './lib/utils/package/esm-detection.js';
15
+ export { isIdentifierName, planCjsWrap, buildSyntheticCjsEntry, isEsmInteropError, } from './lib/utils/package/cjs-named-exports.js';
14
16
  export type { NfFileWatcher, NfFileWatcherOptions, } from './lib/domain/utils/file-watcher.contract.js';
15
17
  export { syncNfFileWatcher, createNfWatcher } from './lib/utils/file-watcher.js';
18
+ export { linkedSharedDirs } from './lib/core/build/resolve-shared-dirs.js';
package/dist/internal.js CHANGED
@@ -1,6 +1,6 @@
1
+ export * from "./internal/browser.js";
1
2
  export * from "./lib/core/build/build-result-map.js";
2
3
  import { hashFile } from "./lib/utils/hash-file.js";
3
- export * from "./lib/utils/errors.js";
4
4
  import { logger, setLogLevel } from "./lib/utils/logger.js";
5
5
  import { RebuildQueue } from "./lib/core/rebuild-queue.js";
6
6
  import { writeImportMap } from "./lib/core/output/write-import-map.js";
@@ -8,20 +8,35 @@ import { getDefaultCachePath, getChecksum } from "./lib/core/cache/cache-persist
8
8
  import {
9
9
  isESMExport
10
10
  } from "./lib/utils/package/package-info.js";
11
- import { isInSkipList, prepareSkipList } from "./lib/config/default-skip-list.js";
12
- import { densifyExternals } from "./lib/core/output/densify-externals.js";
11
+ import {
12
+ isCjsCandidate,
13
+ classifyByExtension,
14
+ hasEsmSyntax
15
+ } from "./lib/utils/package/esm-detection.js";
16
+ import {
17
+ isIdentifierName,
18
+ planCjsWrap,
19
+ buildSyntheticCjsEntry,
20
+ isEsmInteropError
21
+ } from "./lib/utils/package/cjs-named-exports.js";
13
22
  import { syncNfFileWatcher, createNfWatcher } from "./lib/utils/file-watcher.js";
23
+ import { linkedSharedDirs } from "./lib/core/build/resolve-shared-dirs.js";
14
24
  export {
15
25
  RebuildQueue,
26
+ buildSyntheticCjsEntry,
27
+ classifyByExtension,
16
28
  createNfWatcher,
17
- densifyExternals,
18
29
  getChecksum,
19
30
  getDefaultCachePath,
31
+ hasEsmSyntax,
20
32
  hashFile,
33
+ isCjsCandidate,
21
34
  isESMExport,
22
- isInSkipList,
35
+ isEsmInteropError,
36
+ isIdentifierName,
37
+ linkedSharedDirs,
23
38
  logger,
24
- prepareSkipList,
39
+ planCjsWrap,
25
40
  setLogLevel,
26
41
  syncNfFileWatcher,
27
42
  writeImportMap
@@ -20,7 +20,8 @@ function withNativeFederation(config) {
20
20
  ignoreUnusedDeps: config.features?.ignoreUnusedDeps ?? true,
21
21
  denseChunking: config.features?.denseChunking ?? false,
22
22
  denseExternals: config.features?.denseExternals ?? false,
23
- integrityHashes: config.features?.integrityHashes ?? false
23
+ integrityHashes: config.features?.integrityHashes ?? false,
24
+ synthesizeCjsExports: config.features?.synthesizeCjsExports ?? true
24
25
  },
25
26
  ...config.shareScope && { shareScope: config.shareScope }
26
27
  };
@@ -1,4 +1,11 @@
1
1
  import type { FederationInfo } from '../../domain/core/federation-info.contract.js';
2
2
  import type { NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
3
3
  import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
4
+ import { type SharedBundlePlan } from './shared-bundle-plan.js';
4
5
  export declare function buildForFederation(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], signal?: AbortSignal): Promise<FederationInfo>;
6
+ /**
7
+ * Bundles shared/separate externals per plan and populates the federation cache.
8
+ * Shared bundles run sequentially (with signal checks); separate bundles run in
9
+ * parallel. Shared by the initial build and the watch rebuild.
10
+ */
11
+ export declare function executeSharedBundlePlans(plans: SharedBundlePlan[], config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, signal?: AbortSignal): Promise<void>;
@@ -8,9 +8,9 @@ import { densifyExternals } from "../output/densify-externals.js";
8
8
  import { writeFederationInfo } from "../output/write-federation-info.js";
9
9
  import { writeImportMap } from "../output/write-import-map.js";
10
10
  import { logger } from "../../utils/logger.js";
11
- import { inferPackageFromSecondary, normalizePackageName } from "../../utils/normalize.js";
12
11
  import { AbortedError } from "../../utils/errors.js";
13
12
  import { addExternalsToCache } from "../cache/federation-cache.js";
13
+ import { planSharedBundles } from "./shared-bundle-plan.js";
14
14
  import path from "path";
15
15
  async function buildForFederation(config, fedOptions, externals, signal) {
16
16
  fedOptions.federationCache.cachePath = path.join(
@@ -19,66 +19,7 @@ async function buildForFederation(config, fedOptions, externals, signal) {
19
19
  );
20
20
  logger.info("Building federation artifacts");
21
21
  logger.notice("Skip packages you don't want to share in your federation config");
22
- const { sharedBrowser, sharedServer, separateBrowser, separateServer } = splitShared(
23
- config.shared
24
- );
25
- if (Object.keys(sharedBrowser).length > 0) {
26
- logger.info(`Bundling external npm packages with bundle type 'browser-shared'`);
27
- const start2 = process.hrtime();
28
- const sharedPackageInfoBrowser = await bundleShared(
29
- sharedBrowser,
30
- config,
31
- fedOptions,
32
- externals,
33
- { platform: "browser", bundleName: "browser-shared", chunks: config.chunks }
34
- );
35
- logger.measure(start2, "Step 2.1) Bundling all shared browser externals");
36
- addExternalsToCache(fedOptions.federationCache, sharedPackageInfoBrowser);
37
- if (signal?.aborted) throw new AbortedError("[buildForFederation] After shared-browser bundle");
38
- }
39
- if (Object.keys(sharedServer).length > 0) {
40
- logger.info(`Bundling external npm packages with bundle type 'server-shared'`);
41
- const start2 = process.hrtime();
42
- const sharedPackageInfoServer = await bundleShared(
43
- sharedServer,
44
- config,
45
- fedOptions,
46
- externals,
47
- { platform: "node", bundleName: "node-shared", chunks: config.chunks }
48
- );
49
- logger.measure(start2, "Step 2.1) Bundling all shared node externals");
50
- addExternalsToCache(fedOptions.federationCache, sharedPackageInfoServer);
51
- if (signal?.aborted) throw new AbortedError("[buildForFederation] After shared-node bundle");
52
- }
53
- if (Object.keys(separateBrowser).length > 0) {
54
- logger.info(`Bundling external npm packages with bundle type 'browser-separate'`);
55
- const start2 = process.hrtime();
56
- const separatePackageInfoBrowser = await bundleSeparatePackages(
57
- separateBrowser,
58
- externals,
59
- config,
60
- fedOptions,
61
- { platform: "browser" }
62
- );
63
- logger.measure(start2, "Step 2.2) Bundling all separate browser external packages");
64
- addExternalsToCache(fedOptions.federationCache, separatePackageInfoBrowser);
65
- if (signal?.aborted)
66
- throw new AbortedError("[buildForFederation] After separate-browser bundle");
67
- }
68
- if (Object.keys(separateServer).length > 0) {
69
- logger.info(`Bundling external npm packages with bundle type 'node-separate'`);
70
- const start2 = process.hrtime();
71
- const separatePackageInfoServer = await bundleSeparatePackages(
72
- separateServer,
73
- externals,
74
- config,
75
- fedOptions,
76
- { platform: "node" }
77
- );
78
- logger.measure(start2, "Step 2.2) Bundling all separate node external packages");
79
- addExternalsToCache(fedOptions.federationCache, separatePackageInfoServer);
80
- }
81
- if (signal?.aborted) throw new AbortedError("[buildForFederation] After separate-node bundle");
22
+ await executeSharedBundlePlans(planSharedBundles(config, externals), config, fedOptions, signal);
82
23
  const start = process.hrtime();
83
24
  const artifactInfo = await bundleExposedAndMappings(
84
25
  config,
@@ -122,81 +63,38 @@ async function buildForFederation(config, fedOptions, externals, signal) {
122
63
  writeImportMap(fedOptions.federationCache, fedOptions, federationInfo.integrity);
123
64
  return federationInfo;
124
65
  }
125
- async function bundleSeparatePackages(separateBrowser, externals, config, fedOptions, buildOptions) {
126
- const groupedByPackage = {};
127
- for (const [key, shared] of Object.entries(separateBrowser)) {
128
- const packageName = shared.build === "separate" ? key : inferPackageFromSecondary(key);
129
- if (!groupedByPackage[packageName]) {
130
- groupedByPackage[packageName] = {
131
- chunks: shared.chunks,
132
- entries: {}
133
- };
134
- }
135
- groupedByPackage[packageName].entries[key] = shared;
66
+ async function executeSharedBundlePlans(plans, config, fedOptions, signal) {
67
+ for (const plan of plans.filter((p) => p.kind === "shared")) {
68
+ logger.info(`Bundling external npm packages with bundle type '${plan.bundleName}'`);
69
+ const start = process.hrtime();
70
+ const info = await bundleShared(plan.entries, config, fedOptions, plan.externals, {
71
+ platform: plan.platform,
72
+ bundleName: plan.bundleName,
73
+ chunks: plan.chunks
74
+ });
75
+ logger.measure(start, `Step 2.1) Bundling '${plan.bundleName}' externals`);
76
+ addExternalsToCache(fedOptions.federationCache, info);
77
+ if (signal?.aborted)
78
+ throw new AbortedError(`[buildForFederation] After ${plan.bundleName} bundle`);
136
79
  }
137
- const bundlePromises = Object.entries(groupedByPackage).map(
138
- async ([packageName, packageConfig]) => {
139
- return bundleShared(
140
- packageConfig.entries,
141
- config,
142
- fedOptions,
143
- externals.filter((e) => !e.startsWith(packageName)),
144
- {
145
- platform: buildOptions.platform,
146
- chunks: packageConfig.chunks,
147
- bundleName: `${buildOptions.platform}-${normalizePackageName(packageName)}`
148
- }
149
- );
150
- }
151
- );
152
- const buildResults = await Promise.all(bundlePromises);
153
- return buildResults.reduce(
154
- (acc, r) => {
155
- let chunks = acc.chunks;
156
- if (r.chunks) {
157
- chunks = { ...acc.chunks ?? {}, ...r.chunks };
158
- }
159
- let integrity = acc.integrity;
160
- if (r.integrity) {
161
- integrity = { ...acc.integrity ?? {}, ...r.integrity };
162
- }
163
- return {
164
- externals: [...acc.externals, ...r.externals],
165
- chunks,
166
- integrity
167
- };
168
- },
169
- { externals: [] }
170
- );
171
- }
172
- function splitShared(shared) {
173
- const sharedServer = {};
174
- const sharedBrowser = {};
175
- const separateBrowser = {};
176
- const separateServer = {};
177
- for (const key in shared) {
178
- const obj = shared[key];
179
- if (obj?.platform === "node") {
180
- if (obj.build === "default") {
181
- sharedServer[key] = obj;
182
- } else {
183
- separateServer[key] = obj;
184
- }
185
- } else if (obj?.platform === "browser") {
186
- if (obj.build === "default") {
187
- sharedBrowser[key] = obj;
188
- } else {
189
- separateBrowser[key] = obj;
190
- }
191
- }
80
+ const separatePlans = plans.filter((p) => p.kind === "separate");
81
+ if (separatePlans.length > 0) {
82
+ const start = process.hrtime();
83
+ const results = await Promise.all(
84
+ separatePlans.map(
85
+ (plan) => bundleShared(plan.entries, config, fedOptions, plan.externals, {
86
+ platform: plan.platform,
87
+ bundleName: plan.bundleName,
88
+ chunks: plan.chunks
89
+ })
90
+ )
91
+ );
92
+ logger.measure(start, "Step 2.2) Bundling all separate external packages");
93
+ for (const info of results) addExternalsToCache(fedOptions.federationCache, info);
94
+ if (signal?.aborted) throw new AbortedError("[buildForFederation] After separate bundle");
192
95
  }
193
- return {
194
- sharedBrowser,
195
- sharedServer,
196
- separateBrowser,
197
- separateServer
198
- };
199
96
  }
200
97
  export {
201
- buildForFederation
98
+ buildForFederation,
99
+ executeSharedBundlePlans
202
100
  };
@@ -5,6 +5,7 @@ import type { HashPort, IoPort } from '../../domain/utils/io-port.contract.js';
5
5
  import { type NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
6
6
  import type { NormalizedExternalConfig } from '../../domain/config/external-config.contract.js';
7
7
  import type { NFBuildAdapter } from '../../domain/core/build-adapter.contract.js';
8
+ import { type ModuleEvaluator } from './synthesize-cjs-exports.js';
8
9
  export declare function bundleShared(sharedBundles: Record<string, NormalizedExternalConfig>, config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], buildOptions: {
9
10
  platform: 'browser' | 'node';
10
11
  bundleName: string;
@@ -18,6 +19,12 @@ interface BundleSharedDeps {
18
19
  io: IoPort;
19
20
  repo: PackageJsonRepository;
20
21
  adapter: NFBuildAdapter;
22
+ /**
23
+ * Loads a module by absolute path at build time (Node `require`) and returns its
24
+ * runtime exports, from which a CJS external's named exports are then enumerated.
25
+ * Omitted → no named-export synthesis; externals stay default-only.
26
+ */
27
+ evaluateModule?: ModuleEvaluator;
21
28
  }
22
29
  export declare function bundleSharedCore(deps: BundleSharedDeps, sharedBundles: Record<string, NormalizedExternalConfig>, config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], buildOptions: {
23
30
  platform: 'browser' | 'node';
@@ -9,12 +9,21 @@ import { DEFAULT_EXTERNAL_LIST } from "./default-external-list.js";
9
9
  import { isSourceFile, transformChunkImports } from "./rewrite-chunk-imports.js";
10
10
  import { toChunkImport } from "../../domain/core/chunk.js";
11
11
  import { cacheEntryCore, getChecksumCore, getFilename } from "../cache/cache-persistence.js";
12
+ import { linkedContentSignals } from "./resolve-shared-dirs.js";
12
13
  import { computeIntegrityMapCore } from "./compute-integrity.js";
13
14
  import { fileURLToPath } from "url";
14
15
  import { getBuildAdapter } from "./build-adapter.js";
16
+ import { synthesizeCjsNamedExportsEntry } from "./synthesize-cjs-exports.js";
17
+ import { createRequire } from "module";
15
18
  async function bundleShared(sharedBundles, config, fedOptions, externals, buildOptions) {
19
+ const requireFromWorkspace = createRequire(path.join(fedOptions.workspaceRoot, "index.js"));
16
20
  return bundleSharedCore(
17
- { io: nodeIo, repo: sharedPackageJsonRepository, adapter: getBuildAdapter() },
21
+ {
22
+ io: nodeIo,
23
+ repo: sharedPackageJsonRepository,
24
+ adapter: getBuildAdapter(),
25
+ evaluateModule: (absPath) => requireFromWorkspace(absPath)
26
+ },
18
27
  sharedBundles,
19
28
  config,
20
29
  fedOptions,
@@ -25,13 +34,21 @@ async function bundleShared(sharedBundles, config, fedOptions, externals, buildO
25
34
  async function bundleSharedCore(deps, sharedBundles, config, fedOptions, externals, buildOptions) {
26
35
  const builderPackageJson = readBuilderPackageJson(deps.io, fileURLToPath(import.meta.url));
27
36
  const builderVersion = parseBuilderVersion(builderPackageJson);
37
+ const folder = fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
38
+ const contentSignals = linkedContentSignals(
39
+ Object.keys(sharedBundles),
40
+ folder,
41
+ deps.io,
42
+ deps.repo
43
+ );
28
44
  const checksum = getChecksumCore(
29
45
  deps.io,
30
46
  sharedBundles,
31
47
  fedOptions.dev ? "1" : "0",
32
- builderVersion
48
+ builderVersion,
49
+ config.features.synthesizeCjsExports,
50
+ contentSignals
33
51
  );
34
- const folder = fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
35
52
  const bundleCache = cacheEntryCore(
36
53
  deps.io,
37
54
  fedOptions.federationCache.cachePath,
@@ -67,8 +84,22 @@ async function bundleSharedCore(deps, sharedBundles, config, fedOptions, externa
67
84
  const configState = `BUNDLER_CHUNKS;${builderVersion};${JSON.stringify(config)}`;
68
85
  const entryPoints = packageInfos.map((pi) => {
69
86
  const encName = pi.packageName.replace(/[^A-Za-z0-9]/g, "_");
70
- const outName = createOutName(deps.io, pi, configState, fedOptions, encName);
71
- return { fileName: pi.entryPoint, outName };
87
+ const outName = createOutName(
88
+ deps.io,
89
+ pi,
90
+ configState,
91
+ fedOptions,
92
+ encName,
93
+ contentSignals[pi.packageName] ?? ""
94
+ );
95
+ const synthetic = deps.evaluateModule && config.features.synthesizeCjsExports ? synthesizeCjsNamedExportsEntry(
96
+ deps.io,
97
+ deps.evaluateModule,
98
+ pi,
99
+ fedOptions.federationCache.cachePath,
100
+ outName
101
+ ) : null;
102
+ return { fileName: synthetic ?? pi.entryPoint, outName };
72
103
  });
73
104
  const fullOutputPath = path.join(fedOptions.workspaceRoot, fedOptions.outputPath);
74
105
  const useDefaultExternalList = buildOptions.platform === "browser" && !config.features.ignoreUnusedDeps;
@@ -177,8 +208,8 @@ function applyRenames(bundleResult, entryPoints, renamed) {
177
208
  if (next) ep.outName = next;
178
209
  }
179
210
  }
180
- function createOutName(io, pi, configState, fedOptions, encName) {
181
- const hashBase = pi.version + "_" + pi.entryPoint + "_" + configState;
211
+ function createOutName(io, pi, configState, fedOptions, encName, contentSignal = "") {
212
+ const hashBase = pi.version + "_" + pi.entryPoint + "_" + configState + (contentSignal ? "_" + contentSignal : "");
182
213
  const hash = calcHashCore(io, hashBase);
183
214
  const outName = fedOptions.dev ? `${encName}.${hash}-dev.js` : `${encName}.${hash}.js`;
184
215
  return outName;
@@ -1,4 +1,12 @@
1
1
  import type { FederationInfo } from '../../domain/core/federation-info.contract.js';
2
2
  import type { NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
3
3
  import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
4
+ import type { IoPort } from '../../domain/utils/io-port.contract.js';
5
+ import type { PackageJsonRepository } from '../../domain/utils/package-json.contract.js';
4
6
  export declare function rebuildForFederation(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], modifiedFiles: string[], signal?: AbortSignal): Promise<FederationInfo>;
7
+ /**
8
+ * When a modified file belongs to a shared package (e.g. npm-linked dev dep),
9
+ * re-bundle the affected bundle(s); unchanged bundles hit their cache, so
10
+ * `federationCache.externals` regenerates cheaply. No package touched → no-op.
11
+ */
12
+ export declare function rebuildAffectedExternals(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], modifiedFiles: string[], signal?: AbortSignal, io?: IoPort, repo?: PackageJsonRepository): Promise<void>;
@@ -7,8 +7,15 @@ import { writeFederationInfo } from "../output/write-federation-info.js";
7
7
  import { writeImportMap } from "../output/write-import-map.js";
8
8
  import { logger } from "../../utils/logger.js";
9
9
  import { AbortedError } from "../../utils/errors.js";
10
+ import { planSharedBundles } from "./shared-bundle-plan.js";
11
+ import { executeSharedBundlePlans } from "./build-for-federation.js";
12
+ import { affectedSharedKeys, resolveSharedPackageDirs } from "./resolve-shared-dirs.js";
13
+ import { cacheEntryCore, getFilename } from "../cache/cache-persistence.js";
14
+ import { nodeIo } from "../../utils/io/node-io-adapter.js";
15
+ import { sharedPackageJsonRepository } from "../../utils/package/package-info.js";
10
16
  async function rebuildForFederation(config, fedOptions, externals, modifiedFiles, signal) {
11
17
  const federationCache = fedOptions.federationCache;
18
+ await rebuildAffectedExternals(config, fedOptions, externals, modifiedFiles, signal);
12
19
  logger.info(`Re-bundling all internal libraries and exposed modules..'`);
13
20
  const start = process.hrtime();
14
21
  const artifactInfo = await bundleExposedAndMappings(
@@ -47,6 +54,25 @@ async function rebuildForFederation(config, fedOptions, externals, modifiedFiles
47
54
  writeImportMap(federationCache, fedOptions, federationInfo.integrity);
48
55
  return federationInfo;
49
56
  }
57
+ async function rebuildAffectedExternals(config, fedOptions, externals, modifiedFiles, signal, io = nodeIo, repo = sharedPackageJsonRepository) {
58
+ if (modifiedFiles.length === 0) return;
59
+ const plans = planSharedBundles(config, externals);
60
+ if (plans.length === 0) return;
61
+ const dirs = resolveSharedPackageDirs(config, fedOptions, io, repo);
62
+ const affected = affectedSharedKeys(modifiedFiles, dirs, io);
63
+ const affectedPlans = plans.filter((p) => p.keys.some((k) => affected.has(k)));
64
+ if (affectedPlans.length === 0) return;
65
+ const federationCache = fedOptions.federationCache;
66
+ for (const plan of affectedPlans) {
67
+ logger.info(`Detected change in linked shared package(s); re-bundling '${plan.bundleName}'.`);
68
+ cacheEntryCore(io, federationCache.cachePath, getFilename(plan.bundleName, fedOptions.dev)).clear();
69
+ }
70
+ federationCache.externals = [];
71
+ federationCache.chunks = void 0;
72
+ federationCache.integrity = void 0;
73
+ await executeSharedBundlePlans(plans, config, fedOptions, signal);
74
+ }
50
75
  export {
76
+ rebuildAffectedExternals,
51
77
  rebuildForFederation
52
78
  };
@@ -0,0 +1,16 @@
1
+ import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
2
+ import type { NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
3
+ import type { FileReaderPort } from '../../domain/utils/io-port.contract.js';
4
+ import type { PackageJsonRepository } from '../../domain/utils/package-json.contract.js';
5
+ /** Each `config.shared` key → its realpath'd package dir, so watcher, checksum, and
6
+ * file-mapping agree on one identity for symlinked (npm-linked) deps. */
7
+ export declare function resolveSharedPackageDirs(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, io?: FileReaderPort, repo?: PackageJsonRepository): Map<string, string>;
8
+ /** Realpath'd dirs of symlinked shared packages — the bounded watch set.
9
+ * Deduped, since secondaries share a package dir. */
10
+ export declare function linkedSharedDirs(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, io?: FileReaderPort, repo?: PackageJsonRepository): string[];
11
+ /** Per-key content signal (max mtime of the resolved dir) for symlinked deps only.
12
+ * Registry deps get no signal, keeping their checksum version-only. (Every key is
13
+ * still resolved: detecting the symlink requires the realpath + lstat.) */
14
+ export declare function linkedContentSignals(keys: string[], folder: string, io?: FileReaderPort, repo?: PackageJsonRepository): Record<string, string>;
15
+ /** `config.shared` keys whose package directory contains at least one modified file. */
16
+ export declare function affectedSharedKeys(modifiedFiles: readonly string[], dirs: Map<string, string>, io?: FileReaderPort): Set<string>;
@@ -0,0 +1,66 @@
1
+ import * as path from "path";
2
+ import { nodeIo } from "../../utils/io/node-io-adapter.js";
3
+ import { sharedPackageJsonRepository } from "../../utils/package/package-info.js";
4
+ import { toPosix } from "../../utils/path-patterns.js";
5
+ const folderOf = (fedOptions) => fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
6
+ function resolveEntries(keys, folder, io, repo) {
7
+ const out = [];
8
+ for (const key of keys) {
9
+ const pkgJsonPath = repo.findDepPackageJson(key, folder);
10
+ if (!pkgJsonPath) continue;
11
+ const pkgDir = path.dirname(pkgJsonPath);
12
+ out.push({
13
+ key,
14
+ realDir: toPosix(io.realpath(pkgDir)),
15
+ isSymlink: !!io.stat(pkgDir)?.isSymbolicLink
16
+ });
17
+ }
18
+ return out;
19
+ }
20
+ function resolveSharedPackageDirs(config, fedOptions, io = nodeIo, repo = sharedPackageJsonRepository) {
21
+ const entries = resolveEntries(Object.keys(config.shared), folderOf(fedOptions), io, repo);
22
+ return new Map(entries.map((e) => [e.key, e.realDir]));
23
+ }
24
+ function linkedSharedDirs(config, fedOptions, io = nodeIo, repo = sharedPackageJsonRepository) {
25
+ const entries = resolveEntries(Object.keys(config.shared), folderOf(fedOptions), io, repo);
26
+ return [...new Set(entries.filter((e) => e.isSymlink).map((e) => e.realDir))];
27
+ }
28
+ function maxMtime(io, dir) {
29
+ let max = 0;
30
+ const walk = (d) => {
31
+ for (const name of io.readDir(d)) {
32
+ const full = path.join(d, name);
33
+ if (io.isDirectory(full)) walk(full);
34
+ else {
35
+ let s = io.stat(full);
36
+ if (s?.isSymbolicLink) s = io.stat(io.realpath(full));
37
+ if (s && s.mtimeMs > max) max = s.mtimeMs;
38
+ }
39
+ }
40
+ };
41
+ walk(dir);
42
+ return max;
43
+ }
44
+ function linkedContentSignals(keys, folder, io = nodeIo, repo = sharedPackageJsonRepository) {
45
+ const signals = {};
46
+ for (const entry of resolveEntries(keys, folder, io, repo)) {
47
+ if (entry.isSymlink) signals[entry.key] = String(maxMtime(io, entry.realDir));
48
+ }
49
+ return signals;
50
+ }
51
+ function affectedSharedKeys(modifiedFiles, dirs, io = nodeIo) {
52
+ const affected = /* @__PURE__ */ new Set();
53
+ if (modifiedFiles.length === 0 || dirs.size === 0) return affected;
54
+ const realFiles = modifiedFiles.map((f) => toPosix(io.realpath(f)));
55
+ for (const [key, dir] of dirs) {
56
+ const prefix = dir.endsWith("/") ? dir : dir + "/";
57
+ if (realFiles.some((f) => f === dir || f.startsWith(prefix))) affected.add(key);
58
+ }
59
+ return affected;
60
+ }
61
+ export {
62
+ affectedSharedKeys,
63
+ linkedContentSignals,
64
+ linkedSharedDirs,
65
+ resolveSharedPackageDirs
66
+ };
@@ -0,0 +1,22 @@
1
+ import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
2
+ import type { NormalizedExternalConfig } from '../../domain/config/external-config.contract.js';
3
+ export interface SharedBundlePlan {
4
+ bundleName: string;
5
+ platform: 'browser' | 'node';
6
+ chunks: boolean;
7
+ entries: Record<string, NormalizedExternalConfig>;
8
+ externals: string[];
9
+ keys: string[];
10
+ kind: 'shared' | 'separate';
11
+ }
12
+ type SplitSharedResult = {
13
+ sharedServer: Record<string, NormalizedExternalConfig>;
14
+ sharedBrowser: Record<string, NormalizedExternalConfig>;
15
+ separateBrowser: Record<string, NormalizedExternalConfig>;
16
+ separateServer: Record<string, NormalizedExternalConfig>;
17
+ };
18
+ export declare function splitShared(shared: Record<string, NormalizedExternalConfig>): SplitSharedResult;
19
+ /** Single source of truth for package → bundle(name) mapping, shared by the initial
20
+ * build and the watch rebuild so both target the same cache entries. */
21
+ export declare function planSharedBundles(config: NormalizedFederationConfig, externals: string[]): SharedBundlePlan[];
22
+ export {};