Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,11 +641,27 @@ A common local-development setup is to build a shared library in its own repo (w

Since Native Federation shares such a library as an _external_ (it is excluded from Angular's own build), a plain `ng serve` used to ignore edits to it — the change lived under `node_modules`, which the build watcher skips, so you had to clear the cache or restart the dev server to see it. Since version `22.0.6` (requires `@softarc/native-federation` ≥ `4.3.2`), the adapter detects linked shared packages and re-bundles them automatically on change.

Two things about that detection have since changed (requires `@softarc/native-federation` ≥ `4.5.0`):

- **A symlink alone no longer counts as linked.** Package managers that symlink by default — pnpm's default `isolated` linker, or Yarn's `nodeLinker: pnpm` — make _every_ dependency a symlink, which used to put the whole dependency graph on the watch list. A package is now treated as a live checkout only when its real path resolves **outside** every `node_modules` tree, which is exactly what `npm link` produces and what a package manager's internal symlink does not.
- **Watching linked packages is opt-in**, via the `watchLinkedDeps` builder option below. It is off by default because watching means polling the linked checkout for as long as the dev server runs.

Turning the option off does **not** mean edits to a linked library are ignored. A library's type declarations are a TypeScript input, and the adapter resolves them to their real path outside `node_modules` — so an ng-packagr rebuild, which rewrites `dist/*.d.ts` alongside the JavaScript, is already noticed and re-bundled with the option off. What the option adds is coverage of changes that touch no such input: a JavaScript-only edit, or a rebuild whose emitted types come out byte-identical.

A cold `ng build` re-bundles a changed linked library either way, since the adapter checksums each linked package's content on every build. A running `ng serve` is weaker: with the option off, a change touching no watched input can stay stale until you restart the server or run a build.

So that the default is never a silent surprise, a watching build that finds a linked shared package while the option is off says so once at startup:

```
INFO Detected npm-linked shared packages: @my-scope/my-lib. Set 'watchLinkedDeps' to true to rebuild when they change.
```

#### Requirements

- The library is listed in your `federation.config.*` `shared` section (via `shareAll`, an explicit `shared` entry, or `sharedMappings`).
- Its package directory under `node_modules` is a **symlink** — i.e. it was linked with `npm link` (or your package manager's equivalent), not installed from a registry.
- Its package directory under `node_modules` is a **symlink pointing outside `node_modules`** — i.e. it was linked with `npm link` (or your package manager's equivalent), not installed from a registry and not symlinked by your package manager's own linker.
- The library is rebuilt on change so the symlink target actually updates. With an Angular library this means running `ng build --watch` (ng-packagr) in the library's repo.
- For the changes an ng-packagr rebuild does not cover on its own — see above — `watchLinkedDeps` is set to `true` on the builder target you are running.

#### Workflow

Expand All @@ -663,13 +679,33 @@ npm link @my-scope/my-lib
ng serve
```

Watching is opt-in, so enable it on the target you serve with:

```json
"serve": {
"builder": "@angular-architects/native-federation:build",
"options": {
"target": "host:serve-original:development",
"watchLinkedDeps": true
}
}
```

The option is available on both the `:build` and `:remote` builders and defaults to `false`.

Now edit a source file in the library. ng-packagr rebuilds its `dist/`, and the adapter picks up the change, re-bundles the affected shared external, and logs `Done!` — no manual cache clear or dev-server restart needed.

To also refresh the browser automatically when the rebuild finishes, enable SSE-based reloading as described in [Shell reloading when MFE finishes building for local development](#shell-reloading-when-mfe-finishes-building-for-local-development) (`initFederation(manifest, { sse: true })`). Otherwise, a manual browser refresh will show the update.

#### How it works

The adapter resolves the real path of each symlinked shared package and adds it to the federation file watcher. Because linked packages live under `node_modules`, they are watched via polling, and a short debounce coalesces ng-packagr's atomic multi-file writes into a single rebuild. Only the shared externals affected by the change are re-bundled; regular (registry-installed) dependencies keep the version-only cache fast path, so there is no rebuild churn or performance regression for non-linked packages.
The adapter resolves the real path of each shared package and treats it as a live checkout when that path lies outside every `node_modules` tree. With `watchLinkedDeps` enabled, those directories are added to the federation file watcher; a short debounce coalesces ng-packagr's atomic multi-file writes into a single rebuild. Only the shared externals affected by the change are re-bundled; regular (registry-installed) dependencies keep the version-only cache fast path, so there is no rebuild churn or performance regression for non-linked packages.

Watching a linked checkout polls it, which is why it is off by default: a registry dependency is bundled once and cached by checksum, so its bytes cannot change without its version changing, and watching it could never change an outcome. Only a linked checkout changes content under a fixed version.

Angular draws the same line but wires it to `preserveSymlinks`: when it watches the project root it ignores `**/node_modules/**`, and skips that ignore when `preserveSymlinks` is on, precisely so `npm link` keeps working. It has to overload one flag because its resolver decides which path esbuild sees. The adapter does not, because it resolves each shared package's real path itself — so `watchLinkedDeps` governs watching and nothing else.

That separation is worth keeping, so do **not** reach for `preserveSymlinks` here. It changes module resolution: it is the classic route to loading two copies of a singleton like `@angular/core`, and under pnpm it makes every dependency resolve through `.pnpm`. It would also shrink the watch set rather than grow it, since the adapter skips any path with a `node_modules` segment and `preserveSymlinks` is exactly what makes a linked library's files report as `node_modules/@my-scope/my-lib/…` instead of their real location.

## FAQ

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
"@angular-devkit/core": "~22.1.0",
"@angular-devkit/schematics": "~22.1.0",
"@chialab/esbuild-plugin-commonjs": "^0.19.0",
"@softarc/native-federation": "^4.4.0",
"@softarc/native-federation": "~4.5.0-next.1",
"@softarc/native-federation-orchestrator": "^4.5.2",
"es-module-shims": "^2.8.0",
"esbuild": "^0.28.0",
"mrmime": "^2.0.1"
"mrmime": "^2.0.1",
"watchpack": "^2.5.2"
},
"repository": {
"type": "git",
Expand Down
86 changes: 43 additions & 43 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading