Skip to content

Fix Node.js compatibility for CJS consumers and WASM initialization#8

Open
kaleababayneh wants to merge 5 commits intoLumeraProtocol:masterfrom
kaleababayneh:master
Open

Fix Node.js compatibility for CJS consumers and WASM initialization#8
kaleababayneh wants to merge 5 commits intoLumeraProtocol:masterfrom
kaleababayneh:master

Conversation

@kaleababayneh
Copy link
Copy Markdown

@kaleababayneh kaleababayneh commented Mar 21, 2026

Problem

The SDK doesn't work properly in Node.js environments. Two issues block usage:

  1. window is not defined (screenshot 1)
    Importing the SDK immediately crashes because rq-library-wasm uses a top-level import that eagerly evaluates browser_fs_mem.js, a browser-only file that assigns to window. This occurs even if the consumer never uses any RaptorQ functionality.

  2. ERR_UNKNOWN_FILE_EXTENSION: ".wasm" (screenshot 2)
    Node's ESM loader does not recognize .wasm file extensions, causing WASM initialization to fail.

Fix

  1. Lazy-load the WASM module — Instead of importing rq-library-wasm at the top of the file (which immediately executes browser-only code), the module is now loaded on demand via import() only when RaptorQ is actually invoked. A globalThis.window shim is applied before loading so that the browser FS code does not throw in Node.js.

  2. Correct the CJS build pathspackage.json pointed to dist/cjs/index.cjs, but tsc outputs index.js. Updated the paths and added a {"type": "commonjs"} marker in dist/cjs/ so Node resolves the module system correctly.

✅ Built and verified locally in Node.js environments.

Screenshot 2026-03-21 at 22 05 53 Screenshot 2026-03-21 at 22 06 08 Screenshot 2026-03-22 at 11 23 13

@roomote-v0
Copy link
Copy Markdown

roomote-v0 bot commented Mar 21, 2026

Rooviewer Clock   See task

Re-reviewed after 13d2d6b. The package-lock.json deletion resolves that issue. One minor .gitignore artifact from the missing trailing newline, and the async breaking change still stands.

  • getWasmSource() uses typeof window === "undefined" for environment detection, but getRqModule() shims globalThis.window in Node.js -- on a retry after init failure, the Node.js path will incorrectly take the browser code path
  • package-lock.json (8,200+ lines) should not be committed -- this project uses pnpm (pnpm-lock.yaml already exists)
  • Re-exported utility functions (getFileSize, syncDirExists, etc.) changed from synchronous to async, which is a breaking change to the public API surface
  • .gitignore: the coverage/ entry under "# Vitest" was corrupted to coverage/package-lock.json due to the original file missing a trailing newline (no functional impact since coverage/ is already ignored on line 171)
Previous reviews

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

Comment on lines +24 to +25
async function getWasmSource(): Promise<any> {
if (typeof window === "undefined") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getWasmSource() checks typeof window === "undefined" to decide between the Node.js and browser code paths. But getRqModule() (line 17) sets globalThis.window = globalThis as a shim for Node.js. If initialize() fails after getRqModule() has already been called and cached (e.g., the WASM init itself throws), initPromise is reset to null, allowing a retry. On that retry, getWasmSource() now sees window as defined and takes the browser path (import("rq-library-wasm/rq_library_bg.wasm?url")), which will fail in Node.js.

Consider using a check that is not affected by the shim, such as typeof process !== "undefined" && process.versions?.node or checking before the shim is applied.

Fix it with Roo Code or mention @roomote and request a fix.

@@ -0,0 +1,8242 @@
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This project uses pnpm as its package manager (there is already a pnpm-lock.yaml in the repo). Committing a package-lock.json (8,200+ lines) introduces a conflicting lockfile that can cause dependency resolution confusion. This file should be removed from the PR and added to .gitignore.

Fix it with Roo Code or mention @roomote and request a fix.

Comment on lines +315 to +318
export async function getFileSize(...args: Parameters<typeof import("rq-library-wasm").getFileSize>) {
const rq = await getRqModule();
return rq.getFileSize(...args);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These re-exported utility functions (getFileSize, syncDirExists, etc.) were previously synchronous re-exports from rq-library-wasm. Wrapping them in async functions changes their return type from T to Promise<T>. Any downstream consumer that was calling, for example, getFileSize(path) synchronously (without await) will now silently receive a Promise object instead of a number. This is a breaking change to the public API surface. If these functions are not currently consumed externally, this is fine, but it should be called out in release notes or the version should be bumped accordingly.

Fix it with Roo Code or mention @roomote and request a fix.

Comment on lines +176 to +177
coverage/package-lock.json
package-lock.json
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original file ended with coverage/ and no trailing newline. When package-lock.json was appended, it got concatenated onto that line, turning coverage/ into coverage/package-lock.json. No functional impact since coverage/ is already ignored on line 171, but this wasn't the intended result.

Suggested change
coverage/package-lock.json
package-lock.json
coverage/
package-lock.json

Fix it with Roo Code or mention @roomote and request a fix.

Copy link
Copy Markdown
Contributor

@mateeullahmalik mateeullahmalik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR — the intent is good and I appreciate the effort to improve Node compatibility + CJS consumption.

I went through the changes and validated locally. Before we can merge, there are a few blocking issues to address:

  1. CJS runtime break: import.meta.url is still emitted into CJS output from src/wasm/raptorq-proxy.ts, which causes SyntaxError: Cannot use 'import.meta' outside a module when requiring the CJS build.
  2. Public API behavior drift: re-exported FS helpers now become async wrappers (e.g. getFileSize, dirExists, syncDirExists). This changes consumer-facing behavior/signatures and is a breaking change unless explicitly versioned + documented.
  3. Current branch test status: tests/wasm/raptorq-proxy.spec.ts is failing in this branch and should be green before merge.

Please fix the above and I’ll re-review quickly. Happy to approve once these are addressed 🙌

} else {
const { createRequire } = await import("node:module");
// @ts-ignore - import.meta.url is only available in ESM
const req = createRequire(import.meta.url);
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.

import.meta.url here is problematic for the CJS build. In the emitted dist/cjs output this causes a hard parse/runtime failure (Cannot use 'import.meta' outside a module) for require() consumers. We should avoid emitting import.meta into CJS-targeted code paths.

const rq = await getRqModule();
return rq.readFileChunk(...args);
}
export async function getFileSize(...args: Parameters<typeof import("rq-library-wasm").getFileSize>) {
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.

This changes getFileSize from a direct export to an async wrapper returning a Promise. That is a behavior/signature change for public API consumers. Please preserve the original sync/async contract (or treat as explicit breaking change with versioning/docs).

const rq = await getRqModule();
return rq.dirExists(...args);
}
export async function syncDirExists(...args: Parameters<typeof import("rq-library-wasm").syncDirExists>) {
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.

Similar concern for syncDirExists/dirExists wrappers: converting these to async wrappers changes external behavior. Can we keep export semantics aligned with previous API surface to avoid regressions for existing callers?

@kaleababayneh
Copy link
Copy Markdown
Author

kaleababayneh commented Mar 26, 2026

The existing example used DirectSecp256k1HdWallet which doesn't support signArbitrary since cosmjs hasn't implemented it yet (cosmjs#844). Cascade requires ADR-036 arbitrary signing, so the example now creates a combined signer that wraps both DirectSecp256k1HdWallet (for signDirect) and Secp256k1HdWallet (for signAmino/signArbitrary).

Worth considering creating a LumeraSigner.fromMnemonic() helper that handles all three signing methods out of the box.

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