Skip to content

build: classify the firebase symbols a version raise would newly wrap - #3759

Merged
armando-navarro merged 3 commits into
angular:mainfrom
armando-navarro:a38-pr-a-override-entries
Sep 1, 2026
Merged

build: classify the firebase symbols a version raise would newly wrap#3759
armando-navarro merged 3 commits into
angular:mainfrom
armando-navarro:a38-pr-a-override-entries

Conversation

@armando-navarro

Copy link
Copy Markdown
Collaborator

Checklist

Description

@angular/fire does not hand-write its wrapper files. tools/build.ts regenerates src/<module>/firebase.ts from the type declarations of whatever firebase is installed, wrapping every exported function whose name starts with a lowercase letter. Raising the installed version therefore wraps every function Firebase has added in between, all at once, whether or not wrapping does anything useful for a given one. #3756 has the background.

One of them does not merely gain nothing, it breaks. The auth persistence exports are classes, and Firebase constructs whichever one it is given with new. Wrapping replaces the class with an ordinary function, and calling it without new throws. #3550 is a user hitting exactly that on browserLocalPersistence, which is why that one and its three neighbors already carry an entry. browserCookiePersistence is the fifth, and it becomes visible when the version rises.

This PR settles the classification while the installed version is still 12.4.0, so the version raise stays a small change rather than a behavior change bundled with it. Nothing here alters generated output, because the generator builds its export list from the installed declarations, so an entry naming a symbol that version does not have is never consulted.

What each entry decides:

  • maximum, minimum, getTemplateGenerativeModel and makeMemoryCacheProvider stay unwrapped. Each is synchronous, takes no callback and returns a plain value, so wrapping only adds an out-of-injection-context warning to a call that cannot destabilize anything.
  • browserCookiePersistence stays unwrapped, for the new reason above.
  • onRegistered and onUnregistered are wrapped with blockUntilFirst off, matching onMessage. With it on, subscribing registers a pending task that clears only when the callback fires or the caller unsubscribes, and these fire on a Cloud Messaging registration change that may never happen.
  • unregister is quieted to match deleteToken. register keeps the default, matching its counterpart getToken.

Two supporting changes:

  • tools/build.ts no longer runs itself on import, and exports the generator, so npm run generate can regenerate the files without ng build. That is what makes a regenerate-and-compare check runnable in CI.
  • The generator now fails on an override key its entry point does not declare. Overrides are matched by name, so a misspelled or misfiled key is otherwise ignored in silence and the symbol quietly keeps the default. That had already happened once, and is fixed here.

Two smaller items ride along:

  • ngPostUpdate, an ng update migration that took a Tree and returned it unchanged, is removed. It was registered in four places, one of which spelled it ng-post-upgate.
  • docs/zones.md gains a section on what AngularFire asks Angular for when you call a wrapped API, and what is lost when that call happens outside an injection context. Firebase API called outside injection context - missing details on how to fix this #3629 asked for that detail.

Testing

No unit tests, because nothing here changes runtime behavior: the generated files are byte-identical before and after. The check that matters is that regenerating produces no diff, which is what shows the entries are inert against firebase 12.4.0.

npm run generate && git status --porcelain -- src/

That is empty on this branch. Two further results:

  • Adding an entry that does change output produces the expected diff, so the check is capable of failing.
  • Regenerating against firebase 12.18.0 in a scratch checkout changes only src/messaging/firebase.ts, and every entry lands as described above.

If any of this reads wrong to you, particularly the four I am leaving unwrapped, say so and I will revisit before the version raise goes up.

Refs #3756

…ything

ngPostUpdate took a Tree and returned it unchanged. It was registered in
migration.json, listed in the schematics tsconfig, loaded as a build entry point
and bundled on every build, and running it was always a no-op. Its description
claimed it printed results after ng-update, and it printed nothing.

Leaving it in place costs more than the eight lines suggest. Anyone auditing what
ng update does has to open the file to find out that the answer is nothing, and a
dead entry point is one more thing the build has to keep loading.

The entry was spelled ng-post-upgate, missing the d, so a search for the correct
spelling did not find it. tools/build.ts refers to it as ['update', 'index'],
assembled from parts, so a search for the path did not find that either.

Removing it changes no behavior. Nothing referenced ngPostUpdate except the four
places removed here, and the file exported nothing else.
… the generator

The build regenerates src/<module>/firebase.ts from the declarations of whatever
firebase is installed, wrapping every lowercase-initial function. Raising the
minimum version would wrap symbols nobody has classified, and one breaks: the auth
persistence exports are classes that firebase constructs with `new`, and the
wrapper replaces them with an ordinary function. These entries settle that first,
and stay inert until the version rises.

Do not wrap maximum, minimum, getTemplateGenerativeModel or makeMemoryCacheProvider:
each is synchronous and returns a plain value, so wrapping only adds a warning to a
call that cannot destabilize anything.

Wrap the messaging subscriptions with blockUntilFirst off. With it on, subscribing
holds a pending task until the callback fires, and these fire on a registration
change that may never happen.

The generator is now exported so `npm run generate` runs it without `ng build`, and
it fails on an override key its entry point does not declare, which is otherwise
ignored in silence.
The guide said to call Firebase APIs inside an injection context and showed how,
but never said what AngularFire does with one, so the warning read as a lint rule
rather than a description of something lost.

The new section says what AngularFire asks Angular for, what happens without it,
and names the consequence: the call is never added to the PendingTasks register,
which is what server-side rendering waits on before it serializes the page.
@armando-navarro armando-navarro added bump: patch comp: build/pipeline Build, bundling, packaging, release pipeline. comp: docs Documentation. comp: schematics ng add / deploy schematics (src/schematics). comp: zones Change detection / zone.js / zoneless. type: chore Maintenance with no user-facing behavior change. labels Sep 1, 2026

@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.

Approving. Ran the checks rather than reading them: npm run generate is clean at firebase 12.4.0, and the new guard genuinely fails. Re-adding the misfiled isSupported key exits 1 with your message, a typo'd indexedDBLocalPersistance does too, and flipping debugErrorMap to a real override produces the expected src/auth/firebase.ts diff. The docs/zones.md section matches zones.ts:134-138 exactly, and all five persistence exports are typeof function at runtime, with browserCookiePersistence marked "Excluded from this release type" in auth-public.d.ts:740. The four you left unwrapped read right to me.

One question, not blocking. The misfiled isSupported is deleted rather than moved to the firebase/remote-config block, so remote-config keeps ɵzoneWrap(_isSupported, true) (src/remote-config/firebase.ts:31) while messaging has false. Reading zones.ts, that costs a PendingTask around the promise and flips the default log level from VERBOSE to WARN (zones.ts:125), so out-of-context calls warn per call. I did not run that difference. Deliberate, or should the entry move?

Worth noting the guard is stronger than the PR says: buildLibrary awaits it (tools/build.ts:449) and CI runs npm run build, so it already fires on every PR.

@armando-navarro

Copy link
Copy Markdown
Collaborator Author

Deliberate, and I agree it should move.

Moving it changes src/remote-config/firebase.ts, and this PR's whole claim is that regenerating changes nothing, so the move would cost the one check that shows these entries are inert. It is queued as a follow-up that moves the entry and regenerates, which lands as the two-line diff you would expect.

One correction on the cost, since it changes what that follow-up needs to measure. The two effects cannot both reach the same call. warnOutsideInjectionContext has a single call site, zones.ts:138, inside the catch, and that catch returns at :139 before any of the pending-task code runs.

So a call made outside an injection context warns and returns immediately, and registers no pending task whether blockUntilFirst is true or false. The only difference there is whether the per-call line prints. A call made inside one gets the pending-task difference you describe, and never prints a warning at all, because the only warning lives in the catch.

I read that rather than ran it, same as you, so the follow-up should measure it before changing the value. The intent looks clear enough though: both isSupported functions are declared (): Promise<boolean> with no arguments, and analytics and messaging both set false, so whoever wrote the entry wanted false for remote-config and put it one block too high.

Thanks for the note on the check, and that is a better description than the one in my PR body. buildLibrary awaits it and CI runs npm run build, so the unrecognized-key check already fails on every PR. What is still missing is the separate check that the committed generated files match what regenerating produces, which is what I should have said.

@armando-navarro
armando-navarro merged commit e578ccb into angular:main Sep 1, 2026
24 checks passed
@armando-navarro
armando-navarro deleted the a38-pr-a-override-entries branch September 1, 2026 18:40
armando-navarro added a commit to armando-navarro/angularfire that referenced this pull request Sep 2, 2026
The generated src/<module>/firebase.ts files are written from the type
declarations of whatever firebase is installed, so the set of functions
AngularFire wraps is decided by the version the build resolves. That was
12.4.0 while npm latest reached 12.18.0, leaving newly added functions
reaching callers through the star export with no zone integration and no
pending-task registration.

Raising the required version wraps them. Everything the override entries
in angular#3759 classified as unwrapped stays out.

getImagenModel loses its entry because firebase removed the symbol in
12.18.0, so the generator no longer sees it. The exemption list it sat in
is empty now and stays as the place the next unclassified name goes.

Fixes angular#3756
armando-navarro added a commit that referenced this pull request Sep 2, 2026
The generated src/<module>/firebase.ts files are written from the type
declarations of the installed firebase, which decides which functions
AngularFire wraps. That was 12.4.0 while npm latest reached 12.18.0, leaving
everything firebase added in between to reach callers through `export *`, with
no zone integration and no pending-task registration.

Raising the required version wraps them. Everything the override entries in
#3759 classified as unwrapped stays out. `getImagenModel` loses its entry
because firebase removed the symbol in 12.18.0, and the exemption list it sat
in is empty now and stays as the place the next unclassified name goes.

Only `src/messaging/firebase.ts` changes among the generated files, gaining
onRegistered, onUnregistered, register and unregister. `docs/messaging.md` was
updated to use those calls. The Node send example keeps its token field, which
Firebase says still accepts an installation ID during the migration, rather
than the fid field it recommends, because firebase-admin 13.5.0 does not
declare one.

`exportsSeenPerOverrides` is keyed by the overrides object, and firestore and
firestore/lite are handed the same firestoreOverrides, so a name listed for one
silences the check for the other.

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

Labels

bump: patch comp: build/pipeline Build, bundling, packaging, release pipeline. comp: docs Documentation. comp: schematics ng add / deploy schematics (src/schematics). comp: zones Change detection / zone.js / zoneless. type: chore Maintenance with no user-facing behavior change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants