@softarc/native-federation 4.5.0-next.1 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -363,7 +363,7 @@ shared: share({
363
363
  })
364
364
  ```
365
365
 
366
- Finally, it's also possible to break out of the `ignoreUnusedDeps` feature for specific externals if desired, for example when sharing a whole suite of interconnected external dependencies like @angular/core. This can be handy when you want to avoid the chance of cross-version secondary entrypoints being used by the different micro frontends. E.g. mfe1 uses @angular/core v20.1.0 and mfe2 uses @angular/core/rxjs-interop v20.0.8, then you might want consistent use of v20.1.0 so rxjs-interop should be exported by mfe1. The `keepAll` prop allows you to enforce this:
366
+ Finally, it's also possible to exempt the secondary entry points of a specific external from the `ignoreUnusedDeps` feature, for example when sharing a whole suite of interconnected external dependencies like @angular/core. This can be handy when you want to avoid the chance of cross-version secondary entrypoints being used by the different micro frontends. E.g. mfe1 uses @angular/core v20.1.0 and mfe2 uses @angular/core/rxjs-interop v20.0.8, then you might want consistent use of v20.1.0 so rxjs-interop should be exported by mfe1. The `keepAll` prop allows you to enforce this:
367
367
 
368
368
  ```typescript
369
369
  shared: share({
@@ -377,6 +377,10 @@ shared: share({
377
377
  })
378
378
  ```
379
379
 
380
+ `keepAll` is read per **package family**, not per entry point: every entry point of @angular/core is published as long as _something_ still reaches @angular/core, but a package nothing imports at all is pruned anyway. That is what keeps the feature meaningful when `keepAll` is applied to every package at once — it exempts the secondaries from reachability, not the package itself. For a package with no secondary entry points the family is the package itself, so the flag changes nothing there. Use `ignoreUnusedDeps: false` to publish everything unconditionally.
381
+
382
+ Note that mapped paths read the same flag differently: there, `keepAll` opts the mapping out of reachability entirely (see [Keeping mappings that nothing imports](#keeping-mappings-that-nothing-imports)).
383
+
380
384
  The API for configuring and using Native Federation is very similar to the one provided by our Module Federation plugin [@angular-architects/module-federation](https://www.npmjs.com/package/@angular-architects/module-federation). Hence, most of the articles on it are also valid for Native Federation.
381
385
 
382
386
  ### Sharing
@@ -487,7 +491,7 @@ module.exports = withNativeFederation({
487
491
  });
488
492
  ```
489
493
 
490
- - `keepAll` keeps the mapping even when nothing imports it. This is read exactly as it is for a shared package, so a bare `includeSecondaries: true` opts out too.
494
+ - `keepAll` keeps the mapping even when nothing imports it, and on a mapping a bare `includeSecondaries: true` means the same thing — a mapping has no secondary entry points, so the flag can only mean "exempt from reachability". A shared package reads it differently: `true` is the default there and only means "share the secondaries", so `{ keepAll: true }` is the only spelling that affects pruning — and even then the package itself still has to be reached.
491
495
  - `resolveGlob` is additionally required for **wildcard** mappings. A wildcard is a pattern rather than an entry point, and normally only the reachability scan turns it into concrete files; `resolveGlob` expands it against the filesystem instead. Without it, a wildcard mapping is dropped with a warning.
492
496
 
493
497
  An expanded wildcard is named by the same rule the reachability scan uses, so `libs/ui/*` matching `libs/ui/button/index.ts` is shared as `@my-org/ui/button`.
@@ -496,7 +500,7 @@ Expansion only accepts **entry points**. A glob cannot tell a library's public s
496
500
 
497
501
  That restriction is not cosmetic: **only barrel imports can be shared as a mapped path.** A mapped path is advertised under its import specifier and marked external, so the specifier has to be one a browser import map can resolve, and a dot in the last segment does not resolve (see [vitejs/vite#21036](https://github.com/vitejs/vite/issues/21036)).
498
502
 
499
- So the rule is simply *would this end up in `remoteEntry.json`?* If it would, a non-barrel specifier fails the build:
503
+ So the rule is simply _would this end up in `remoteEntry.json`?_ If it would, a non-barrel specifier fails the build:
500
504
 
501
505
  ```
502
506
  Invalid 'shared mappings' config. Only barrel imports can be shared as a sharedMapping:
@@ -693,7 +697,7 @@ For a zero-build integration, declare your remotes in a manifest and include the
693
697
  </script>
694
698
 
695
699
  <!-- Include the orchestrator -->
696
- <script src="https://unpkg.com/@softarc/native-federation-orchestrator@4.5.1/quickstart.mjs"></script>
700
+ <script src="https://unpkg.com/@softarc/native-federation-orchestrator@4.6.0/quickstart.mjs"></script>
697
701
  ```
698
702
 
699
703
  The `mfe-loader-available` event signals that the orchestrator has fetched the
@@ -3,9 +3,13 @@ import {
3
3
  expandWildcardMapping,
4
4
  isWildcardMapping
5
5
  } from "./expand-mappings.js";
6
+ import { inferPackageFromSecondary } from "../utils/normalize.js";
6
7
  import { logger } from "../utils/logger.js";
7
8
  function removeUnusedDeps(usedDependencies, config, ctx) {
8
- const filteredDependencies = Object.entries(config.shared).filter(([shared, meta]) => !!meta.includeSecondaries || usedDependencies.external.has(shared)).reduce((acc, [shared, meta]) => ({ ...acc, [shared]: meta }), {});
9
+ const usedPackages = new Set([...usedDependencies.external].map(inferPackageFromSecondary));
10
+ const filteredDependencies = Object.entries(config.shared).filter(
11
+ ([shared, meta]) => meta.includeSecondaries ? usedPackages.has(inferPackageFromSecondary(shared)) : usedDependencies.external.has(shared)
12
+ ).reduce((acc, [shared, meta]) => ({ ...acc, [shared]: meta }), {});
9
13
  const sharedMappings = withoutSkippedMappings(
10
14
  { ...keptMappings(config, ctx), ...usedDependencies.internal },
11
15
  config.skip
@@ -6,9 +6,11 @@ import { logger } from "../../utils/logger.js";
6
6
  import { AbortedError } from "../../utils/errors.js";
7
7
  import { addExternalsToCache } from "../cache/federation-cache.js";
8
8
  import { planSharedBundles } from "./shared-bundle-plan.js";
9
+ import { hintUnwatchedLinkedDeps } from "./resolve-shared-dirs.js";
9
10
  async function buildForFederation(config, fedOptions, externals, signal) {
10
11
  logger.info("Building federation artifacts");
11
12
  logger.notice("Skip packages you don't want to share in your federation config");
13
+ hintUnwatchedLinkedDeps(config, fedOptions);
12
14
  await executeSharedBundlePlans(planSharedBundles(config, externals), config, fedOptions, signal);
13
15
  const start = process.hrtime();
14
16
  const artifactInfo = await bundleExposedAndMappings(
@@ -8,6 +8,9 @@ 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. Empty unless `watchLinkedDeps` is on. */
10
10
  export declare function linkedSharedDirs(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, io?: FileReaderPort, repo?: PackageJsonRepository): string[];
11
+ /** Names the npm-linked shared packages a watching build leaves unwatched -- see
12
+ * AGENTS.md "What to watch". Advisory only, so a resolution failure must not fail the build. */
13
+ export declare function hintUnwatchedLinkedDeps(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, io?: FileReaderPort, repo?: PackageJsonRepository): void;
11
14
  /**
12
15
  * Source directories of the workspace libs in `config.sharedMappings` — the watch set
13
16
  * for mappings, derived from config alone rather than from a bundler cache.
@@ -1,6 +1,10 @@
1
1
  import * as path from "path";
2
2
  import { nodeIo } from "../../utils/io/node-io-adapter.js";
3
- import { sharedPackageJsonRepository } from "../../utils/package/package-info.js";
3
+ import { logger } from "../../utils/logger.js";
4
+ import {
5
+ getPkgFolder,
6
+ sharedPackageJsonRepository
7
+ } from "../../utils/io/package-json-repository.js";
4
8
  import { isOutsideNodeModules, isUnderDir, toPosix } from "../../utils/path-patterns.js";
5
9
  const folderOf = (fedOptions) => fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
6
10
  function resolveEntries(keys, folder, io, repo) {
@@ -28,6 +32,27 @@ function linkedSharedDirs(config, fedOptions, io = nodeIo, repo = sharedPackageJ
28
32
  const entries = resolveEntries(Object.keys(config.shared), folderOf(fedOptions), io, repo);
29
33
  return [...new Set(entries.filter((e) => e.isLinkedCheckout).map((e) => e.realDir))];
30
34
  }
35
+ function hintUnwatchedLinkedDeps(config, fedOptions, io = nodeIo, repo = sharedPackageJsonRepository) {
36
+ if (!fedOptions.watch || fedOptions.watchLinkedDeps) return;
37
+ try {
38
+ const names = /* @__PURE__ */ new Map();
39
+ for (const entry of resolveEntries(
40
+ Object.keys(config.shared),
41
+ folderOf(fedOptions),
42
+ io,
43
+ repo
44
+ )) {
45
+ if (entry.isLinkedCheckout && !names.has(entry.realDir)) {
46
+ names.set(entry.realDir, getPkgFolder(entry.key));
47
+ }
48
+ }
49
+ if (names.size === 0) return;
50
+ logger.notice(
51
+ `Detected npm-linked shared packages: ${[...names.values()].join(", ")}. Set 'watchLinkedDeps' to true to rebuild when they change.`
52
+ );
53
+ } catch {
54
+ }
55
+ }
31
56
  function sharedMappingDirs(config) {
32
57
  const dirs = Object.keys(config.sharedMappings).map((entryPoint) => toPosix(path.dirname(entryPoint))).filter(isOutsideNodeModules);
33
58
  return [...new Set(dirs)];
@@ -77,6 +102,7 @@ function affectedSharedKeys(modifiedFiles, dirs, io = nodeIo) {
77
102
  }
78
103
  export {
79
104
  affectedSharedKeys,
105
+ hintUnwatchedLinkedDeps,
80
106
  linkedContentSignals,
81
107
  linkedSharedDirs,
82
108
  resolveSharedPackageDirs,
@@ -59,7 +59,7 @@ async function normalizeFederationOptionsCore(deps, options, cache) {
59
59
  });
60
60
  logger.info("Removed unused dependencies.");
61
61
  logger.debug(
62
- 'This can be disabled per dependency/external using the "includeSecondaries: {keepAll: true}" property. Or in general by disabling the "ignoreUnusedDeps" feature. '
62
+ 'Keep everything with "ignoreUnusedDeps: false", or one mapping with "includeSecondaries: {keepAll: true}". On a shared package that flag only keeps the secondaries of a package something still imports.'
63
63
  );
64
64
  } else {
65
65
  config.sharedMappings = expandOrDropWildcards(config, {
@@ -14,9 +14,9 @@ export type SharedMappingConfigs = Record<string, ExternalConfig>;
14
14
  * have nothing to select. `requiredVersion` and `version` stay optional because their
15
15
  * defaults are read from the mapped lib's package.json at build time.
16
16
  *
17
- * `includeSecondaries` collapses to a boolean exactly as it does for a shared external:
18
- * it means "exempt from `ignoreUnusedDeps` pruning". `resolveGlob` is lifted out of it
19
- * because a mapping has no secondary entry points for it to apply to.
17
+ * `includeSecondaries` collapses to a boolean meaning "exempt from `ignoreUnusedDeps`
18
+ * pruning" stronger than on a shared external, where it only exempts the secondaries.
19
+ * `resolveGlob` is lifted out of it because a mapping has no secondaries to apply it to.
20
20
  */
21
21
  export interface NormalizedMappingConfig {
22
22
  singleton: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softarc/native-federation",
3
- "version": "4.5.0-next.1",
3
+ "version": "4.5.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "packageManager": "pnpm@11.18.0",
@@ -26,7 +26,7 @@
26
26
  "eslint": "^10.4.1",
27
27
  "globals": "^17.3.0",
28
28
  "jiti": "^2.6.1",
29
- "jsdom": "^29.0.0",
29
+ "jsdom": "^30.0.0",
30
30
  "knip": "^6.26.0",
31
31
  "prettier": "^3.9.4",
32
32
  "tslib": "^2.3.0",