Skip to content

fix(upload): vendor form-data so bundlers can ship it - #681

Merged
John-David Dalton (jdalton) merged 1 commit into
mainfrom
jdalton/ce-356-vendor-form-data
Aug 4, 2026
Merged

John-David Dalton (jdalton) merged 1 commit into
mainfrom
jdalton/ce-356-vendor-form-data

Conversation

@jdalton

@jdalton John-David Dalton (jdalton) commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

socket@latest (1.1.151/1.1.152) throws Cannot find module 'form-data' on every fresh install's first multipart upload and a customer's CI has been down for over a day. The break is in how this SDK reaches form-data, so the fix lands here.

Root cause. getFormData() used createRequire(...)('form-data') — a dynamic call no bundler can follow. socket-cli's rollup inlines the SDK into dist/vendor.js, so the bare specifier shipped with form-data neither bundled nor declared, and the first upload threw on any clean install. The 4.1.3 sibling chunk does not survive consumer bundling either: rollup inlines dist/index.js without carrying the chunk across, and both ./form-data.js and form-data resolve MODULE_NOT_FOUND from the built CLI (reproduced with createRequire against the real artifact).

Fix. Vendor form-data through src/external/ the way socket-lib handles its externals:

  • src/external/form-data.js — static module.exports = require('form-data') shim, with a hand-authored structural .d.ts so published types never import a package consumers don't install.
  • getFormData() requires the shim by static relative literal, and a scoped external() in the rolldown config emits that require verbatim into dist/index.js, so the deferred node:http eval that keeps SDK importers --build-snapshot-safe is preserved.
  • A separate externals build emits self-contained dist/external/form-data.js (socket-lib's build-externals step, SDK-sized), which the verbatim require resolves at runtime — and which a consumer bundler resolves and inlines. Ships via files, never an exports subpath.
  • The external() match is by resolved path under this repo's src/external/ only — never a blanket external/-segment test, which would also externalize the nested dist/external/* internals of the inlined @socketsecurity/lib and emit requires into a package consumers never install.
  • @types/node is now explicit in both tsconfigs — it was previously loaded only through form-data's typings, which vanish with the old chunk entry; without it the whole program silently loses node globals (45 errors).

Verified end-to-end with socket-cli rebuilt against this SDK: zero form-data requires anywhere in the CLI dist (bytes inlined into vendor.js), the undeclared-runtime-require scan is back to the 1.1.147 baseline, and a dist-mode uploadManifestFiles against a live local server put a real 6,984-byte multipart body on the wire (verified for both the plain-install path and the bundled path).

Actions needed

  • Review and merge, then cut an SDK release — the customer fix ships only when socket-cli v1.x picks up that release and publishes 1.1.153 (I can drive both once this lands).
  • Customer workaround until then: pin socket@1.1.147.
Why not the simpler fixes, and what deliberately did not change
  • Just bump the CLI to SDK 4.1.3 — measured, does not work: rollup inlines the SDK without the sibling chunk, both specifiers MODULE_NOT_FOUND from the built dist.
  • Add form-data to the CLI's dependencies — works but treats the symptom in one consumer; any other bundling consumer hits the same wall.
  • A ./form-data package export with self-reference resolution — tried first; preferring the export from a source run loads the bundled copy while callers hold the node_modules copy, putting two FormData classes in one process and breaking instanceof (6 test failures). Reverted.
  • Blanket-externalizing external/ paths in the main bundle (socket-lib's exact predicate) — breaks this build: the SDK inlines @socketsecurity/lib, whose internals require lib's own dist/external/*; externalizing those emits relative requires into a package consumers never install. The scoped-by-resolved-path predicate is the fix; a matching scoping PR for socket-lib's own config is coming separately.
  • Exports surface unchanged: same five entries, no new public subpath.
  • Regression tests pin the shim shape, the packaging (files + exports ignore), and the no-bare-specifier bundle shape; the mirror-named split is file-upload.test.mts + package-exports.config.test.mts.
  • Pre-existing and untouched: 3 utils.test.mts resolveBasePath failures (fail on pristine main in a /tmp worktree — macOS /private symlink) and the dispatch-table-is-current check (fails on pristine main).

cursor[bot]

This comment was marked as resolved.

socket-cli 1.1.151/1.1.152 crashed every fresh install's first
multipart upload with "Cannot find module 'form-data'" (customer CI
down 24h+). The CLI's rollup inlines the SDK, and the
SDK reached form-data through createRequire('form-data') — a
dynamic call no bundler can follow — so the bare specifier shipped
with form-data neither bundled nor declared. The 4.1.3 sibling
chunk (dist/form-data.js) did not survive consumer bundling either:
rollup inlines dist/index.js without carrying the chunk across, and
both specifiers resolve MODULE_NOT_FOUND from the built output.

Vendor form-data through src/external/ the way socket-lib handles
its externals: getFormData() requires './external/form-data.js' by
static relative literal, a scoped external() emits it verbatim, and
a separate externals build ships the self-contained
dist/external/form-data.js it resolves — laziness (the deferred
node:http eval that keeps SDK importers snapshot-safe) preserved.
Consumer bundlers resolve the relative require against the shipped
file and inline it. The external() match is by resolved path under
THIS repo's src/external only, never a blanket external/ segment
test — a segment match also externalizes the nested dist/external
internals of the inlined @socketsecurity/lib, emitting requires
into a package consumers never install. dist/external ships via
files but is never an exports subpath.

@types/node is now explicit in both tsconfigs: it was previously
loaded only through form-data's typings, which vanish with the old
chunk entry — without it the whole program loses node globals.

Verified end-to-end: socket-cli rebuilt against this SDK contains
zero form-data requires (bytes inlined into vendor.js), and a
dist-mode uploadManifestFiles put a real multipart body on the
wire. Regression tests pin the shim, the packaging, and the
no-bare-specifier bundle shape.
@jdalton
John-David Dalton (jdalton) force-pushed the jdalton/ce-356-vendor-form-data branch from 00f5933 to 20cb07e Compare August 4, 2026 19:25
@jdalton
John-David Dalton (jdalton) merged commit 2b77d70 into main Aug 4, 2026
4 checks passed
@jdalton
John-David Dalton (jdalton) deleted the jdalton/ce-356-vendor-form-data branch August 4, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant