Skip to content

feat!: build deployments with web-standard FormData - #489

Merged
LautaroPetaccio merged 3 commits into
masterfrom
feat/web-standard-formdata-deploy
Jun 20, 2026
Merged

feat!: build deployments with web-standard FormData#489
LautaroPetaccio merged 3 commits into
masterfrom
feat/web-standard-formdata-deploy

Conversation

@LautaroPetaccio

@LautaroPetaccio LautaroPetaccio commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

What

Makes the client isomorphic on native fetch — it works in both Node (≥ 18) and the browser, with no node-fetch / Node-only APIs in the request/response paths.

Deployments use web-standard FormData

deploy() builds the multipart body with the web-standard FormData + Blob (global in browsers and Node ≥ 18) instead of the Node form-data package. Files are appended as Blobs for both node and browser, so the old isNode() branch is gone.

form-data produced a stream whose multipart/...; boundary= Content-Type only got set when the request went through node-fetch — so deploy() couldn't be used with a native-fetch fetcher (it sent text/plain and dropped the body). With web FormData, native fetch (and node-fetch v3 / browsers) set the multipart Content-Type automatically.

Proof (web FormData → native fetch() → busboy parse):

content-type: multipart/form-data; boundary=----formdata-undici-...   ← native fetch set it
field entityId: QmENTITY        file QmA: { filename: "QmA", content: "opq" }
authChain[0][type]: SIGNER      file QmB: { filename: "QmB", content: "asd" }

Downloads return Uint8Array

downloadContent() previously returned a Node Buffer (Buffer.from(...)), which doesn't exist in the browser. It now returns a web-standard Uint8Array — the last Node-only API in src. The internal hash check (hashV0/hashV1) already accepts Uint8Array.

Fully native + cleanup

  • Default fetcher now uses @dcl/fetch-component (native, same retry/timeout behaviour) instead of @well-known-components/fetch-component (which was node-fetch v2 via cross-fetch).
  • Dropped dependencies: form-data, @types/form-data, @well-known-components/fetch-component, cross-fetch.
  • engines.node set to >=18 (the client now relies on global fetch/FormData/Blob).
  • Removed the dead, non-exported isNode() helper.
  • skipLibCheck enabled (the @dcl/core-commons declarations pulled in via @dcl/fetch-component reference jest types in their published mocks).

Breaking changes

  1. deploy() requires the injected fetcher to support web-standard FormData request bodies — native fetch, node-fetch v3, or a browser fetch. Fetchers built on node-fetch v2 no longer serialize the deployment body correctly.
  2. downloadContent() now resolves to Uint8Array instead of Buffer. Buffer is a Uint8Array subclass so byte access is unchanged, but consumers using Buffer-only methods (e.g. toString('hex')) must wrap the result with Buffer.from(...).

Tests

  • Rewrote the buildEntityFormDataForDeployment unit test to assert via the web FormData API (form.get(), File.name, Blob.text()) instead of node form-data's .getBuffer().
  • Updated downloadContent assertions to compare Uint8Array.
  • Full build (prebuild snapshots + orval + tsc) ✓, suite 55 passed / 4 suites, tsc --noEmit ✓, eslint ✓.

🤖 Generated with Claude Code

deploy() now builds the multipart body with the web-standard FormData and Blob
(global in browsers and Node >= 18) instead of the node `form-data` package,
appending file contents as Blobs for both node and browser (the node/browser
branch and the form-data dependency are gone).

When the resulting form is used as a request body, native fetch (and node-fetch
v3) set the multipart Content-Type with the boundary automatically, so
deployments now work with a native-fetch fetcher without relying on node-fetch's
form-data handling.

BREAKING CHANGE: deploy() requires the injected fetcher to support web-standard
FormData request bodies (native fetch, node-fetch v3, or a browser fetch).
Fetchers built on node-fetch v2 no longer serialize the deployment body
correctly.
@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown

Test this pull request

  • The package can be tested by running
    yarn upgrade "https://sdk-team-cdn.decentraland.org/@dcl/catalyst-client/branch/feat/web-standard-formdata-deploy/dcl-catalyst-client-21.11.1-27879978083.commit-80891e4.tgz"

Follow-up to the web-standard FormData deploy change, completing the move off
node-fetch in the client.

- default fetcher now uses @dcl/fetch-component (native, with the same
  retry/timeout behaviour) instead of @well-known-components/fetch-component
  (which was node-fetch v2 via cross-fetch). Updated the build snapshot script
  and tests accordingly.
- drop the now-unused direct dependencies @well-known-components/fetch-component
  and cross-fetch.
- declare engines.node >= 18 (the client now relies on global fetch/FormData/Blob).
- remove the dead, non-exported isNode() helper.
- enable skipLibCheck (the @dcl/core-commons type declarations pulled in via
  @dcl/fetch-component reference jest types in their published mocks).
downloadContent built its result with Buffer (Node-only), which breaks the
client in the browser. Use the web-standard Uint8Array instead — hashV0/hashV1
already accept it, and Buffer was the only Node-only API left in src.

BREAKING CHANGE: downloadContent() now resolves to Uint8Array instead of
Buffer. Buffer is a Uint8Array subclass, so byte access is unaffected, but
consumers relying on Buffer-only methods (e.g. toString('hex')) must wrap the
result with Buffer.from().

@decentraland-bot decentraland-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.

Review Summary

Clean, well-motivated PR that makes @dcl/catalyst-client isomorphic by replacing Node-specific APIs with web standards. Net result: −46 lines, 3 fewer dependencies, unified code path for Node ≥ 18 and browsers.

What changed

  • deploy() — Replaced the form-data npm package with web-standard FormData + Blob. The old isNode() branch (Node → Buffer, browser → Blob) is gone; a single new Blob([file]) path works everywhere.
  • downloadContent() — Returns Uint8Array instead of Buffer. The internal hash check already accepted Uint8Array.
  • Default fetcher — Swapped @well-known-components/fetch-component (node-fetch v2 via cross-fetch) for @dcl/fetch-component (native fetch).
  • Dropped depsform-data, @types/form-data, cross-fetch, @well-known-components/fetch-component.
  • Removed dead isNode() helper and arrayBufferFrom() shim.
  • engines.node ≥ 18 — Correctly documents the runtime requirement.

Findings

[P2 — Minor] skipLibCheck: true masks type errors

tsconfig.json now enables skipLibCheck because @dcl/core-commons (transitive via @dcl/fetch-component) publishes declaration files that reference jest types. This is a pragmatic workaround but it disables type-checking of all .d.ts files, which can hide real type mismatches in third-party packages. Consider filing an issue on @dcl/core-commons to fix the published declarations so skipLibCheck can be reverted.

[P2 — Minor] Consumer migration path

With ~30 repos across the org depending on this package, the two breaking changes are significant:

  1. deploy() now requires a fetcher that supports web-standard FormData bodies (node-fetch v2 via @well-known-components/fetch-component will no longer serialize correctly).
  2. downloadContent() returns Uint8Array — consumers calling Buffer-only methods (e.g. .toString('hex')) need Buffer.from(result).

Both are clearly documented in the PR description. Since versioning is handled by oddish-action and feat!: triggers a major bump, consumers won't be broken silently. Consider adding a short migration note in the README or CHANGELOG to help downstream maintainers.

Security

No security issues found. No hardcoded secrets, no injection vectors, auth chain handling unchanged, @dcl/fetch-component is an internal DCL package with minimal supply-chain risk.

Git conventions (ADR-6)

  • PR titlefeat!: build deployments with web-standard FormData
  • Branchfeat/web-standard-formdata-deploy

CI

All checks passing ✅

Code quality

  • The unification from two code paths (Node/browser) to one (new Blob([file])) is a clear simplification.
  • DeploymentPreparationData.files already typed as Map<string, Uint8Array>, so the Uint8Array return from downloadContent() is consistent with the rest of the type surface.
  • Tests are well-updated: the buildEntityFormDataForDeployment test now asserts via the web FormData API (form.get(), Blob.text()), and the download tests compare against new Uint8Array(…). Notably, one test input still uses Buffer.from('asd') (line 17), which validates that Buffer (as a Uint8Array subclass) still works as deploy input — good backward-compat check.
  • The custom IFetchComponent / FetchResponse types in types.ts are well-defined as the structural intersection of the old WKC interface and the native fetch interface, making migration transparent for callers.

Verdict

Approved — No P0 or P1 issues. The two P2 items (skipLibCheck tech debt, consumer migration docs) are worth tracking but don't block merge.


Reviewed by Jarvis 🤖 · Requested by Lautaro Petaccio via Slack

@LautaroPetaccio
LautaroPetaccio merged commit ad7de29 into master Jun 20, 2026
4 checks passed
@LautaroPetaccio
LautaroPetaccio deleted the feat/web-standard-formdata-deploy branch June 20, 2026 19:07
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.

2 participants