@softarc/native-federation 4.3.2 → 4.4.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.
Files changed (48) 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 +3 -2
  5. package/dist/internal.js +5 -1
  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 +18 -6
  21. package/dist/lib/core/build/assemble-federation-info.d.ts +8 -0
  22. package/dist/lib/core/build/assemble-federation-info.js +34 -0
  23. package/dist/lib/core/build/build-for-federation.js +7 -45
  24. package/dist/lib/core/build/bundle-exposed-and-mappings.d.ts +1 -3
  25. package/dist/lib/core/build/bundle-exposed-and-mappings.js +20 -32
  26. package/dist/lib/core/build/bundle-shared.js +9 -3
  27. package/dist/lib/core/build/rebuild-for-federation.js +6 -33
  28. package/dist/lib/core/build/resolve-shared-dirs.d.ts +16 -0
  29. package/dist/lib/core/build/resolve-shared-dirs.js +8 -4
  30. package/dist/lib/core/cache/cache-persistence.d.ts +4 -2
  31. package/dist/lib/core/cache/cache-persistence.js +27 -8
  32. package/dist/lib/core/normalize-options.d.ts +2 -2
  33. package/dist/lib/core/normalize-options.js +23 -11
  34. package/dist/lib/core/output/write-federation-outputs.d.ts +7 -0
  35. package/dist/lib/core/output/write-federation-outputs.js +9 -0
  36. package/dist/lib/domain/config/federation-config.contract.d.ts +27 -2
  37. package/dist/lib/domain/utils/file-watcher.contract.d.ts +12 -0
  38. package/dist/lib/domain/utils/io-port.contract.d.ts +6 -3
  39. package/dist/lib/utils/file-watcher.d.ts +15 -4
  40. package/dist/lib/utils/file-watcher.js +112 -19
  41. package/dist/lib/utils/io/node-io-adapter.js +10 -5
  42. package/dist/lib/utils/package/entry-point-resolver.js +2 -5
  43. package/dist/lib/utils/package/package-info.d.ts +13 -0
  44. package/dist/lib/utils/package/package-info.js +30 -4
  45. package/dist/lib/utils/package/resolve-wildcard-keys.js +8 -2
  46. package/dist/lib/utils/path-patterns.d.ts +14 -0
  47. package/dist/lib/utils/path-patterns.js +12 -0
  48. package/package.json +5 -5
@@ -2,21 +2,25 @@ 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,
@@ -74,10 +78,18 @@ function normalizeShared(config, skip, chunks) {
74
78
  result = Object.keys(result).filter((key) => !isInSkipList(key, skip)).reduce((acc, cur) => ({ ...acc, [cur]: result[cur] }), {});
75
79
  return result;
76
80
  }
77
- function removeSkippedMappings(config, skipList) {
78
- const rootTsConfigPath = findRootTsConfigJson();
79
- const paths = getRawMappedPaths(rootTsConfigPath, config.sharedMappings);
80
- 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
+ }, {});
81
93
  }
82
94
  export {
83
95
  withNativeFederation
@@ -0,0 +1,8 @@
1
+ import type { ArtifactInfo, FederationInfo } from '../../domain/core/federation-info.contract.js';
2
+ import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
3
+ import type { NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
4
+ /**
5
+ * Derives the `remoteEntry.json` payload from the populated federation cache. Shared by the initial
6
+ * build and the watch rebuild.
7
+ */
8
+ export declare function assembleFederationInfo(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, artifactInfo: ArtifactInfo): FederationInfo;
@@ -0,0 +1,34 @@
1
+ import { densifyExternals } from "../output/densify-externals.js";
2
+ function assembleFederationInfo(config, fedOptions, artifactInfo) {
3
+ const federationCache = fedOptions.federationCache;
4
+ const sharedExternals = applyShareScope(
5
+ [...federationCache.externals, ...artifactInfo.mappings],
6
+ config.shareScope
7
+ );
8
+ const federationInfo = {
9
+ name: config.name,
10
+ shared: config.features.denseExternals ? densifyExternals(sharedExternals) : sharedExternals,
11
+ exposes: artifactInfo.exposes,
12
+ buildNotificationsEndpoint: fedOptions.buildNotifications?.enable && fedOptions.dev ? fedOptions.buildNotifications?.endpoint : void 0
13
+ };
14
+ if (federationCache.chunks) {
15
+ federationInfo.chunks = federationCache.chunks;
16
+ }
17
+ if (artifactInfo.chunks) {
18
+ federationInfo.chunks = { ...federationInfo.chunks ?? {}, ...artifactInfo.chunks };
19
+ }
20
+ if (config.features.integrityHashes) {
21
+ federationInfo.integrity = {
22
+ ...federationCache.integrity ?? {},
23
+ ...artifactInfo.integrity ?? {}
24
+ };
25
+ }
26
+ return federationInfo;
27
+ }
28
+ function applyShareScope(externals, shareScope) {
29
+ if (!shareScope) return externals;
30
+ return externals.map((external) => external.shareScope ? external : { ...external, shareScope });
31
+ }
32
+ export {
33
+ assembleFederationInfo
34
+ };
@@ -1,22 +1,12 @@
1
- import {
2
- bundleExposedAndMappings,
3
- describeExposed,
4
- describeSharedMappings
5
- } from "./bundle-exposed-and-mappings.js";
1
+ import { bundleExposedAndMappings } from "./bundle-exposed-and-mappings.js";
6
2
  import { bundleShared } from "./bundle-shared.js";
7
- import { densifyExternals } from "../output/densify-externals.js";
8
- import { writeFederationInfo } from "../output/write-federation-info.js";
9
- import { writeImportMap } from "../output/write-import-map.js";
3
+ import { assembleFederationInfo } from "./assemble-federation-info.js";
4
+ import { writeFederationOutputs } from "../output/write-federation-outputs.js";
10
5
  import { logger } from "../../utils/logger.js";
11
6
  import { AbortedError } from "../../utils/errors.js";
12
7
  import { addExternalsToCache } from "../cache/federation-cache.js";
13
8
  import { planSharedBundles } from "./shared-bundle-plan.js";
14
- import path from "path";
15
9
  async function buildForFederation(config, fedOptions, externals, signal) {
16
- fedOptions.federationCache.cachePath = path.join(
17
- fedOptions.federationCache.cachePath,
18
- fedOptions.projectName
19
- );
20
10
  logger.info("Building federation artifacts");
21
11
  logger.notice("Skip packages you don't want to share in your federation config");
22
12
  await executeSharedBundlePlans(planSharedBundles(config, externals), config, fedOptions, signal);
@@ -31,36 +21,8 @@ async function buildForFederation(config, fedOptions, externals, signal) {
31
21
  logger.measure(start, "Step 3) Bundling all internal libraries and exposed modules.");
32
22
  if (signal?.aborted)
33
23
  throw new AbortedError("[buildForFederation] After exposed-and-mappings bundle");
34
- const exposedInfo = !artifactInfo ? describeExposed(config, fedOptions) : artifactInfo.exposes;
35
- const sharedMappingInfo = !artifactInfo ? describeSharedMappings(config, fedOptions) : artifactInfo.mappings;
36
- const sharedExternals = [...fedOptions.federationCache.externals, ...sharedMappingInfo];
37
- if (config?.shareScope) {
38
- Object.values(sharedExternals).forEach((external) => {
39
- if (!external.shareScope) external.shareScope = config.shareScope;
40
- });
41
- }
42
- const shared = config.features.denseExternals ? densifyExternals(sharedExternals) : sharedExternals;
43
- const buildNotificationsEndpoint = fedOptions.buildNotifications?.enable && fedOptions.dev ? fedOptions.buildNotifications?.endpoint : void 0;
44
- const federationInfo = {
45
- name: config.name,
46
- shared,
47
- exposes: exposedInfo,
48
- buildNotificationsEndpoint
49
- };
50
- if (fedOptions.federationCache.chunks) {
51
- federationInfo.chunks = fedOptions.federationCache.chunks;
52
- }
53
- if (artifactInfo?.chunks) {
54
- federationInfo.chunks = { ...federationInfo.chunks ?? {}, ...artifactInfo?.chunks };
55
- }
56
- if (config.features.integrityHashes) {
57
- federationInfo.integrity = {
58
- ...fedOptions.federationCache.integrity ?? {},
59
- ...artifactInfo?.integrity ?? {}
60
- };
61
- }
62
- writeFederationInfo(federationInfo, fedOptions);
63
- writeImportMap(fedOptions.federationCache, fedOptions, federationInfo.integrity);
24
+ const federationInfo = assembleFederationInfo(config, fedOptions, artifactInfo);
25
+ writeFederationOutputs(federationInfo, fedOptions);
64
26
  return federationInfo;
65
27
  }
66
28
  async function executeSharedBundlePlans(plans, config, fedOptions, signal) {
@@ -75,7 +37,7 @@ async function executeSharedBundlePlans(plans, config, fedOptions, signal) {
75
37
  logger.measure(start, `Step 2.1) Bundling '${plan.bundleName}' externals`);
76
38
  addExternalsToCache(fedOptions.federationCache, info);
77
39
  if (signal?.aborted)
78
- throw new AbortedError(`[buildForFederation] After ${plan.bundleName} bundle`);
40
+ throw new AbortedError(`[executeSharedBundlePlans] After ${plan.bundleName} bundle`);
79
41
  }
80
42
  const separatePlans = plans.filter((p) => p.kind === "separate");
81
43
  if (separatePlans.length > 0) {
@@ -91,7 +53,7 @@ async function executeSharedBundlePlans(plans, config, fedOptions, signal) {
91
53
  );
92
54
  logger.measure(start, "Step 2.2) Bundling all separate external packages");
93
55
  for (const info of results) addExternalsToCache(fedOptions.federationCache, info);
94
- if (signal?.aborted) throw new AbortedError("[buildForFederation] After separate bundle");
56
+ if (signal?.aborted) throw new AbortedError("[executeSharedBundlePlans] After separate bundle");
95
57
  }
96
58
  }
97
59
  export {
@@ -1,4 +1,4 @@
1
- import type { ArtifactInfo, ExposesInfo, SharedInfo } from '../../domain/core/federation-info.contract.js';
1
+ import type { ArtifactInfo } from '../../domain/core/federation-info.contract.js';
2
2
  import type { FileReaderPort } from '../../domain/utils/io-port.contract.js';
3
3
  import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
4
4
  import { type NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
@@ -7,7 +7,5 @@ export declare function bundleExposedAndMappings(config: NormalizedFederationCon
7
7
  export declare function bundleExposedAndMappingsCore(deps: {
8
8
  adapter: NFBuildAdapter;
9
9
  }, config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], modifiedFiles?: string[], signal?: AbortSignal): Promise<ArtifactInfo>;
10
- export declare function describeExposed(config: NormalizedFederationConfig, options: NormalizedFederationOptions): Array<ExposesInfo>;
11
- export declare function describeSharedMappings(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions): Array<SharedInfo>;
12
10
  export declare function getMappingVersionCore(io: FileReaderPort, fileName: string, workspaceRoot: string): string;
13
11
  export declare function getMappingVersion(fileName: string, workspaceRoot: string): string;
@@ -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
  }
@@ -108,40 +115,23 @@ async function bundleExposedAndMappingsCore(deps, config, fedOptions, externals,
108
115
  const integrity = config.features.integrityHashes ? computeIntegrityMap([...entryFiles, ...chunkPaths], "") : void 0;
109
116
  return { mappings: sharedResult, exposes: exposedResult, chunks: exportedChunks, integrity };
110
117
  }
111
- function describeExposed(config, options) {
112
- const result = [];
113
- for (const key in config.exposes) {
114
- const expose = config.exposes[key];
115
- const localPath = normalize(
116
- path.normalize(path.join(options.workspaceRoot, expose.file))
117
- );
118
- result.push({
119
- key,
120
- outFileName: "",
121
- ...expose.element && { element: expose.element },
122
- dev: !options.dev ? void 0 : {
123
- entryPoint: localPath
124
- }
125
- });
126
- }
127
- return result;
128
- }
129
- function describeSharedMappings(config, fedOptions) {
130
- const result = [];
131
- for (const [mappedPath, mappedImport] of Object.entries(config.sharedMappings)) {
132
- result.push(toSharedMappingInfo(mappedPath, mappedImport, "", config, fedOptions));
133
- }
134
- return result;
135
- }
136
118
  function toSharedMappingInfo(mappedPath, mappedImport, outFileName, config, fedOptions) {
137
119
  const mappingVersion = config.features.mappingVersion ? getMappingVersion(mappedPath, fedOptions.workspaceRoot) : "";
120
+ const mappingConfig = resolveMappingConfig(
121
+ mappedImport,
122
+ config.sharedMappingsConfig,
123
+ config.features.mappingVersion
124
+ );
125
+ const version = mappingConfig.version ?? mappingVersion;
138
126
  return {
139
127
  packageName: mappedImport,
140
128
  outFileName,
141
- requiredVersion: mappingVersion.length > 0 ? "~" + mappingVersion : "",
142
- singleton: true,
143
- strictVersion: config.features.mappingVersion,
144
- version: mappingVersion,
129
+ requiredVersion: mappingConfig.requiredVersion ?? (version.length > 0 ? "~" + version : ""),
130
+ singleton: mappingConfig.singleton,
131
+ strictVersion: mappingConfig.strictVersion,
132
+ version,
133
+ ...mappingConfig.shareScope && { shareScope: mappingConfig.shareScope },
134
+ ...mappingConfig.pool && { pool: mappingConfig.pool },
145
135
  dev: !fedOptions.dev ? void 0 : {
146
136
  entryPoint: normalize(path.normalize(mappedPath))
147
137
  }
@@ -171,8 +161,6 @@ function getMappingVersion(fileName, workspaceRoot) {
171
161
  export {
172
162
  bundleExposedAndMappings,
173
163
  bundleExposedAndMappingsCore,
174
- describeExposed,
175
- describeSharedMappings,
176
164
  getMappingVersion,
177
165
  getMappingVersionCore
178
166
  };
@@ -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";
@@ -41,13 +42,18 @@ async function bundleSharedCore(deps, sharedBundles, config, fedOptions, externa
41
42
  deps.io,
42
43
  deps.repo
43
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
+ }
44
49
  const checksum = getChecksumCore(
45
50
  deps.io,
46
51
  sharedBundles,
47
52
  fedOptions.dev ? "1" : "0",
48
53
  builderVersion,
49
- config.features.synthesizeCjsExports,
50
- contentSignals
54
+ config.features,
55
+ contentSignals,
56
+ resolvedVersions
51
57
  );
52
58
  const bundleCache = cacheEntryCore(
53
59
  deps.io,
@@ -1,10 +1,6 @@
1
- import {
2
- bundleExposedAndMappings,
3
- describeExposed,
4
- describeSharedMappings
5
- } from "./bundle-exposed-and-mappings.js";
6
- import { writeFederationInfo } from "../output/write-federation-info.js";
7
- import { writeImportMap } from "../output/write-import-map.js";
1
+ import { bundleExposedAndMappings } from "./bundle-exposed-and-mappings.js";
2
+ import { assembleFederationInfo } from "./assemble-federation-info.js";
3
+ import { writeFederationOutputs } from "../output/write-federation-outputs.js";
8
4
  import { logger } from "../../utils/logger.js";
9
5
  import { AbortedError } from "../../utils/errors.js";
10
6
  import { planSharedBundles } from "./shared-bundle-plan.js";
@@ -14,7 +10,6 @@ import { cacheEntryCore, getFilename } from "../cache/cache-persistence.js";
14
10
  import { nodeIo } from "../../utils/io/node-io-adapter.js";
15
11
  import { sharedPackageJsonRepository } from "../../utils/package/package-info.js";
16
12
  async function rebuildForFederation(config, fedOptions, externals, modifiedFiles, signal) {
17
- const federationCache = fedOptions.federationCache;
18
13
  await rebuildAffectedExternals(config, fedOptions, externals, modifiedFiles, signal);
19
14
  logger.info(`Re-bundling all internal libraries and exposed modules..'`);
20
15
  const start = process.hrtime();
@@ -27,31 +22,9 @@ async function rebuildForFederation(config, fedOptions, externals, modifiedFiles
27
22
  );
28
23
  logger.measure(start, "To re-bundle all internal libraries and exposed modules.");
29
24
  if (signal?.aborted)
30
- throw new AbortedError("[buildForFederation] After exposed-and-mappings bundle");
31
- const exposedInfo = !artifactInfo ? describeExposed(config, fedOptions) : artifactInfo.exposes;
32
- const sharedMappingInfo = !artifactInfo ? describeSharedMappings(config, fedOptions) : artifactInfo.mappings;
33
- const sharedExternals = [...federationCache.externals, ...sharedMappingInfo];
34
- const buildNotificationsEndpoint = fedOptions.buildNotifications?.enable && fedOptions.dev ? fedOptions.buildNotifications?.endpoint : void 0;
35
- const federationInfo = {
36
- name: config.name,
37
- shared: sharedExternals,
38
- exposes: exposedInfo,
39
- buildNotificationsEndpoint
40
- };
41
- if (federationCache.chunks) {
42
- federationInfo.chunks = federationCache.chunks;
43
- }
44
- if (artifactInfo?.chunks) {
45
- federationInfo.chunks = { ...federationInfo.chunks ?? {}, ...artifactInfo?.chunks };
46
- }
47
- if (config.features.integrityHashes) {
48
- federationInfo.integrity = {
49
- ...federationCache.integrity ?? {},
50
- ...artifactInfo?.integrity ?? {}
51
- };
52
- }
53
- writeFederationInfo(federationInfo, fedOptions);
54
- writeImportMap(federationCache, fedOptions, federationInfo.integrity);
25
+ throw new AbortedError("[rebuildForFederation] After exposed-and-mappings bundle");
26
+ const federationInfo = assembleFederationInfo(config, fedOptions, artifactInfo);
27
+ writeFederationOutputs(federationInfo, fedOptions);
55
28
  return federationInfo;
56
29
  }
57
30
  async function rebuildAffectedExternals(config, fedOptions, externals, modifiedFiles, signal, io = nodeIo, repo = sharedPackageJsonRepository) {
@@ -8,6 +8,22 @@ export declare function resolveSharedPackageDirs(config: NormalizedFederationCon
8
8
  /** Realpath'd dirs of symlinked shared packages — the bounded watch set.
9
9
  * Deduped, since secondaries share a package dir. */
10
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[];
11
27
  /** Per-key content signal (max mtime of the resolved dir) for symlinked deps only.
12
28
  * Registry deps get no signal, keeping their checksum version-only. (Every key is
13
29
  * still resolved: detecting the symlink requires the realpath + lstat.) */
@@ -1,7 +1,7 @@
1
1
  import * as path from "path";
2
2
  import { nodeIo } from "../../utils/io/node-io-adapter.js";
3
3
  import { sharedPackageJsonRepository } from "../../utils/package/package-info.js";
4
- import { toPosix } from "../../utils/path-patterns.js";
4
+ import { isUnderDir, toPosix } from "../../utils/path-patterns.js";
5
5
  const folderOf = (fedOptions) => fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
6
6
  function resolveEntries(keys, folder, io, repo) {
7
7
  const out = [];
@@ -25,6 +25,10 @@ function linkedSharedDirs(config, fedOptions, io = nodeIo, repo = sharedPackageJ
25
25
  const entries = resolveEntries(Object.keys(config.shared), folderOf(fedOptions), io, repo);
26
26
  return [...new Set(entries.filter((e) => e.isSymlink).map((e) => e.realDir))];
27
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
+ }
28
32
  function maxMtime(io, dir) {
29
33
  let max = 0;
30
34
  const walk = (d) => {
@@ -53,8 +57,7 @@ function affectedSharedKeys(modifiedFiles, dirs, io = nodeIo) {
53
57
  if (modifiedFiles.length === 0 || dirs.size === 0) return affected;
54
58
  const realFiles = modifiedFiles.map((f) => toPosix(io.realpath(f)));
55
59
  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);
60
+ if (realFiles.some((f) => isUnderDir(f, dir))) affected.add(key);
58
61
  }
59
62
  return affected;
60
63
  }
@@ -62,5 +65,6 @@ export {
62
65
  affectedSharedKeys,
63
66
  linkedContentSignals,
64
67
  linkedSharedDirs,
65
- resolveSharedPackageDirs
68
+ resolveSharedPackageDirs,
69
+ sharedMappingDirs
66
70
  };
@@ -1,10 +1,12 @@
1
1
  import type { NormalizedExternalConfig } from '../../domain/config/external-config.contract.js';
2
+ import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
2
3
  import type { ChunkInfo, IntegrityMap, SharedInfo } from '../../domain/core/federation-info.contract.js';
3
4
  import type { FileReaderPort, FileWriterPort, HashPort } from '../../domain/utils/io-port.contract.js';
4
5
  export declare const getDefaultCachePath: (workspaceRoot: string) => string;
5
6
  export declare const getFilename: (title: string, dev?: boolean) => string;
6
- export declare const getChecksum: (shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string, synthesizeCjsExports?: boolean, contentSignals?: Record<string, string>) => string;
7
- export declare const getChecksumCore: (hash: HashPort, shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string, synthesizeCjsExports?: boolean, contentSignals?: Record<string, string>) => string;
7
+ export declare const getChecksum: (shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string, features?: FeatureFlags, contentSignals?: Record<string, string>, resolvedVersions?: Record<string, string>) => string;
8
+ export type FeatureFlags = Partial<NormalizedFederationConfig['features']>;
9
+ export declare const getChecksumCore: (hash: HashPort, shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string, features?: FeatureFlags, contentSignals?: Record<string, string>, resolvedVersions?: Record<string, string>) => string;
8
10
  export type CacheMetadata = {
9
11
  checksum: string;
10
12
  externals: SharedInfo[];
@@ -6,15 +6,31 @@ const getFilename = (title, dev) => {
6
6
  const devSuffix = dev ? "-dev" : "";
7
7
  return `${title}${devSuffix}.meta.json`;
8
8
  };
9
- const getChecksum = (shared, dev, builderVersion = "", synthesizeCjsExports = true, contentSignals = {}) => getChecksumCore(nodeIo, shared, dev, builderVersion, synthesizeCjsExports, contentSignals);
10
- const getChecksumCore = (hash, shared, dev, builderVersion = "", synthesizeCjsExports = true, contentSignals = {}) => {
9
+ const getChecksum = (shared, dev, builderVersion = "", features = {}, contentSignals = {}, resolvedVersions = {}) => getChecksumCore(nodeIo, shared, dev, builderVersion, features, contentSignals, resolvedVersions);
10
+ const featureState = (features) => Object.entries(features).sort(([a], [b]) => a < b ? -1 : 1).map(([flag, on]) => `${flag}=${on ? "1" : "0"}`).join(",");
11
+ const SHARED_INFO_FIELDS = [
12
+ "requiredVersion",
13
+ "singleton",
14
+ "strictVersion",
15
+ "shareScope",
16
+ "pool"
17
+ ];
18
+ const sharedInfoState = (config) => {
19
+ const values = SHARED_INFO_FIELDS.map((field) => config[field] ?? null);
20
+ return values.every((value) => value === null) ? "" : `!${JSON.stringify(values)}`;
21
+ };
22
+ const getChecksumCore = (hash, shared, dev, builderVersion = "", features = {}, contentSignals = {}, resolvedVersions = {}) => {
11
23
  const denseExternals = Object.keys(shared).sort().reduce((clean, external) => {
12
- const version = shared[external].version ? `@${shared[external].version}` : "";
24
+ const installed = resolvedVersions[external];
25
+ const declared = shared[external].version;
26
+ const version = installed ? `~${installed}` : declared ? `@${declared}` : "";
13
27
  const signal = contentSignals[external] ? `#${contentSignals[external]}` : "";
14
- return clean + ":" + external + version + signal;
28
+ return clean + ":" + external + version + sharedInfoState(shared[external]) + signal;
15
29
  }, "deps");
16
- const cjs = synthesizeCjsExports ? "1" : "0";
17
- return hash.hash("sha256", denseExternals + `:dev=${dev}:builder=${builderVersion}:cjs=${cjs}`).hex();
30
+ return hash.hash(
31
+ "sha256",
32
+ denseExternals + `:dev=${dev}:builder=${builderVersion}:features=${featureState(features)}`
33
+ ).hex();
18
34
  };
19
35
  const cacheEntryCore = (io, pathToCache, fileName) => {
20
36
  const metadataFile = path.join(pathToCache, fileName);
@@ -36,8 +52,11 @@ const cacheEntryCore = (io, pathToCache, fileName) => {
36
52
  io.mkdirp(fullOutputPath);
37
53
  cachedResult.files.forEach((file) => {
38
54
  const cachedFile = path.join(pathToCache, file);
39
- const distFileName = path.join(fullOutputPath, file);
40
- if (io.exists(cachedFile)) io.copyFile(cachedFile, distFileName);
55
+ if (!io.exists(cachedFile))
56
+ throw new Error(
57
+ `Cached artifact '${file}' recorded in '${metadataFile}' is missing. Delete '${pathToCache}' and rebuild.`
58
+ );
59
+ io.copyFile(cachedFile, path.join(fullOutputPath, file));
41
60
  });
42
61
  },
43
62
  clear: () => {
@@ -1,11 +1,11 @@
1
1
  import type { NormalizedFederationConfig } from '../domain/config/federation-config.contract.js';
2
2
  import type { FederationOptions, NormalizedFederationOptions } from '../domain/core/federation-options.contract.js';
3
- import type { FileReaderPort } from '../domain/utils/io-port.contract.js';
3
+ import type { FileReaderPort, GlobPort } from '../domain/utils/io-port.contract.js';
4
4
  import { type FederationCache } from '../../domain.js';
5
5
  import { getUsedDependenciesFactory } from '../config/get-used-dependencies.js';
6
6
  type ConfigLoader = (fullConfigPath: string) => Promise<NormalizedFederationConfig>;
7
7
  interface NormalizeFederationDeps {
8
- io: FileReaderPort;
8
+ io: FileReaderPort & GlobPort;
9
9
  loadConfig: ConfigLoader;
10
10
  usedDependenciesFactory?: typeof getUsedDependenciesFactory;
11
11
  }
@@ -2,6 +2,8 @@ import * as path from "path";
2
2
  import { pathToFileURL } from "url";
3
3
  import { nodeIo } from "../utils/io/node-io-adapter.js";
4
4
  import { removeUnusedDeps } from "../config/remove-unused-deps.js";
5
+ import { expandOrDropWildcards } from "../config/expand-mappings.js";
6
+ import { assertBarrelMappings } from "../config/validate-mappings.js";
5
7
  import { createFederationCache } from "./cache/federation-cache.js";
6
8
  import { getDefaultCachePath } from "./cache/cache-persistence.js";
7
9
  import { getUsedDependenciesFactory } from "../config/get-used-dependencies.js";
@@ -21,35 +23,45 @@ async function normalizeFederationOptionsCore(deps, options, cache) {
21
23
  throw new Error("Expected " + fullConfigPath);
22
24
  }
23
25
  let config = await deps.loadConfig(fullConfigPath);
24
- const federationCache = cache ?? createFederationCache(
26
+ const projectName = resolveProjectName(options.projectName ?? config.name);
27
+ const suppliedCache = cache ?? createFederationCache(
25
28
  getDefaultCachePath(options.workspaceRoot)
26
29
  );
30
+ const federationCache = {
31
+ ...suppliedCache,
32
+ cachePath: path.join(suppliedCache.cachePath, projectName),
33
+ externals: [...suppliedCache.externals]
34
+ };
27
35
  const normalizedOptions = {
28
36
  ...options,
29
37
  entryPoints: options.entryPoints ?? Object.values(config.exposes ?? {}).map((e) => e.file),
30
- projectName: resolveProjectName(options.projectName ?? config.name),
38
+ projectName,
31
39
  cacheExternalArtifacts: options.cacheExternalArtifacts ?? true,
32
40
  federationCache
33
41
  };
34
- if (config.features.ignoreUnusedDeps) {
42
+ const nothingShared = Object.keys(config.shared).length === 0 && Object.keys(config.sharedMappings).length === 0;
43
+ if (nothingShared) {
44
+ logger.debug("Nothing is shared, skipping the used dependency scan.");
45
+ } else if (config.features.ignoreUnusedDeps) {
35
46
  const getUsedDeps = (deps.usedDependenciesFactory ?? getUsedDependenciesFactory)(
36
47
  options.workspaceRoot,
37
48
  options.entryPoints
38
49
  );
39
- config = removeUnusedDeps(getUsedDeps(config), config);
50
+ config = removeUnusedDeps(getUsedDeps(config), config, {
51
+ io: deps.io,
52
+ workspaceRoot: options.workspaceRoot
53
+ });
40
54
  logger.info("Removed unused dependencies.");
41
55
  logger.debug(
42
56
  'This can be disabled per dependency/external using the "includeSecondaries: {keepAll: true}" property. Or in general by disabling the "ignoreUnusedDeps" feature. '
43
57
  );
44
58
  } else {
45
- const withWildcard = Object.keys(config.sharedMappings).some((m) => m.includes("*"));
46
- if (withWildcard) {
47
- logger.warn(
48
- "Sharing mapped paths with wildcards (*) is only supported with ignoreUnusedDeps feature."
49
- );
50
- config.sharedMappings = Object.entries(config.sharedMappings).filter(([_path]) => !_path.includes("*")).reduce((acc, [_path, _import]) => ({ ...acc, [_path]: _import }), {});
51
- }
59
+ config.sharedMappings = expandOrDropWildcards(config, {
60
+ io: deps.io,
61
+ workspaceRoot: options.workspaceRoot
62
+ });
52
63
  }
64
+ assertBarrelMappings(config.sharedMappings);
53
65
  return { config, options: normalizedOptions };
54
66
  }
55
67
  function resolveProjectName(name) {
@@ -0,0 +1,7 @@
1
+ import type { FederationInfo } from '../../domain/core/federation-info.contract.js';
2
+ import type { NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
3
+ /**
4
+ * The import map is built from the flat federation cache rather than `federationInfo.shared`: a
5
+ * dense group has no import-map representation, every specifier needs its own entry.
6
+ */
7
+ export declare function writeFederationOutputs(federationInfo: FederationInfo, fedOptions: NormalizedFederationOptions): void;
@@ -0,0 +1,9 @@
1
+ import { writeFederationInfo } from "./write-federation-info.js";
2
+ import { writeImportMap } from "./write-import-map.js";
3
+ function writeFederationOutputs(federationInfo, fedOptions) {
4
+ writeFederationInfo(federationInfo, fedOptions);
5
+ writeImportMap(fedOptions.federationCache, fedOptions, federationInfo.integrity);
6
+ }
7
+ export {
8
+ writeFederationOutputs
9
+ };