Skip to content

The barrel re-exports fs, so importing from @sasjs/utils pulls the crypto-browserify chain #275

Closed

Description

@sasjs-dev

What

@sasjs/utils is used in browser bundles, but importing anything from the barrel (import { x } from '@sasjs/utils') pulls the whole crypto-browserify chain, because the barrel re-exports the fs module:

src/index.ts:11        export * from './fs'
src/fs/index.ts:1      export { getHash, compareHashes, extractHashArray } from './hash'
src/fs/hash.ts:3       import { createHash } from 'crypto'

Webpack aliases that crypto to crypto-browserify, which drags in browserify-sign -> parse-asn1 -> asn1.js -> vm-browserify: roughly 555 KB of AES/RSA/ECDSA polyfills, plus an eval() from the vm shim.

Why it matters

That combination - dynamic code execution plus a full crypto suite in one bundle - is the dual-use shape npm's publish scanner is trained on, and it slows the scan of any package that bundles us. It is also dead weight for a consumer that only wanted, say, a type guard.

sasjs/adapter is unaffected only because it uses deep imports (@sasjs/utils/types, /error, /file, /time, /auth, /input, /logger). Anything importing from the barrel pays for it.

This is the remaining half of #274, which removed the crypto import from utils/base64.ts - the other entry point into the same chain.

Options

  1. Stop re-exporting ./fs from src/index.ts and require @sasjs/utils/fs for it. Cleanest, but breaking for barrel consumers of getHash, compareHashes, extractHashArray, generateCompileProgram and the sync helpers.
  2. Split fs into a subpath export via the exports map, keeping the barrel free of it. Same effect, declared explicitly rather than by omission.
  3. Make the crypto import lazy inside fs/hash.ts (a dynamic import() inside the functions that hash), so a bundler can leave it out of the initial chunk.

crypto itself cannot go - createHash genuinely needs it. The problem is it sitting on the barrel's path.

Verification, whichever option

Build a bundle that imports one trivial thing from the barrel, and assert the crypto chain is absent:

grep -c 'runInThisContext\|browserify-sign\|parse-asn1' <bundle>.js   # expect 0

Activity

  1. sasjs-dev commented on Oct 15, 2026

    @sasjs-dev
    ContributorAuthor

    Correcting the verification step: the greps cannot see this chain.

    grep -c 'runInThisContext\|browserify-sign\|parse-asn1' *.js returns 0 for a bundle that carries all three, because webpack renumbers modules and rewrites requests - the package name never appears in the output. The published @sasjs/adapter 4.19.4 bundle is the proof: it carries 19 browserify-sign modules and not one occurrence of the string browserify-sign. So the check as written passes whether or not the chain is there.

    Two checks that do work, both run against 4.19.4 (chain present, exit 1) and against the adapter's rebuilt bundle (chain absent, exit 0):

    1. Read the module list off the source map. A bundle built with an inline source map carries sources, the path of every module in it:

    const fs = require('fs')
    const b = fs.readFileSync(process.argv[2], 'utf8')
    const i = b.indexOf('sourceMappingURL=data:application/json;charset=utf-8;base64,')
    const m = JSON.parse(Buffer.from(b.slice(i).split(',')[1], 'base64').toString())
    const bad = m.sources.filter((s) =>
      /crypto-browserify|browserify-sign|parse-asn1|public-encrypt|vm-browserify|asn1\.js/.test(s)
    )
    console.log(bad.length ? bad.join('\n') : 'clean')
    process.exit(bad.length ? 1 : 0)

    2. Look for code, not names. Signatures that only this chain carries:

    browserify/crypto-browserify           crypto-browserify's own comment
    createCredentials is not implemented   crypto-browserify
    DH_CHECK_P_NOT_SAFE_PRIME              crypto-browserify's constants table
    _signType                              browserify-sign
    function oaep (key, msg)               public-encrypt
    runInThisContext                       the vm shim
    process.binding is not supported       the process/browser polyfill
    inspect.styles                         the util npm polyfill
    

    The last one is the shape worth knowing about: a require('util') inside a node-guarded function still resolves, because webpack consults resolve.fallback only when a request does not resolve - and the util package is in the tree. It needs an alias. Measured on the adapter: 28 KiB of Node's util source (util.js 19697 bytes, support/types.js 8672, support/isBufferBrowser.js 203) in both browser outputs, removed by resolve.alias: { util: false }.

    On the adapter, and the barrel specifically

    The adapter is not a barrel consumer, and that is structural rather than lucky: no file under its src/ imports @sasjs/utils (the only occurrence is a jest.mock in a spec), and the 4.19.4 bundle's source map lists 42 @sasjs/utils/* modules with @sasjs/utils/index.js absent and no fs/* module at all. What put the chain in that bundle was @sasjs/utils/utils/base64.js line 7, require('crypto') - #274, not the barrel. Both halves are real and separate entry points.

    The adapter now asserts this on every build: release/bundle-check.mjs (in #932) fails on the polyfill signatures above, and on any browser-reachable source file importing the barrel or @sasjs/utils/fs. A barrel consumer has no such guard, which is the case for closing this issue.

  2. sasjs-dev commented on Oct 15, 2026

    @sasjs-dev
    ContributorAuthor

    Fixed, in #283.

    The barrel no longer re-exports ./fs, and the helpers stay reachable at @sasjs/utils/fs:

    import { getHash } from '@sasjs/utils'      // before
    import { getHash } from '@sasjs/utils/fs'   // after

    Measured on a webpack browser build (target web, Node polyfill plugin - the shape this issue describes) importing one runtime value from the barrel:

    bundle modules packages crypto-suite packages
    before 1971 kB 473 103 21
    after 822 kB 292 65 0

    That is the whole chain this issue names - crypto-browserify, browserify-sign, parse-asn1, asn1.js, vm-browserify - gone from a barrel-importing bundle, along with the vm shim's eval.

    Two corrections to the issue's own reasoning, both from that measurement:

    Option 1 does not make the barrel browser-safe, only crypto-free. The remaining 822 kB is the other node-only modules the barrel re-exports by design: prompts (65 modules, via ./input), fs-extra (28, via ./file and ./fs), @fast-csv/format, chalk, consola, graceful-fs. Such a bundle still fails to build on prompts' unresolved readline. So the honest contract, now in the README, is that the barrel is the Node entry point and browser code imports the subpath it needs.

    Option 3 would not help. A dynamic import('crypto') inside fs/hash.ts still gets bundled - webpack emits it as a separate chunk - so the chain stays in the package's browser output; it defers rather than removes.

    Option 2 remains the larger breaking change of the two: this package has no exports field, so adding one means enumerating every existing subpath or deep imports stop resolving.

    One consumer in the estate did import them from the barrel: the CLI's fs commands (src/commands/fs/fsCommand.ts), which use all seven. Fixed in sasjs/cli#1484, which lands independently of the utils release. The server, lint, vscode-extension and Data Controller either do not use these helpers or already import them from @sasjs/utils/fs (vscode-extension's sync-directories command has done so all along).

    (An earlier version of this comment claimed the CLI did not use them. That was wrong, and the search behind it was wrong: it filtered grep output by lines containing import/require, which drops every name listed on its own line inside a multi-line import block.)

    #283 carries Closes #275, so this closes on merge. It declares BREAKING CHANGE, so it releases as a major; the estate pins utils exactly and uses the subpaths, so no consumer has to move with it.

  3. sasjs-gha commented on Oct 15, 2026

    @sasjs-gha

    🎉 This issue has been resolved in version 4.0.0 🎉

    The release is available on GitHub release

    Your semantic-release bot 📦🚀

  4. added a commit that references this issue on Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestjavascriptPull requests that update javascript codereleased

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions