Skip to content

alasql's optional react-native-fs dep pulls ~140M of react-native/hermes/metro into installs and published Docker images #1937

Open
2 of 4 issues completed

Description

@heskew

Summary

alasql (4.17.x) declares optionalDependencies: { "react-native-fs": "^2.20.0" }. react-native-fs itself is 828K and unused in Node (alasql only requires it behind a guard for RN environments), but it has a peer dependency on react-native — and npm 7+ auto-installs peer deps. So every plain npm install harper pulls in react-native@0.82, react@19, hermes, metro, and react-devtools.

Confirmed in published Docker images

harperfast/harper:latest (5.1.23) ships:

package size
hermes-compiler 47M
react-native 37M
@react-native/* 25M
react-devtools-core 17M
metro* ~9M
react-native-fs, react, hermes-parser, misc ~4M

~140M of the 373M node_modules (~38%) is the react-native tree. The Dockerfile installs the packed tarball with plain npm install --ignore-scripts --global, which includes optional deps and auto-installed peers.

Not Docker-specific: every npm install of harper 5.x gets the same ~140M. (The legacy harperdb/harperdb 4.x images are unaffected only because they ship alasql 4.6.6, which predates the optional dep.)

Beyond size, this puts metro/hermes/react-devtools in a production database image — pure CVE-scanner noise and supply-chain surface for code that can never execute.

Fix

Do not use --omit=optional in the Dockerfile — harper's own tree relies on optionalDependencies for the @harperfast/rocksdb-js-* platform prebuilds, cbor/msgpackr extractors, bufferutil, utf-8-validate, etc.

Recommended: add an npm override in package.json, next to the existing alasql → xlsx override, stubbing react-native-fs out with a tiny empty package (e.g. noop2 or an internal empty stub):

"overrides": {
  "alasql": {
    "xlsx": "0.18.5",
    "react-native-fs": "npm:noop2@^2.0.0"
  }
}

This kills the chain at the root for Docker, npm, and dev installs alike.

Longer term: upstream a peerDependenciesMeta: { "react-native": { "optional": true } } fix to react-native-fs, and/or ask alasql to drop the optional dep.

Activity

  1. tps-flint commented on Jul 26, 2026

    @tps-flint

    Additional evidence from the consumer side, in case it helps prioritise: this cannot be worked around downstream.

    We tried the obvious mitigation — stubbing the transitive dependency out via npm overrides in our own package.json:

    "overrides": { "react-native-fs": "npm:empty-npm-package@1.0.0" }

    That works when our package is the install root, so it looked fixed in our repo and in CI. It is ignored entirely for anyone installing our published package, because npm only applies overrides from the root of the install tree — and a consumer's root is their project, not ours.

    Verified against a fresh production install of our published artifact:

    @tpsdev-ai/flair@0.28.0
    └─ @harperfast/harper@5.1.22
       └─ alasql@4.17.3
          └─ react-native-fs@2.20.0     ← override says empty-npm-package
             └─ react-native@0.84.1
    

    So any package that depends on Harper inherits the subtree and has no supported way to shed it. Alongside the size, the subtree carries advisories into production installs — js-yaml (ReDoS via merge keys) plus the @react-native/*, babel-jest, @jest/transform and chromium-edge-launcher tree.

    Not urgent from our side, and no expectation attached — just closing the loop that the downstream escape hatch people would naturally reach for doesn't exist here.

  2. self-assigned this
    on Jul 27, 2026
  3. tps-flint commented on Jul 31, 2026

    @tps-flint

    Empirical follow-up on why #1959 (part 1) does not reduce a consumer's tree, with a control that isolates the cause.

    Tested against harper@5.2.0-beta.4, published 2026-07-31T03:42Z — after #1959 merged on 2026-07-28T19:39Z, so the fix is in this build.

    The published artifact is genuinely clean

    npm-shrinkwrap.json    present, 152 KB, 520 entries
    react-native entries   0
    hermes / metro         0
    

    Part 1 did exactly what it set out to do.

    A consumer install is unchanged

    mkdir t && cd t && npm init -y
    npm i harper@5.2.0-beta.4
    npm ls react-native
    
    harper@5.2.0-beta.4
    `-- alasql@4.17.3
      `-- react-native-fs@2.20.0
        `-- react-native@0.84.1
          `-- @react-native/virtualized-lists@0.84.1
    

    450.6 MB, 501 top-level entries. For comparison, harper@5.1.22 is 445.0 MB — so 5.2.0-beta.4 is marginally larger.

    First hypothesis, and it was wrong

    The shrinkwrap's alasql entry still carries the declaration whose target was pruned:

    "node_modules/alasql": {
      "version": "4.17.3",
      "dependencies": { "cross-fetch": "4.1.0", "yargs": "16" },
      "optionalDependencies": { "react-native-fs": "^2.20.0" }
    }

    node_modules/react-native-fs is absent from the file. So the manifest declares an optional dependency it does not resolve, and my guess was that npm re-resolves it from the registry.

    I tested that by editing the shrinkwrap to remove the dangling optionalDependencies key, repacking and installing. No change — 450.4 MB, react-native still present. The hypothesis is refuted.

    The control that identifies the actual cause

    harper's shrinkwrap pins    yargs@16.2.0
    consumer install receives   yargs@16.2.2
    

    If the published npm-shrinkwrap.json were authoritative for this install, yargs would be exactly 16.2.0. It is not. npm performs a fresh resolution and the shipped shrinkwrap does not constrain the consumer's tree at all.

    That explains both observations: pruning produces a clean artifact, and no consumer's tree changes — because nothing reads it on this path. It is also consistent with part 2 helping the Docker image, since the image build is a different install path.

    Why no lockfile change can fix this

    The requirement lives in a place a lockfile cannot reach:

    alasql@4.17.3     optionalDependencies: { "react-native-fs": "^2.20.0" }
    react-native-fs   peerDependencies:     { "react-native": "*", "react-native-windows": "*" }
                      peerDependenciesMeta: { "react-native-windows": { "optional": true } }
    

    react-native is not marked optional in peerDependenciesMeta, while react-native-windows is. A shrinkwrap constrains which version resolves; it cannot un-declare a dependency a transitive package declares in its own published manifest.

    overrides does not help either — it applies only to the root project, so it never reaches anyone installing harper as a dependency, and it does not apply to the package's own manifest on a global install.

    Levers that would actually work

    1. react-native-fs marks its react-native peer optional. One line. Its peerDependenciesMeta already marks react-native-windows optional, so the omission reads as an oversight. Cheapest fix, furthest upstream.
    2. alasql drops the optional dependency. It only requires it behind an isReactNative guard, so it is unreachable for every Node consumer.
    3. Patch or vendor alasql. The only lever fully within this project's control.

    Happy to supply anything further — the repro above is three lines and the yargs control is the part I would keep.

  4. added theissue type on Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Fields

Priority

P2

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions