Skip to content

chore(sample): update the sample app to Angular 21 - #3671

Open
markgoho wants to merge 19 commits into
angular:mainfrom
markgoho:mgoho/chore/update-angular
Open

chore(sample): update the sample app to Angular 21#3671
markgoho wants to merge 19 commits into
angular:mainfrom
markgoho:mgoho/chore/update-angular

Conversation

@markgoho

@markgoho markgoho commented Sep 10, 2025

Copy link
Copy Markdown
Contributor

Issue number for this PR: #3669

Carries sample/ from Angular 19 to a proper Angular 21 app.

The migration:

  • @angular/* 21, @angular/build builders, TypeScript 5.9
  • provideExperimentalZonelessChangeDetection -> provideZonelessChangeDetection, zone.js removed
  • provideServerRoutesConfig(serverRoutes) -> provideServerRendering(withRoutes(serverRoutes)) from @angular/ssr
  • BootstrapContext threading in main.server.ts
  • @angular/animations and @angular/platform-browser-dynamic dropped
  • angular.json carries the v21 ng update boilerplate (schematics naming defaults, CLI analytics off)

Matching what Angular 21 generates for new apps:

  • Tests run on vitest through the bare @angular/build:unit-test target, with no hand-rolled test entry file, the test providers in the spec, and @vitest/coverage-v8 keeping ng test --coverage working. The karma and jasmine packages are gone
  • Express 5 for the SSR server, including the scaffold's PM2 guard and error-first listen callback
  • The v21 solution-style tsconfig layout
  • firebase declared as a direct dependency, as ng add produces
  • angular.json trimmed to the scaffold's shape (no extract-i18n target or default-valued outputPath/index, security.allowedHosts added) and @types/node at the scaffold's ^20.17 range

Sample fixes along the way, each its own commit:

  • Event-handler Firebase calls run inside the injection context (runInInjectionContext, the docs/zones.md pattern), clearing the dev-mode warning
  • The Cloud Functions demo is runnable again: its folder's one-time install and build is documented in the README, and its runtime moved to Node 22
  • firebase-tools declared as a devDependency at ^15.0.0 (inside the library's supported range) and invoked from the local install in start
  • The unused cookie-store dependency removed, and the README retitled and pointed at npm start (the demos need the emulators), with the dead ng e2e section dropped
  • A new spec asserts the app and @angular/fire resolve to the same installed firebase copy, failing loudly if the two version ranges ever stop overlapping
  • Consumes the library as file:../angular-fire-21.0.0-rc.0.tgz, with tarball production documented in sample/README.md

Verified locally: root build + npm pack, sample install, ng build, ng test (4 specs, vitest), and a full emulator session exercising sign-in, a Firestore write, and the Cloud Function call with a clean console.


Original author's notes

This PR builds on @markgoho's Angular 19 to 20 migration, which did the heavy lifting before the Angular 21 carry and cleanup above. His original notes, preserved:

  • installing angular/fire v20 was fun, I had to initially uninstall it from the sample app, update to Angular 20, then build v20 from source, then add it back in
  • many of the changes are lint-related
  • Karma tests are passing with the inclusion of a test.ts file -- it kept complaining about needing Zone.js even though this is a zoneless app, since there's only 3 tests, we may want to explore Vitest as an alternative
  • sadly, I did need to pass --no-verify when committing this, there are ~350 lint errors in the repo and I didn't want to include those fixes in this change
  • both client and server apps run, no errors appear locally, and most functionality that I could test continues to work
  • many, many updates needed to this app to bring it in line with newer patterns in Angular that have evolved since Signals came out, but we can tackle those in future PRs

Comment thread sample/src/test.ts Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new file courtesy of grok-code-fast-1, we're doing something similar in other sample apps in the project, but if there's something here we don't need, let's call that out

Comment thread sample/src/server.ts

if (isMainModule(import.meta.url)) {
// eslint-disable-next-line @typescript-eslint/dot-notation
const port = process.env['PORT'] || 4000;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both process.env.PORT (trying to access a property on an index type) and process.env['PORT'] (please use dot notation) have some sort of error associated with it, I went with this option

we may want to take a look at lint rules in the future

Comment thread sample/src/app/app.component.ts Outdated
Comment on lines +28 to +35
@defer (hydrate on idle) { <app-database /> } @placeholder { Database!
&hellip; } @defer (hydrate on idle) { <app-firestore /> } @placeholder {
Firestore! &hellip; } @defer (hydrate on idle) { <app-functions /> }
@placeholder { Functions! &hellip; } @defer (hydrate on idle) {
<app-messaging /> } @placeholder { Messaging! &hellip; } @defer (hydrate on
idle) { <app-remote-config /> } @placeholder { Remote Config! &hellip; }
@defer (hydrate never) { <app-storage /> } @placeholder { Storage! &hellip;
} @defer (hydrate on idle) { <app-upboats /> } @placeholder { &hellip; }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend we start using prettier for formatting (something to think about)

Comment thread sample/package.json Outdated
"scripts": {
"ng": "ng",
"start": "firebase emulators:exec --import seed \"ng serve\"",
"start": "npx --yes firebase-tools@latest emulators:exec --import seed \"ng serve\"",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe someone had firebase-tools installed globally, but I don't and I'd prefer to follow the pattern I see in the root package.json of installing this on demand

alternatively, we could install the package in the devDependencies and keep it up to date

@markgoho

Copy link
Copy Markdown
Contributor Author

@jamesdaniels any feedback for this PR?

@markgoho

markgoho commented Nov 1, 2025

Copy link
Copy Markdown
Contributor Author

@jamesdaniels should we try to get this in before v21 comes along? 🤓

Continues the Angular 19 to 20 update in this PR up to Angular 21, so the
sample matches the workspace library (which is on Angular 21).

- bump all @angular/* to ^21.0.0 and @angular/fire to the Angular 21 build;
  drop the unused, deprecated @angular/animations and the deprecated
  @angular/platform-browser-dynamic; bump @types/node to ^22 for the Node 22
  runtime.
- main.server.ts: pass the BootstrapContext that Angular 21 requires for the
  server bootstrap. Without it, build-time route extraction fails with NG0401.
- test.ts: reconcile the test setup with the app's zoneless mode. Drop the
  zone.js imports, switch to @angular/platform-browser/testing, and provide
  zoneless change detection so fixture.detectChanges works.
- rename the project ng20-test to ng21-test throughout: package.json name and
  serve:ssr script, angular.json project key/outputPath/buildTargets, the
  index.html and README titles, and AppComponent.title with its spec.

Builds and unit tests pass against the Angular 21 library.
@armando-navarro

Copy link
Copy Markdown
Collaborator

Thanks for this, @markgoho, and for opening #3669 to track it. I picked this up to get it into the Angular 21 release, and I pushed a few commits to your branch to carry it the rest of the way.

Rather than land it at Angular 20, I took it straight to Angular 21 so the sample matches the library (the workspace is on 21 now). The sample pins @angular/fire to a packed build of the library, so a v20 sample would point at a build we no longer produce; going to 21 keeps it runnable and let me verify it end to end.

On top of your 19 to 20 work, the 20 to 21 commit:

  • passes the BootstrapContext that Angular 21 now requires in main.server.ts (without it the build fails route extraction with NG0401),
  • reconciles test.ts with the app's zoneless mode (drops the zone.js imports, switches to @angular/platform-browser/testing, provides zoneless change detection),
  • bumps the Angular packages to 21 and drops the now-deprecated @angular/animations and @angular/platform-browser-dynamic,
  • renames the project to ng21-test and updates the titles and README.

ng build and the unit tests pass against the Angular 21 library. I left the environment-file cleanup you mentioned in #3669 out of scope for this pass. If you spot anything off in the delta, I'd genuinely welcome the review before I merge it as part of the release.

@tyler-reitz tyler-reitz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Angular migration itself is correct and I have no objections to any of it: provideExperimentalZonelessChangeDetectionprovideZonelessChangeDetection, provideServerRoutesConfigprovideServerRendering(withRoutes(...)) from @angular/ssr, the BootstrapContext threading in main.server.ts, @angular-devkit/build-angular:*@angular/build:*, and dropping zone.js / platform-browser-dynamic / animations.

Main ask: strip the formatting churn

The sample isn't covered by the repo's lint — angular.json lints src/**/*.ts and src/**/*.html only — so none of the reformatting is required by project tooling. It looks like an editor's Prettier config, and it buries the real changes.

It also actively hurts readability in the templates. app.component.ts turns seven aligned one-line @defer blocks into eight lines of wrapped prose:

    @defer (hydrate on idle) { <app-database /> } @placeholder { Database!
    &hellip; } @defer (hydrate on idle) { <app-firestore /> } @placeholder {

and auth.component.ts collapses } @if (...) { onto shared lines.

If reformatting the sample is wanted, it'd be better as its own PR with a checked-in config so it doesn't drift again. Relatedly, the // eslint-disable-next-line @typescript-eslint/dot-notation added to server.ts is dead weight — nothing lints that file.

Metadata is stale

Title and description say Angular 20, but the head commit is chore(sample): carry the sample app to Angular 21 and sample/package.json now pins ^21.0.0. The author's notes (Angular 20, ~350 lint errors, --no-verify) describe an earlier state of the branch. Worth rewriting the title/body and re-verifying the checklist against what's actually here now.

Smaller points

  • "start": "npx --yes firebase-tools@latest emulators:exec ..." fetches an unpinned firebase-tools on every run, requires network, and can drift from the version the library peers (^14.0.0 || ^15.0.0). Prefer the workspace binary or a pinned version.
  • "@angular/fire": "file:../angular-fire-21.0.0-rc.0.tgz" now encodes an RC in the filename, so it needs a bump every release, and nothing in sample/README.md says how to produce that tarball. Same pattern as before (19.0.0.tgz) so not a regression — but since it's being touched anyway, pointing at dist/packages-dist/ would stop the churn.
  • src/test.ts plus "main": "src/test.ts": hand-rolling initTestEnvironment is the pre-v20 shape. @angular/build:karma has a providersFile option meant for exactly this zoneless case — worth checking whether it works here, since it would also remove the Firebase providers now duplicated between test.ts and app.component.spec.ts. And on the Vitest note: Angular 21's default for new projects is @angular/build:unit-test, so that instinct is right, just not this PR's job.
  • app.component.spec.ts changing querySelector('h1')?.textContentcompiled.textContent is a real fix (the template has no h1, so the old assertion was failing), but asserting 'Hello World!' against the whole rendered tree is a weak check. Fine as-is — flagging so it's a deliberate choice rather than a side effect.

@armando-navarro armando-navarro added comp: other Doesn't clearly map to a module or cross-cutting area. type: chore Maintenance with no user-facing behavior change. labels Aug 3, 2026
No behavior change. The Angular 20 migration commits carried an
editor's Prettier reformatting (quotes, wrapping, import order) across
every touched file, which buried the real changes. This restores
main's formatting everywhere, reverts interface Animal back to the
type alias, and drops a dead eslint-disable comment in server.ts
(nothing lints the sample). Build and the 3 specs verified green at
this commit.
… the tarball

Delete the hand-rolled src/test.ts. When the test target in
angular.json does not name an entry file, the @angular/build:karma
builder generates the test-environment initialization itself. The
Firebase test providers already lived in the spec, so the diff moves
only the zoneless change-detection provider there.

firebase-tools moves into devDependencies so npm start stops
downloading the newest release on every run and cannot drift ahead of
the firebase-tools versions the library supports. The start script
runs the firebase command that install provides.

The README now documents how to produce the tarball the sample
installs the library from.
Angular 21 generates new applications with the @angular/build:unit-test
builder running vitest, so the sample now matches what the CLI produces
for its target version. The bare builder entry picks up the same
defaults a new app gets.

The specs run unchanged: describe, it, and expect keep working as
vitest globals, so only the type declarations move from jasmine to
vitest/globals.

The karma and jasmine packages leave devDependencies, replaced by
vitest, jsdom (the browserless DOM the tests now run in), and
@vitest/coverage-v8 so ng test --coverage keeps producing reports.
The sample imports from firebase/auth directly but never declared the
package, relying on the copy @angular/fire bundles being hoisted where
the import can find it. An application that sets up @angular/fire
through ng add carries its own firebase entry, so the sample now
matches, and npm dedupes the two ranges to a single installed copy.
Angular 21 scaffolds its SSR server on express ^5.1.0, so the sample
now matches, with @types/express moving to the matching major.

Express 5 changed route-pattern parsing, so the catch-all handler is
registered without the '/**' path string, the same shape the v21
scaffold uses. The two-step dirname/fileURLToPath computation becomes
import.meta.dirname, available on every Node version Angular 21
supports.

The listener block also adopts the scaffold's shape: the server now
starts under the PM2 process manager too, and the listen callback
throws startup errors instead of ignoring them.
The root tsconfig becomes the solution-style coordinator the v21 CLI
generates: it compiles nothing itself and references the app and spec
configs as separate projects, which gives editors the right typing
per context.

The module setting moves to preserve, which implies the bundler
resolution and import interop the older explicit flags provided, and
the unused outDir goes away. tsconfig.app.json switches from listing
entry files by hand to including all sources except specs.
…context

Calls like signInAnonymously and updateDoc were made directly inside
click handlers, which run after the component's injection context is
gone, so AngularFire logged its outside-injection-context warning and
could not apply its change-detection and SSR safeguards there.

The two affected components, auth and upboats, now capture an
EnvironmentInjector and re-enter it with runInInjectionContext inside
each handler, the exact pattern docs/zones.md teaches. The functions
demo needs no wrap, because its callable is produced once during field
initialization, inside the injection context, and the returned function
never touches the injector. The raw Firebase SDK call, signInWithPopup,
is outside the warning system's scope and stays as is.
The functions folder is its own small project, and without its own
install and build the functions emulator skips it with a warning, so
the Call! demo had no backend. The README now documents the one-time
setup.

Its engines entry moves from Node 20 to 22, matching the runtime the
deploy schematic generates, and its TypeScript compile target moves to
es2022 alongside.
The heading was the PascalCased project name the CLI scaffold derived
from the folder, never a chosen title.
Nothing imports it: not the sample source and not the shipped
@angular/fire code. It also sat in the build's externalDependencies
list, which only matters for packages that actually appear in imports.
It looks left over from an earlier approach to the session cookie
handling that the current server-side code no longer uses.
angular.json drops the extract-i18n target, outputPath, and index,
none of which ng new generates anymore (the removed values equal the
builder's defaults), and gains the security.allowedHosts entry new
apps carry.

@types/node moves from ^22 to ^20.17.19, the range new apps declare.
The types package should describe the OLDEST Node the app supports,
which is Angular 21's 20.19 requirement, so the compiler rejects APIs
that would crash there. Every API the server uses is typed well within
that range.
The development-server section told readers to run bare ng serve, but
every demo talks to the local emulators, so that yields a page with no
backend. It now leads with npm start, which boots the seeded emulators
and then serves.

The scaffold's end-to-end section instructed ng e2e, which errors
because no e2e target is configured, so it goes until a real e2e setup
exists.
The sample app declares firebase and @angular/fire bundles its own
firebase dependency. npm installs a single shared copy only while the
two version ranges overlap. If they ever stop overlapping, npm installs
two copies side by side and objects from one copy are rejected by the
other at runtime. This assertion fails loudly the moment that happens.
@armando-navarro armando-navarro changed the title chore(workspace): update sample app to Angular 20 chore(sample): update the sample app to Angular 21 Aug 15, 2026
@armando-navarro

armando-navarro commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

I pushed a stack of small commits addressing everything, starting with one that reverts the reformatting, so every real change can be reviewed on its own.

Formatting

I restored every file whose changes were formatting-only to the version on main, so 12 files that were previously modified now have no diff at all, and rebuilt the rest to carry only the real changes.

providersFile / test.ts

I went straight to your underlying instinct here, which markgoho's original notes also raised: Angular 21 makes vitest the default for new apps, so the sample now runs its tests through the bare @angular/build:unit-test target, with no hand-rolled src/test.ts (the builder generates the test-environment initialization itself), the test providers living in the spec, and @vitest/coverage-v8 keeping ng test --coverage working. The karma and jasmine packages are gone and the specs run unchanged.

firebase-tools

I went with your devDependency option: firebase-tools declared at ^15.0.0 (inside the library's ^14.0.0 || ^15.0.0 peer range), and start runs the locally installed binary.

The tarball path

I kept the tarball but added a README section documenting how to produce it from the root build, including the note that the filename tracks the root package version.

The spec assertion

Agreed the check is loose, and here is why it stays that way for now. The old test looked for an <h1> element, but the template never had one, which is why that assertion was failing. The "Hello World!" text sits directly in the template with no element around it, so the closest the test can get is searching the whole rendered output for the text, which is what it does now. Making the check stricter would mean changing the demo's markup to give the text a real element, and I kept that out of this PR.

Beyond the review

While in here I finished carrying the sample to what Angular 21 actually generates, each as its own commit:

  • Express 5 for the SSR server, the version the v21 scaffold ships
  • The v21 solution-style tsconfig layout
  • firebase declared as a direct dependency, matching what ng add produces (the sample imports firebase/auth directly but never declared it)
  • Event-handler Firebase calls now run inside the injection context via runInInjectionContext, the pattern docs/zones.md teaches, which also clears the dev-mode warning the sample used to print
  • The Cloud Functions demo works again: its folder needed its own install and build, now documented in the README, and its runtime moved to Node 22
  • The remaining configuration aligned with the scaffold: the leftover extract-i18n target and default-valued outputPath/index dropped, security.allowedHosts added, and @types/node at the ^20.17 range new apps declare
  • The README now leads with npm start (the emulator-backed way the demos actually work) and its dead ng e2e section is gone
  • A new spec asserts the sample app and @angular/fire resolve to the same installed firebase copy, so if the two version ranges ever stop overlapping, the suite fails loudly instead of the runtime rejecting objects across copies
  • Removed the unused cookie-store dependency and gave the README a real title

Everything was verified against the emulators end to end: sign-in, a Firestore write, and the Cloud Function call, with a clean console.

If any of this lands differently for you, happy to adjust before it merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: other Doesn't clearly map to a module or cross-cutting area. type: chore Maintenance with no user-facing behavior change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants