Skip to content

fix: Unvalidated remote types, sharedMappings skip list, and strict mf-runtime - #1122

Merged
Aukevanoost merged 4 commits into
mainfrom
fix/mf-runtime-unvalidated-remote-type
Aug 7, 2026
Merged

fix: Unvalidated remote types, sharedMappings skip list, and strict mf-runtime#1122
Aukevanoost merged 4 commits into
mainfrom
fix/mf-runtime-unvalidated-remote-type

Conversation

@Aukevanoost

@Aukevanoost Aukevanoost commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Three defects found while building the webpack Module Federation demo, plus the test-config fix needed to run any of it. Each commit stands alone and can be reviewed in order.

1. loadRemoteModule could call through with unassigned variables

libs/mf-runtime/src/lib/loader/dynamic-federation.ts

The type === 'script' / 'module' chain had no else. The manifest is a runtime-fetched JSON file and parseConfig only defaults type, it never validates it — so a typo such as {"mfe1": {"type": "esm", ...}} survived into the config, matched neither branch, and left loadRemoteEntryOptions and key unassigned. loadRemoteEntry(undefined) then resolved without doing anything and lookupExposedModule(undefined, ...) failed with a TypeError naming neither the remote nor the bad type.

Options are now resolved into an internal union before use, so both variables are provably assigned, and an unsupported type throws an error naming the remote and the offending value.

This also clears the 9 --strict errors in the file. All fixes are internal narrowing — the exported types are untouched, verified by diffing the ng-packagr .d.ts output before and after: byte-identical for both entry points.

Side effect worth knowing: the old code mutated the caller's options object (options.type = ...). It no longer does.

2. libs/mf-runtime compiles with strict: true

With the above in place the library is strict-clean, so apps/shell can drop its own strict: false. That was never about the demo's code — the shell resolves @angular-architects/module-federation to libs/mf-runtime/src through the inherited tsconfig paths, and a strict program cannot consume loosely compiled sources. The demo is what people read and copy, so it should be checked the way their app will be. It also restores Angular's null-related template checks, which derive from strictNullChecks.

libs/mf and libs/mf-tools are still strict: false — nothing in the demo depends on them, so that is separate work.

3. sharedMappings never honoured the skip list ⚠️ breaking

Array.prototype.filter returns a new array; the result was discarded, so the call was a no-op and sharedMappings reached mappings.register() unfiltered.

Behaviour change: a package both listed in sharedMappings and on the skip list is no longer mapped or shared. Affected names are tslib, zone.js, @angular-architects/module-federation, @angular-architects/module-federation-runtime, the three @softarc/* entries, @angular/{router,common}/upgrade, and anything in the caller's own skip. In practice these are not things people put in sharedMappings — it exists for monorepo libraries resolved through tsconfig paths — but a config relying on the old behaviour will silently stop sharing that package.

Found while writing the tests, not changed here: the skip list is only consulted for an explicit sharedMappings array. Omit it and SharedMappings.register takes its share-all branch, mapping every non-wildcard tsconfig path — skip-listed or not. The new spec pins that asymmetry rather than changing it; worth a separate decision.

4. The three demo app specs had never run

All three failed to compile with TS2307 on @angular/core/testing. That subpath exists only in the package's exports map — no testing/ directory, and no top-level fallback the way there is for the package root — and tsconfig.base.json sets moduleResolution: node, which predates exports and cannot see it. (libs/mf-tools passes because it only imports the @angular/core root, which still resolves through the package's top-level typings.)

The spec configs move to bundler resolution. Only specs are affected; app builds go through Angular's builder with its own resolution.

Verification

  • Regression proof — each new spec was run against its pre-fix source. Item 1: 2 of 9 fail. Item 3: 4 of 6 fail. The rest pass on both, so existing behaviour is preserved and the new tests are not vacuous. The three app specs were checked the same way by flipping their expected values.
  • Public API — generated .d.ts diffed at each step, unchanged throughout.
  • Browser — headless Chrome over CDP against nx serve shell (Cypress cannot load the type="module" output), re-run after each item: manifest fetch, both loadRemoteModule call shapes, remoteEntry from :4201/:4202, and the shared AuthService singleton reading back across build boundaries. The only console error is the documented, inert import.meta one.
  • nx run-many -t lint test build is green across all 8 projects, and nx format:check is clean. This is the first commit where the full gate passes.

…mote types

The manifest is fetched at runtime and parseConfig only defaults `type`, it
never validates it. A typo such as `{"type": "esm"}` therefore reached
loadRemoteModule intact, matched neither the 'script' nor the 'module' branch,
and left `loadRemoteEntryOptions` and `key` unassigned. loadRemoteEntry(undefined)
then resolved without doing anything and lookupExposedModule(undefined, ...)
failed with a TypeError naming neither the remote nor the bad type.

Resolve the options into an internal union before use, so both variables are
provably assigned, and throw an error naming the remote and the offending value
instead. This also clears the 9 --strict errors in the file; all fixes are
internal narrowing, and the generated .d.ts is unchanged for both entry points.

Also drop a stale @ts-expect-error from test-setup.ts. The library had no spec,
so the file was never compiled and the unused directive went unnoticed.
With the unsupported-remote-type fix in place the library is strict-clean, so
the setting no longer has to be relaxed. The only remaining violation was the
`globalThis.ngJest` assignment in test-setup.ts, now written as an indexed
access instead of a suppression so it holds under either setting.

This lets apps/shell drop its own `strict: false`. That was never about the
demo's code — the shell resolves @angular-architects/module-federation to
libs/mf-runtime/src through the inherited tsconfig `paths`, and a strict program
cannot consume loosely compiled sources. The demo is the thing people read and
copy, so it should be checked the same way their app will be; it also restores
Angular's null-related template checks, which derive from strictNullChecks.

libs/mf and libs/mf-tools are still `strict: false` — nothing in the demo
depends on them, so that is separate work.
`Array.prototype.filter` returns a new array. The result was discarded, so the
call was a no-op and sharedMappings reached mappings.register() unfiltered.
Assign it back.

BREAKING CHANGE: a package that is both listed in `sharedMappings` and on the
skip list is no longer mapped or shared. Affected names are the ones assembled
into `skip`: tslib, zone.js, @angular-architects/module-federation,
@angular-architects/module-federation-runtime, the three @softarc/* entries,
@angular/router/upgrade, @angular/common/upgrade, and anything in the caller's
own `skip` option. In practice these are not things people put in
sharedMappings, which exists for monorepo libraries resolved through tsconfig
`paths`, but a config relying on the old behaviour will silently stop sharing
that package.

The skip list is still only consulted for an explicit `sharedMappings` array.
Omitting it maps every non-wildcard tsconfig path, skip-listed or not; the new
spec pins that asymmetry rather than changing it.
The three demo app specs have never run: all of them failed to compile with
TS2307 on '@angular/core/testing'. That subpath exists only in the package's
`exports` map -- there is no testing/ directory and no top-level fallback the
way there is for the package root -- and tsconfig.base.json sets
`moduleResolution: node`, which predates `exports` and cannot see it.

Switch the spec configs to bundler resolution, which reads `exports`. It
requires `module` to be es2015 or later, so that moves to esnext;
jest-preset-angular emits CommonJS either way, which is why the suites now run.

Only the specs are affected. The app builds go through Angular's builder, which
brings its own resolution, and no other project imports an exports-only subpath
-- libs/mf-tools only ever imports the @angular/core root, which still resolves
through the package's top-level `typings`.
@Aukevanoost
Aukevanoost merged commit 54a6a99 into main Aug 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant