Skip to content

fix(functions): warn when a functions lockfile omits peer dependencies - #10918

Draft
IzaakGough wants to merge 7 commits into
mainfrom
@invertase/fix-issue-5673
Draft

fix(functions): warn when a functions lockfile omits peer dependencies#10918
IzaakGough wants to merge 7 commits into
mainfrom
@invertase/fix-issue-5673

Conversation

@IzaakGough

@IzaakGough IzaakGough commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #5673.

The problem

The build server runs npm ci with npm's default legacy-peer-deps=false. A lockfile resolved with that setting enabled omits the peer dependencies npm would otherwise install, so the build fails with Missing: <package> from lock file even though the local install succeeded.

A stock firebase init functions project is enough to hit it: jest is a peer dependency of the scaffolded firebase-functions-test, and npm ci validates the whole tree before dropping devDependencies, so a dev-only package fails a production deploy.

Until now the only signal was a multi-minute Cloud Build round trip returning several hundred lines of raw npm output. The workaround that spread through the issue was deleting package-lock.json, which only works because the builder then falls back to npm install.

The change

Warn before upload. warnIfLockfileOmitsPeerDeps reads the lockfile and reports non-optional peer dependencies it does not contain, resolving each peer the way npm does. Reading the artifact rather than npm's config matters: npm install --legacy-peer-deps produces exactly this lockfile while leaving nothing behind in config, so a config-based check is blind to it and simultaneously warns people whose lockfile is fine. Runs on the deploy path only, spawns nothing, and is wrapped so it can never fail a deploy.

The warning is suppressed when the functions directory ships an .npmrc enabling legacy-peer-deps, since that file is uploaded and the build server honors it, unless functions.ignore excludes it.

Explain the failure when it happens anyway. printLockfileErrors matches npm's out-of-sync error and leads with the far more common cause, a lockfile that is simply out of date, mentioning legacy-peer-deps second.

Verification

Reproduced against real deploys. A lockfile from npm install --legacy-peer-deps fails the build with the issue's exact error; adding only functions/.npmrc with legacy-peer-deps=true, lockfile byte-identical, makes the same deploy succeed.

The warning was exercised end to end against the built CLI: a lockfile missing jest warns while npm config get legacy-peer-deps reads false; a clean lockfile stays silent; a shipping .npmrc suppresses it; adding .npmrc to functions.ignore restores it.

No false positives against firebase-tools' own 1859-entry npm-shrinkwrap.json or a clean install of the same package.json.

Scope

Does not cover the other cause in that issue thread, where the lockfile was written by a different npm major than the build server uses (string-width-cjs and friends). The improved build failure message applies there; the pre-upload check does not.

…ild server

The Cloud Functions builder runs `npm ci` with npm's default
legacy-peer-deps=false. A package-lock.json resolved on a machine with the
setting enabled omits the peer dependencies npm would otherwise install, so the
build fails with "Missing: <package> from lock file" even though the local
install succeeded. In a stock `firebase init functions` project the trigger is
jest, a peer dependency of the scaffolded firebase-functions-test devDependency,
which is why untouched new projects hit it.

Users had no way to see this before a multi-minute Cloud Build round trip, and
the failure surfaced as several hundred lines of raw npm output with no
indication of the cause. The workaround that spread instead was deleting
package-lock.json, which works only because the builder then falls back to
`npm install`.

Two changes:

- Validate before upload. If the source dir has a package-lock.json, npm reports
  legacy-peer-deps=true, and no .npmrc is being shipped to carry the setting
  along, warn and name both fixes. Skipped entirely when there is no lockfile,
  and never fails a deploy: if npm cannot be run the check stays quiet.
- Map the build failure. When a deployment error carries the `npm ci` lockfile
  text, print the explanation and the fix instead of leaving the user with the
  raw dump.

Verified against real deploys: an identical lockfile fails the build without
functions/.npmrc and succeeds with it.

Fixes #5673
@wiz-9635d3485b

wiz-9635d3485b Bot commented Aug 11, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 3 Medium 1 Low
Software Management Finding Software Management Findings -
Total 3 Medium 1 Low

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request adds warnings and clearer error messages when Cloud Functions deployments fail due to npm lockfile mismatches, specifically when legacy-peer-deps is enabled locally but not on the build server. It introduces local validation to warn users before deployment and enhances error reporting to print actionable troubleshooting steps if npm ci fails. The review feedback highlights several violations of the repository's style guide regarding the use of any as an escape hatch in reporter.ts, validate.ts, and the test suite, suggesting type-safe alternatives such as custom interfaces, intersection types, and proper error narrowing.

Comment on lines +158 to +169
/** Collects every message in an error's cause chain so we can pattern match on them. */
function errorMessages(err: unknown): string {
const original = (err as DeploymentError)?.original as any;
return [
(err as Error)?.message,
original?.message,
original?.original?.message,
original?.context?.body?.error?.message,
]
.filter(Boolean)
.join(" ");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using as any violates the repository style guide rule against using any as an escape hatch. Since err is guaranteed to be a DeploymentError when called from printLockfileErrors, we can type the parameter more precisely and define a clean interface for the nested error structure to achieve full type safety.

interface DetailedError {
  message?: string;
  original?: DetailedError;
  context?: {
    body?: {
      error?: {
        message?: string;
      };
    };
  };
}

/** Collects every message in an error's cause chain so we can pattern match on them. */
function errorMessages(err: DeploymentError): string {
  const original = err.original as DetailedError | undefined;
  return [
    err.message,
    original?.message,
    original?.original?.message,
    original?.context?.body?.error?.message,
  ]
    .filter(Boolean)
    .join(" ");
}
References
  1. Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment on lines +73 to +78
try {
return /^\s*legacy-peer-deps\s*=/m.test(fs.readFileSync(npmrc, "utf8"));
} catch (e: any) {
logger.debug("Failed to read .npmrc in functions source directory:", e.message);
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Avoid using any in the catch clause. Typing the caught error as unknown and narrowing it (or using safe checks) is more type-safe and adheres to the repository style guide.

  try {
    return /^\s*legacy-peer-deps\s*=/m.test(fs.readFileSync(npmrc, "utf8"));
  } catch (e: unknown) {
    const message = e instanceof Error ? e.message : String(e);
    logger.debug("Failed to read .npmrc in functions source directory:", message);
    return false;
  }
References
  1. Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment on lines +370 to +376
it("finds lockfile errors nested in the original error", () => {
const rawError = new Error("Deployment failed") as any;
rawError.original = {
message:
"npm ERR! `npm ci` can only install packages when your package.json and " +
"package-lock.json are in sync. Missing: p-limit@2.3.0 from lock file",
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Avoid using as any to attach custom properties to an Error object. Using an intersection type is a cleaner and type-safe alternative that adheres to the repository style guide.

Suggested change
it("finds lockfile errors nested in the original error", () => {
const rawError = new Error("Deployment failed") as any;
rawError.original = {
message:
"npm ERR! `npm ci` can only install packages when your package.json and " +
"package-lock.json are in sync. Missing: p-limit@2.3.0 from lock file",
};
it("finds lockfile errors nested in the original error", () => {
const rawError = new Error("Deployment failed") as Error & { original?: unknown };
rawError.original = {
message:
"npm ERR! `npm ci` can only install packages when your package.json and " +
"package-lock.json are in sync. Missing: p-limit@2.3.0 from lock file",
};
References
  1. Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

…orter

Replaces the any casts flagged in review with a NestedError interface and
narrows errorMessages to DeploymentError, which is what printLockfileErrors
already guarantees before calling it.
…the functions dir

Two defects found in review:

Running the CLI under `npm run` or `npx` leaks the outer project's resolved
npm config into our environment, and npm_config_legacy_peer_deps and
npm_config_local_prefix beat cwd in the child. In a monorepo whose root .npmrc
sets the flag, that warned on a functions lockfile that was resolved correctly.
Strip npm_config_* from the child env so it resolves from sourceDir alone.

The warning also sat in the Node delegate's validate(), which the emulator runs
from discoverTriggers on every debounced file change. It describes how the build
server will treat an uploaded lockfile, so it belongs on the deploy path in
prepare, not on a path where nothing is uploaded.
Reading npm config missed the common case: `npm install --legacy-peer-deps`
writes the same broken lockfile but leaves no trace in config, so the warning
stayed silent for the users it was for, while firing on users whose lockfile
was fine. Inspect the lockfile for peer dependencies it does not contain
instead, which catches it however the setting was applied and drops the npm
subprocess.

Also honors functions.ignore before assuming a shipped .npmrc reaches the
build, reads npm-shrinkwrap.json, parses the .npmrc value rather than matching
the key, and leads the build failure message with the far more common cause,
a lockfile that is simply out of date.
@IzaakGough IzaakGough changed the title fix(functions): warn when a lockfile needs legacy-peer-deps on the build server fix(functions): warn when a functions lockfile omits peer dependencies Aug 12, 2026
…upload

Second review round. `.npmrc` quotes its values and accepts a bare key, both of
which read as enabled to npm but not to us, so a user who followed the warning's
own advice kept getting warned. Ignore globs were matched with different options
than the packaging code uses, so the two could disagree about whether the file
ships.

The check also sat in loadCodebases, which `functions:lifecycle:list` and the
discovery command share, so it now runs in the upload phase where it belongs and
keys off the endpoints' runtime rather than the build's optional one.

Adds the untested shapes: workspaces, scoped names, ignore globs, truncation,
and a build failure payload captured from a real deploy.
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.

Firebase function deployment fails with 'missing' dependencies error

2 participants