You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
The barrel re-exports fs, so importing from @sasjs/utils pulls the crypto-browserify chain #275
@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
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.
Split fs into a subpath export via the exports map, keeping the barrel free of it. Same effect, declared explicitly rather than by omission.
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:
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:
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.
The barrel no longer re-exports ./fs, and the helpers stay reachable at @sasjs/utils/fs:
import{getHash}from'@sasjs/utils'// beforeimport{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.
Activity
build(release): guard the browser bundle against the Node polyfill tree
sasjs-dev commentedon Oct 15, 2026
Correcting the verification step: the greps cannot see this chain.
grep -c 'runInThisContext\|browserify-sign\|parse-asn1' *.jsreturns 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/adapter4.19.4 bundle is the proof: it carries 19 browserify-sign modules and not one occurrence of the stringbrowserify-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:2. Look for code, not names. Signatures that only this chain carries:
The last one is the shape worth knowing about: a
require('util')inside a node-guarded function still resolves, because webpack consultsresolve.fallbackonly 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 byresolve.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 ajest.mockin a spec), and the 4.19.4 bundle's source map lists 42@sasjs/utils/*modules with@sasjs/utils/index.jsabsent and nofs/*module at all. What put the chain in that bundle was@sasjs/utils/utils/base64.jsline 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.sasjs-dev commentedon Oct 15, 2026
Fixed, in #283.
The barrel no longer re-exports
./fs, and the helpers stay reachable at@sasjs/utils/fs:Measured on a webpack browser build (target web, Node polyfill plugin - the shape this issue describes) importing one runtime value from the barrel:
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./fileand./fs),@fast-csv/format,chalk,consola,graceful-fs. Such a bundle still fails to build on prompts' unresolvedreadline. 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')insidefs/hash.tsstill 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
exportsfield, 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 declaresBREAKING CHANGE, so it releases as a major; the estate pins utils exactly and uses the subpaths, so no consumer has to move with it.feat(barrel)!: stop re-exporting ./fs, the one module that imports cr…
sasjs-gha commentedon Oct 15, 2026
🎉 This issue has been resolved in version 4.0.0 🎉
The release is available on GitHub release
Your semantic-release bot 📦🚀
fix(deps): bump @sasjs/utils to 4.0.0, the release that drops fs from…
refactor(test): read the utils file helpers from their own subpath
feat(package): ship the adapter as compiled modules, with the browser…
fix(fs): import the hash and sync helpers from the utils subpath (#1484)
chore(release): 5.1.0 [skip ci]