Skip to content

fix(schematics): repair init's bootstrap.ts handling, add --webcomponent - #135

Merged
Aukevanoost merged 5 commits into
mainfrom
issues/core-re-export
Sep 16, 2026
Merged

Aukevanoost merged 5 commits into
mainfrom
issues/core-re-export

Conversation

@Aukevanoost

@Aukevanoost Aukevanoost commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Five commits. The schematic work is the reason for the PR; 548fb0e was already on the branch, and the last two are review fixes.

548fb0e chore: bad re-export of initFederation

initFederation's public signature took Record<string, string> | string while core's takes FederationManifest | string. Narrower than what it forwards to, so a valid manifest was a type error at the adapter boundary.

2992edc fix: derive bootstrap.ts from main.ts, not from its own existence

makeMainAsync keyed off tree.exists('bootstrap.ts') as a proxy for "already initialised". The two can disagree, and both directions were broken:

  • bootstrap.ts missing from an already-federated project (renamed, deleted, build entry repointed between runs): the guard fell through and copied main.ts into it. main.ts was the stub by then, so the new bootstrap.ts called initFederation again and did import('./bootstrap') on itself. The real bootstrap code went with the overwrite — no copy anywhere.
  • bootstrap.ts present for any other reason (a hand-rolled one — plausible filename): early return, and only this step stopped. Polyfills, angular.json, tsconfig.federation.json, package.json and federation.config.mjs were all still written, since makeMainAsync runs last in the chain([...]). init reported success on a workspace whose entry point never called initFederation. Trace was one console.info.

What goes into bootstrap.ts now follows from main.ts's own content. Generating the scaffold needs the root component's symbol, not just its file, so the app.component.ts/app.ts probe moved out of schematic.ts into resolveAppComponent, which now also reads the exported class name — Angular >=20 scaffolds App in app/app.ts, earlier versions AppComponent in app/app.component.ts. schematic.ts reuses it for exposes-seeding so the two can't drift.

A missing build entry point now throws naming the path and the angular.json key, instead of a bare PathDoesNotExistException out of tree.overwrite.

The step had no spec, which is why this survived. It has one now, fixtures verbatim from ng new on Angular 22.

a2c33eb feat: --webcomponent on init

Generates a custom-element bootstrap.ts instead of moving main.ts into it:

import { createApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
import { createCustomElement } from '@angular/elements';

(() => {
  createApplication(appConfig)
    .then(({ injector }) => {
      customElements.define(
        'mfe-test-app', // your componentname
        createCustomElement(App, { injector }),
      );
    })
    .catch((err) => console.error(err));
})();

Tag is mfe-<project> — custom element names need a hyphen, which a one-word project name lacks. The // your componentname marker stays: the tag is part of the remote's contract with its host and is meant to be edited.

@angular/elements is added at whatever range the workspace has for @angular/core — it ships in lockstep with the framework, so a floating range resolves a mismatched major. No @angular/core to match against is an error, not a guess. Note this lands in the root package.json, so in a monorepo one remote's flag makes it available workspace-wide; package.json has no per-project scoping.

The flag is only accepted for --type remote, and an existing bootstrap.ts is never clobbered.

d8c9cc6 fix: stop init from overwriting main.ts with nowhere to put it

Review fix. Deriving bootstrap.ts from main.ts's content made main.ts the file that gets rewritten, and three paths rewrote it over contents that were not recoverable afterwards.

  • A re-run regenerated the stub from scratch. Anything the user had changed about the initFederation call — shimMode: false for Shim mode corrupts every method named import (e.g. DevExtreme Diagram); allow opting into native import maps #70, an adjusted hostRemoteEntry/cacheTag, remotes added by hand — was reverted without a word. main.ts is now left alone once it already calls initFederation; only a missing bootstrap.ts is regenerated. A host bakes its remote map into main.ts, so a host re-run now says the map was not refreshed rather than leaving it to be discovered.
  • bootstrap.ts taken + main.ts not federated destroyed main.ts. Its contents were dropped with a console.warn as the only trace, and there is no second place to move them to. Now a SchematicsException naming both files, thrown before anything is written.
  • --webcomponent discarded main.ts silently. It replaces main.ts rather than moving it, which the feature cannot avoid, but a remote calling registerLocaleData or initialising Sentry ahead of bootstrapApplication lost it without a word. It warns now, matching the row above.

Resulting behaviour:

main.ts federated bootstrap.ts exists result
no no bootstrap.ts gets main.ts's content; main.ts becomes the stub
no yes throwsmain.ts's contents have nowhere to go
yes no bootstrap.ts regenerated from the root component; main.ts untouched
yes yes both untouched, silent

With --webcomponent the first row generates the element bootstrap instead of moving main.ts, and warns that main.ts was not kept.

Three smaller fixes from the same review:

  • resolveAppComponent returned null whenever it could not read a class name, even though it had found the file. schematic.ts only wants the path and fell back to the update-this.ts placeholder — a federation.config.mjs exposing a file that is not there, for an app.ts using a default export or a separate export { App }. className is nullable now and only resolveAppRefs, which needs the symbol, rejects.
  • @angular/elements and its NodePackageInstallTask were queued from the flag alone, before makeMainAsync had decided the flag was a no-op because bootstrap.ts already existed. addDependencies moves into the chain behind makeMainAsync and takes its outcome. --webcomponent against a host/dynamic-host is rejected up front: the generated bootstrap only registers an element and never bootstraps the shell.
  • The generated createApplication chain had no .catch, unlike the bootstrapApplication one, so a throwing provider was an unhandled rejection on a blank page.

722e88a fix: take FederationManifest from the orchestrator, not the build entry

Review fix. initFederation's parameter type came from @softarc/native-federation — the build-time entry that exports buildForFederation and the esbuild adapters — while the initFederation it forwards to lives in @softarc/native-federation-orchestrator.

The two descriptors differ: the build-time one carries an optional main that nothing in the orchestrator bundle reads, so initFederation({ mfe1: { url, main } }) type-checked and then ignored main at runtime. The emitted index.d.ts also kept the import, pulling the build entry's type graph into every consumer compilation.

Verification

227 tests, typecheck clean, eslint 0 errors. 2992edc was checked green standalone (208 tests) so the fix is cherry-pickable without the feature.

Beyond unit tests, the built dist was run as a real schematic against ng new on Angular 22 (@angular/core ^22.1.0), confirming: plain init moves main.ts; deleting bootstrap.ts and re-running regenerates a correct scaffold instead of the self-import; --webcomponent emits the element registration with mfe-test-app; @angular/elements resolves to ^22.1.0 matching core. Both generated variants type-check under Angular's strict tsconfig.app.json.

Note

The branch name is from 548fb0e and doesn't describe the schematic work; the two are unrelated beyond sharing a branch.

…xistence

init decided what to do with main.ts by checking whether bootstrap.ts was
there. The two can disagree, and both ways they disagree were broken.

When bootstrap.ts had gone missing from an already-initialised project --
renamed, deleted, or the build entry repointed between runs -- the guard fell
through and copied main.ts into it. main.ts was the federation stub by then,
so bootstrap.ts called initFederation a second time and imported itself, and
the original bootstrap code went with the overwrite.

When bootstrap.ts existed for any other reason the guard returned early, and
only this step stopped: polyfills, angular.json, tsconfig.federation.json,
package.json and federation.config.mjs were all still written. init reported
success on a workspace whose entry point never called initFederation.

main.ts is now always rewritten, and its own content decides what goes into
bootstrap.ts: the unfederated source when there is some to move, a regenerated
bootstrapApplication scaffold when main.ts is already the stub. An existing
bootstrap.ts is never overwritten -- it is kept, with a warning that main.ts's
previous contents were dropped.

Generating that scaffold needs the root component's symbol and not just its
file, so the app.component.ts/app.ts probe moves out of schematic.ts into
resolveAppComponent and now reads the exported class name: Angular >=20
scaffolds `App` in app/app.ts, earlier versions `AppComponent` in
app/app.component.ts.

A missing build entry point throws with the offending path and the angular.json
key it came from, rather than a bare PathDoesNotExistException out of
tree.overwrite.

The step had no spec, which is why this survived. It has one now, with fixtures
taken verbatim from `ng new` on Angular 22.
Remotes that are consumed as custom elements rather than as lazy Angular
routes need a different bootstrap: createApplication instead of
bootstrapApplication, and a customElements.define for the root component.
Until now that had to be written by hand after every init, and the tag name
and injector wiring are easy to get subtly wrong.

`--webcomponent` generates that bootstrap.ts instead of moving main.ts into
it. The element is registered as mfe-<project> -- custom element names must
contain a hyphen, which a one-word project name does not -- and the generated
line keeps the `// your componentname` marker, since the tag is part of the
remote's contract with its host and is meant to be edited.

@angular/elements is added at whatever range the workspace already has for
@angular/core. It ships in lockstep with the framework, so a floating range
would resolve a major that does not match, and a workspace without
@angular/core to match against is an error rather than a guess.

An existing bootstrap.ts is still never overwritten; the flag warns that it
was a no-op rather than discarding the file.
…ut it

Deriving bootstrap.ts from main.ts's content made main.ts the file that gets
rewritten, and three paths rewrote it over contents that were not recoverable
afterwards.

A re-run regenerated the stub from scratch, so anything the user had changed
about the initFederation call -- a shimMode: false for #70, an adjusted
hostRemoteEntry, remotes added by hand -- was reverted without a word. main.ts
is now left as it is once it already calls initFederation; only a missing
bootstrap.ts is regenerated. A host bakes its remote map into main.ts, so a
host re-run says that the map was not refreshed rather than leaving it to be
discovered.

When bootstrap.ts was already taken and main.ts was not federated, main.ts's
contents were dropped with a console.warn as the only trace. There is no
second place to move them to, so that is now a SchematicsException naming both
files, thrown before anything is written.

--webcomponent replaces main.ts instead of moving it, which the feature cannot
avoid, but it did so in silence: a remote calling registerLocaleData or
initialising Sentry ahead of bootstrapApplication lost it. It warns now, which
is what the bootstrap.ts-is-taken path already did.

resolveAppComponent returned null whenever it could not read a class name,
even though it had found the file. schematic.ts only wants the path, and fell
back to the update-this.ts placeholder -- a federation.config.mjs exposing a
file that is not there, for an app.ts using a default export or a separate
export statement. className is now nullable and only resolveAppRefs, which
needs the symbol, rejects.

@angular/elements and its install task were queued from the flag alone, before
makeMainAsync had decided the flag was a no-op because bootstrap.ts already
existed. addDependencies moves into the chain behind makeMainAsync and takes
the outcome, so the dependency follows the bootstrap that was actually
written. --webcomponent against a host or dynamic-host is rejected up front:
the generated bootstrap only registers an element and never bootstraps the
shell.

The generated createApplication chain also had no .catch, unlike the
bootstrapApplication one, so a throwing provider was an unhandled rejection on
a blank page.
initFederation's parameter type came from @softarc/native-federation -- the
build-time entry that exports buildForFederation and the esbuild adapters --
while the initFederation it forwards to lives in
@softarc/native-federation-orchestrator.

The two descriptors are not the same. The build-time one carries an optional
`main` that nothing in the orchestrator bundle reads, so
initFederation({ mfe1: { url, main } }) type-checked and then ignored `main`
at runtime. The emitted index.d.ts also kept the import, pulling the build
entry's type graph into every consumer compilation.
@Aukevanoost
Aukevanoost merged commit 31f1ea6 into main Sep 16, 2026
1 check failed
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