@softarc/native-federation 4.3.1 → 4.4.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.
Files changed (57) hide show
  1. package/README.md +71 -1
  2. package/dist/config.d.ts +1 -0
  3. package/dist/config.js +2 -0
  4. package/dist/internal.d.ts +5 -1
  5. package/dist/internal.js +24 -0
  6. package/dist/lib/config/expand-mappings.d.ts +24 -0
  7. package/dist/lib/config/expand-mappings.js +88 -0
  8. package/dist/lib/config/get-used-dependencies.d.ts +0 -2
  9. package/dist/lib/config/get-used-dependencies.js +4 -40
  10. package/dist/lib/config/mapped-paths.d.ts +7 -2
  11. package/dist/lib/config/mapped-paths.js +21 -4
  12. package/dist/lib/config/mapping-utils.d.ts +32 -0
  13. package/dist/lib/config/mapping-utils.js +68 -0
  14. package/dist/lib/config/match-mapping.d.ts +9 -0
  15. package/dist/lib/config/match-mapping.js +46 -0
  16. package/dist/lib/config/remove-unused-deps.d.ts +2 -1
  17. package/dist/lib/config/remove-unused-deps.js +35 -2
  18. package/dist/lib/config/validate-mappings.d.ts +15 -0
  19. package/dist/lib/config/validate-mappings.js +28 -0
  20. package/dist/lib/config/with-native-federation.js +20 -7
  21. package/dist/lib/core/build/build-for-federation.d.ts +7 -0
  22. package/dist/lib/core/build/build-for-federation.js +32 -134
  23. package/dist/lib/core/build/bundle-exposed-and-mappings.js +21 -8
  24. package/dist/lib/core/build/bundle-shared.d.ts +7 -0
  25. package/dist/lib/core/build/bundle-shared.js +45 -8
  26. package/dist/lib/core/build/rebuild-for-federation.d.ts +8 -0
  27. package/dist/lib/core/build/rebuild-for-federation.js +26 -0
  28. package/dist/lib/core/build/resolve-shared-dirs.d.ts +32 -0
  29. package/dist/lib/core/build/resolve-shared-dirs.js +70 -0
  30. package/dist/lib/core/build/shared-bundle-plan.d.ts +22 -0
  31. package/dist/lib/core/build/shared-bundle-plan.js +70 -0
  32. package/dist/lib/core/build/synthesize-cjs-exports.d.ts +12 -0
  33. package/dist/lib/core/build/synthesize-cjs-exports.js +58 -0
  34. package/dist/lib/core/cache/cache-persistence.d.ts +4 -2
  35. package/dist/lib/core/cache/cache-persistence.js +28 -6
  36. package/dist/lib/core/federation-builder.d.ts +2 -1
  37. package/dist/lib/core/federation-builder.js +3 -0
  38. package/dist/lib/core/normalize-options.d.ts +2 -2
  39. package/dist/lib/core/normalize-options.js +15 -9
  40. package/dist/lib/core/output/densify-externals.js +1 -0
  41. package/dist/lib/domain/config/federation-config.contract.d.ts +29 -2
  42. package/dist/lib/domain/utils/file-watcher.contract.d.ts +19 -1
  43. package/dist/lib/domain/utils/io-port.contract.d.ts +20 -6
  44. package/dist/lib/utils/file-watcher.d.ts +15 -4
  45. package/dist/lib/utils/file-watcher.js +126 -16
  46. package/dist/lib/utils/io/node-io-adapter.js +85 -24
  47. package/dist/lib/utils/package/cjs-named-exports.d.ts +21 -0
  48. package/dist/lib/utils/package/cjs-named-exports.js +36 -0
  49. package/dist/lib/utils/package/entry-point-resolver.js +2 -5
  50. package/dist/lib/utils/package/esm-detection.d.ts +13 -0
  51. package/dist/lib/utils/package/esm-detection.js +29 -0
  52. package/dist/lib/utils/package/package-info.d.ts +13 -0
  53. package/dist/lib/utils/package/package-info.js +30 -4
  54. package/dist/lib/utils/package/resolve-wildcard-keys.js +8 -2
  55. package/dist/lib/utils/path-patterns.d.ts +14 -0
  56. package/dist/lib/utils/path-patterns.js +12 -0
  57. package/package.json +6 -6
@@ -0,0 +1,28 @@
1
+ import { logger } from "../utils/logger.js";
2
+ const IMPORTABLE_EXTENSIONS = /* @__PURE__ */ new Set(["mjs", "js", "mts", "ts", "jsx", "tsx", "json"]);
3
+ const MAX_LISTED = 5;
4
+ function isNonBarrelImport(importName) {
5
+ if (!importName.includes(".")) return false;
6
+ const queryIndex = importName.search(/[?#]/);
7
+ const sanitized = queryIndex >= 0 ? importName.slice(0, queryIndex) : importName;
8
+ const lastSegment = sanitized.slice(sanitized.lastIndexOf("/") + 1);
9
+ const dotIndex = lastSegment.lastIndexOf(".");
10
+ if (dotIndex < 0) return false;
11
+ return !IMPORTABLE_EXTENSIONS.has(lastSegment.slice(dotIndex + 1));
12
+ }
13
+ function assertBarrelMappings(paths) {
14
+ const invalid = Object.values(paths).filter(isNonBarrelImport);
15
+ if (invalid.length === 0) return;
16
+ for (const importName of invalid) {
17
+ logger.warn(`Only barrel imports can be shared as a sharedMapping: '${importName}'.`);
18
+ }
19
+ const shown = invalid.slice(0, MAX_LISTED).map((i) => `'${i}'`).join(", ");
20
+ const rest = invalid.length - MAX_LISTED;
21
+ throw new Error(
22
+ `Invalid 'shared mappings' config. Only barrel imports can be shared as a sharedMapping: ${shown}${rest > 0 ? ` and ${rest} more` : ""}.`
23
+ );
24
+ }
25
+ export {
26
+ assertBarrelMappings,
27
+ isNonBarrelImport
28
+ };
@@ -2,25 +2,30 @@ import { getRawMappedPaths } from "./mapped-paths.js";
2
2
  import { fromPackageJson } from "./share-utils.js";
3
3
  import { findRootTsConfigJson } from "./project-paths.js";
4
4
  import { isInSkipList, prepareSkipList } from "./default-skip-list.js";
5
+ import { normalizeMappingConfig, withoutSkippedMappings } from "./mapping-utils.js";
5
6
  import { logger } from "../utils/logger.js";
6
7
  function withNativeFederation(config) {
7
8
  const skip = prepareSkipList(config.skip ?? []);
8
9
  const chunks = config.chunks ?? true;
10
+ const mappingVersion = config.features?.mappingVersion ?? true;
11
+ const { paths, configs } = getRawMappedPaths(findRootTsConfigJson(), config.sharedMappings);
9
12
  const normalized = {
10
13
  $type: "classic",
11
14
  name: config.name ?? "",
12
15
  exposes: normalizeExposes(config.exposes),
13
16
  shared: normalizeShared(config, skip, chunks),
14
- sharedMappings: removeSkippedMappings(config, skip),
17
+ sharedMappings: withoutSkippedMappings(paths, skip),
18
+ sharedMappingsConfig: normalizeMappingConfigs(configs, mappingVersion),
15
19
  chunks,
16
20
  skip,
17
21
  externals: config.externals ?? [],
18
22
  features: {
19
- mappingVersion: config.features?.mappingVersion ?? true,
23
+ mappingVersion,
20
24
  ignoreUnusedDeps: config.features?.ignoreUnusedDeps ?? true,
21
25
  denseChunking: config.features?.denseChunking ?? false,
22
26
  denseExternals: config.features?.denseExternals ?? false,
23
- integrityHashes: config.features?.integrityHashes ?? false
27
+ integrityHashes: config.features?.integrityHashes ?? false,
28
+ synthesizeCjsExports: config.features?.synthesizeCjsExports ?? true
24
29
  },
25
30
  ...config.shareScope && { shareScope: config.shareScope }
26
31
  };
@@ -73,10 +78,18 @@ function normalizeShared(config, skip, chunks) {
73
78
  result = Object.keys(result).filter((key) => !isInSkipList(key, skip)).reduce((acc, cur) => ({ ...acc, [cur]: result[cur] }), {});
74
79
  return result;
75
80
  }
76
- function removeSkippedMappings(config, skipList) {
77
- const rootTsConfigPath = findRootTsConfigJson();
78
- const paths = getRawMappedPaths(rootTsConfigPath, config.sharedMappings);
79
- return Object.entries(paths).filter(([, _import]) => !isInSkipList(_import, skipList)).reduce((acc, [_path, _import]) => ({ ...acc, [_path]: _import }), {});
81
+ const IGNORED_MAPPING_PROPS = ["build", "platform", "chunks", "packageInfo"];
82
+ function normalizeMappingConfigs(configs, mappingVersion) {
83
+ return Object.entries(configs).reduce((acc, [pattern, cfg]) => {
84
+ const ignored = IGNORED_MAPPING_PROPS.filter((prop) => cfg[prop] !== void 0);
85
+ if (ignored.length > 0) {
86
+ logger.warn(
87
+ `Mapping '${pattern}' sets ${ignored.join(", ")}, which mapped paths do not honour (they all share one bundle). Ignored.`
88
+ );
89
+ }
90
+ acc[pattern] = normalizeMappingConfig(cfg, mappingVersion);
91
+ return acc;
92
+ }, {});
80
93
  }
81
94
  export {
82
95
  withNativeFederation
@@ -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
  };
@@ -7,6 +7,7 @@ import { nodeIo } from "../../utils/io/node-io-adapter.js";
7
7
  import { AbortedError } from "../../utils/errors.js";
8
8
  import { rewriteChunkImports } from "./rewrite-chunk-imports.js";
9
9
  import { getBuildAdapter } from "./build-adapter.js";
10
+ import { resolveMappingConfig } from "../../config/mapping-utils.js";
10
11
  async function bundleExposedAndMappings(config, fedOptions, externals, modifiedFiles, signal) {
11
12
  return bundleExposedAndMappingsCore(
12
13
  { adapter: getBuildAdapter() },
@@ -78,7 +79,13 @@ async function bundleExposedAndMappingsCore(deps, config, fedOptions, externals,
78
79
  for (const item of shared) {
79
80
  const distEntryFile = popFromResultMap(resultMap, item.outName);
80
81
  sharedResult.push(
81
- toSharedMappingInfo(item.fileName, item.key, path.basename(distEntryFile), config, fedOptions)
82
+ toSharedMappingInfo(
83
+ item.fileName,
84
+ item.key,
85
+ path.basename(distEntryFile),
86
+ config,
87
+ fedOptions
88
+ )
82
89
  );
83
90
  entryFiles.push(distEntryFile);
84
91
  }
@@ -112,9 +119,7 @@ function describeExposed(config, options) {
112
119
  const result = [];
113
120
  for (const key in config.exposes) {
114
121
  const expose = config.exposes[key];
115
- const localPath = normalize(
116
- path.normalize(path.join(options.workspaceRoot, expose.file))
117
- );
122
+ const localPath = normalize(path.normalize(path.join(options.workspaceRoot, expose.file)));
118
123
  result.push({
119
124
  key,
120
125
  outFileName: "",
@@ -135,13 +140,21 @@ function describeSharedMappings(config, fedOptions) {
135
140
  }
136
141
  function toSharedMappingInfo(mappedPath, mappedImport, outFileName, config, fedOptions) {
137
142
  const mappingVersion = config.features.mappingVersion ? getMappingVersion(mappedPath, fedOptions.workspaceRoot) : "";
143
+ const mappingConfig = resolveMappingConfig(
144
+ mappedImport,
145
+ config.sharedMappingsConfig,
146
+ config.features.mappingVersion
147
+ );
148
+ const version = mappingConfig.version ?? mappingVersion;
138
149
  return {
139
150
  packageName: mappedImport,
140
151
  outFileName,
141
- requiredVersion: mappingVersion.length > 0 ? "~" + mappingVersion : "",
142
- singleton: true,
143
- strictVersion: config.features.mappingVersion,
144
- version: mappingVersion,
152
+ requiredVersion: mappingConfig.requiredVersion ?? (version.length > 0 ? "~" + version : ""),
153
+ singleton: mappingConfig.singleton,
154
+ strictVersion: mappingConfig.strictVersion,
155
+ version,
156
+ ...mappingConfig.shareScope && { shareScope: mappingConfig.shareScope },
157
+ ...mappingConfig.pool && { pool: mappingConfig.pool },
145
158
  dev: !fedOptions.dev ? void 0 : {
146
159
  entryPoint: normalize(path.normalize(mappedPath))
147
160
  }
@@ -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';
@@ -1,7 +1,8 @@
1
1
  import * as path from "path";
2
2
  import {
3
3
  sharedPackageJsonRepository,
4
- getPackageInfo
4
+ getPackageInfo,
5
+ installedVersions
5
6
  } from "../../utils/package/package-info.js";
6
7
  import { logger } from "../../utils/logger.js";
7
8
  import { nodeIo } from "../../utils/io/node-io-adapter.js";
@@ -9,12 +10,21 @@ import { DEFAULT_EXTERNAL_LIST } from "./default-external-list.js";
9
10
  import { isSourceFile, transformChunkImports } from "./rewrite-chunk-imports.js";
10
11
  import { toChunkImport } from "../../domain/core/chunk.js";
11
12
  import { cacheEntryCore, getChecksumCore, getFilename } from "../cache/cache-persistence.js";
13
+ import { linkedContentSignals } from "./resolve-shared-dirs.js";
12
14
  import { computeIntegrityMapCore } from "./compute-integrity.js";
13
15
  import { fileURLToPath } from "url";
14
16
  import { getBuildAdapter } from "./build-adapter.js";
17
+ import { synthesizeCjsNamedExportsEntry } from "./synthesize-cjs-exports.js";
18
+ import { createRequire } from "module";
15
19
  async function bundleShared(sharedBundles, config, fedOptions, externals, buildOptions) {
20
+ const requireFromWorkspace = createRequire(path.join(fedOptions.workspaceRoot, "index.js"));
16
21
  return bundleSharedCore(
17
- { io: nodeIo, repo: sharedPackageJsonRepository, adapter: getBuildAdapter() },
22
+ {
23
+ io: nodeIo,
24
+ repo: sharedPackageJsonRepository,
25
+ adapter: getBuildAdapter(),
26
+ evaluateModule: (absPath) => requireFromWorkspace(absPath)
27
+ },
18
28
  sharedBundles,
19
29
  config,
20
30
  fedOptions,
@@ -25,13 +35,26 @@ async function bundleShared(sharedBundles, config, fedOptions, externals, buildO
25
35
  async function bundleSharedCore(deps, sharedBundles, config, fedOptions, externals, buildOptions) {
26
36
  const builderPackageJson = readBuilderPackageJson(deps.io, fileURLToPath(import.meta.url));
27
37
  const builderVersion = parseBuilderVersion(builderPackageJson);
38
+ const folder = fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
39
+ const contentSignals = linkedContentSignals(
40
+ Object.keys(sharedBundles),
41
+ folder,
42
+ deps.io,
43
+ deps.repo
44
+ );
45
+ const resolvedVersions = installedVersions(Object.keys(sharedBundles), folder, deps.repo);
46
+ for (const [key, cfg] of Object.entries(sharedBundles)) {
47
+ if (cfg.packageInfo) resolvedVersions[key] = cfg.packageInfo.version ?? "";
48
+ }
28
49
  const checksum = getChecksumCore(
29
50
  deps.io,
30
51
  sharedBundles,
31
52
  fedOptions.dev ? "1" : "0",
32
- builderVersion
53
+ builderVersion,
54
+ config.features,
55
+ contentSignals,
56
+ resolvedVersions
33
57
  );
34
- const folder = fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
35
58
  const bundleCache = cacheEntryCore(
36
59
  deps.io,
37
60
  fedOptions.federationCache.cachePath,
@@ -67,8 +90,22 @@ async function bundleSharedCore(deps, sharedBundles, config, fedOptions, externa
67
90
  const configState = `BUNDLER_CHUNKS;${builderVersion};${JSON.stringify(config)}`;
68
91
  const entryPoints = packageInfos.map((pi) => {
69
92
  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 };
93
+ const outName = createOutName(
94
+ deps.io,
95
+ pi,
96
+ configState,
97
+ fedOptions,
98
+ encName,
99
+ contentSignals[pi.packageName] ?? ""
100
+ );
101
+ const synthetic = deps.evaluateModule && config.features.synthesizeCjsExports ? synthesizeCjsNamedExportsEntry(
102
+ deps.io,
103
+ deps.evaluateModule,
104
+ pi,
105
+ fedOptions.federationCache.cachePath,
106
+ outName
107
+ ) : null;
108
+ return { fileName: synthetic ?? pi.entryPoint, outName };
72
109
  });
73
110
  const fullOutputPath = path.join(fedOptions.workspaceRoot, fedOptions.outputPath);
74
111
  const useDefaultExternalList = buildOptions.platform === "browser" && !config.features.ignoreUnusedDeps;
@@ -177,8 +214,8 @@ function applyRenames(bundleResult, entryPoints, renamed) {
177
214
  if (next) ep.outName = next;
178
215
  }
179
216
  }
180
- function createOutName(io, pi, configState, fedOptions, encName) {
181
- const hashBase = pi.version + "_" + pi.entryPoint + "_" + configState;
217
+ function createOutName(io, pi, configState, fedOptions, encName, contentSignal = "") {
218
+ const hashBase = pi.version + "_" + pi.entryPoint + "_" + configState + (contentSignal ? "_" + contentSignal : "");
182
219
  const hash = calcHashCore(io, hashBase);
183
220
  const outName = fedOptions.dev ? `${encName}.${hash}-dev.js` : `${encName}.${hash}.js`;
184
221
  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,32 @@
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
+ /**
12
+ * Source directories of the workspace libs in `config.sharedMappings` — the watch set
13
+ * for mappings, derived from config alone rather than from a bundler cache.
14
+ *
15
+ * Coarser than the inputs a build actually compiled, and deliberately so: it covers
16
+ * files added to a lib after the last build, which a compiled-inputs watch set cannot
17
+ * know about yet. It does not follow imports out of the lib, so an adapter that can
18
+ * enumerate its build inputs should watch both. See angular-adapter#94.
19
+ *
20
+ * How coarse is the caller's to bound: an entry point that is not a lib barrel widens the
21
+ * watch to whatever directory it sits in (`'@app/env': ['src/environments/environment.ts']`
22
+ * to that folder, `'@shared': ['src/index.ts']` to all of `src`), and with `sharedMappings`
23
+ * unset every tsconfig path becomes a mapping. Watch these natively rather than polled —
24
+ * they are source trees, not the dist output `linkedSharedDirs` exists for.
25
+ */
26
+ export declare function sharedMappingDirs(config: NormalizedFederationConfig): string[];
27
+ /** Per-key content signal (max mtime of the resolved dir) for symlinked deps only.
28
+ * Registry deps get no signal, keeping their checksum version-only. (Every key is
29
+ * still resolved: detecting the symlink requires the realpath + lstat.) */
30
+ export declare function linkedContentSignals(keys: string[], folder: string, io?: FileReaderPort, repo?: PackageJsonRepository): Record<string, string>;
31
+ /** `config.shared` keys whose package directory contains at least one modified file. */
32
+ export declare function affectedSharedKeys(modifiedFiles: readonly string[], dirs: Map<string, string>, io?: FileReaderPort): Set<string>;
@@ -0,0 +1,70 @@
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 { isUnderDir, 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 sharedMappingDirs(config) {
29
+ const dirs = Object.keys(config.sharedMappings).map((entryPoint) => toPosix(path.dirname(entryPoint))).filter((dir) => !dir.includes("node_modules"));
30
+ return [...new Set(dirs)];
31
+ }
32
+ function maxMtime(io, dir) {
33
+ let max = 0;
34
+ const walk = (d) => {
35
+ for (const name of io.readDir(d)) {
36
+ const full = path.join(d, name);
37
+ if (io.isDirectory(full)) walk(full);
38
+ else {
39
+ let s = io.stat(full);
40
+ if (s?.isSymbolicLink) s = io.stat(io.realpath(full));
41
+ if (s && s.mtimeMs > max) max = s.mtimeMs;
42
+ }
43
+ }
44
+ };
45
+ walk(dir);
46
+ return max;
47
+ }
48
+ function linkedContentSignals(keys, folder, io = nodeIo, repo = sharedPackageJsonRepository) {
49
+ const signals = {};
50
+ for (const entry of resolveEntries(keys, folder, io, repo)) {
51
+ if (entry.isSymlink) signals[entry.key] = String(maxMtime(io, entry.realDir));
52
+ }
53
+ return signals;
54
+ }
55
+ function affectedSharedKeys(modifiedFiles, dirs, io = nodeIo) {
56
+ const affected = /* @__PURE__ */ new Set();
57
+ if (modifiedFiles.length === 0 || dirs.size === 0) return affected;
58
+ const realFiles = modifiedFiles.map((f) => toPosix(io.realpath(f)));
59
+ for (const [key, dir] of dirs) {
60
+ if (realFiles.some((f) => isUnderDir(f, dir))) affected.add(key);
61
+ }
62
+ return affected;
63
+ }
64
+ export {
65
+ affectedSharedKeys,
66
+ linkedContentSignals,
67
+ linkedSharedDirs,
68
+ resolveSharedPackageDirs,
69
+ sharedMappingDirs
70
+ };
@@ -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 {};