Migrate in this order or you will migrate twice
What was the situation
Once you admit the version bump is not the migration, someone still has to pick an order. On an Enate-style product that already had a shell and remotes, the tempting plan was "do it all in this release": standalone, new control flow, signals, zoneless, new builder.
That plan ships nothing. Every remote is half-migrated. The shell cannot federate a mix. You roll back the parts that hurt, and you have migrated twice.
Mimecast-era platforms look smooth because the order was already decided. The work is making an older federated app walk that same order, one step that can go to production.
When does this apply
Use this when more than one of these is still true: NgModules own the tree, templates use *ngIf, state is not signals, Zone.js is still the intern.
- A shell plus remotes, and you cannot freeze the product for a quarter.
- A shared library that both old and new remotes import.
- A team that can land one kind of change per remote, not four.
Skip this if you are starting a new product in the Nx repo. Start on the new shape. There is nothing to sequence.
How is it done
Ship this order. Do not skip ahead because a blog post made zoneless look easy.
- Standalone — routes and components first, then the leftover NgModules. Federation still works. The unit of the app becomes the component, which every later step assumes.
- Control flow —
@if/@forin templates. Mechanical. Easy to review. Does not change how data moves. - Signals — local UI state, then
input()/output(), theninject()instead of constructor wiring. The view starts telling the truth about what it reads. - Zoneless — last. Only when the view already updates because signals changed, not because Zone patched a timeout.
A remote can sit at step 2 while another is on step 3. The shell should be at least as far as the furthest remote it hosts, or you will debug "works in isolation, dead in the shell" for the wrong reason.
Keep a checklist per remote, not a platform-wide banner that says "we are on signals now". Enate taught me that the shell will lie for you. Mimecast-era Nx tags make the lie visible, but only if the remote actually finished the step.
shell ████ standalone ████ control flow ██░░░░ signals ░░░░░ zonelessremote-a ████ standalone ████ control flow ████ signals ░░░░░ zonelessremote-b ████ standalone ██░░░░ control flowThat picture is shippable. Four bars filled on every remote on the same Friday is not.
Watch out for
Do not migrate the shared UI library to signal inputs while a remote still binds with @Input and ngOnChanges copies. Migrate the library one release after the last consumer can take input().
Do not turn on zoneless in the shell to "get it over with". Every remote inherits that decision, including the one still mutating plain fields in a plugin callback.