Skip to content

POC: support dynamic ESM imports in node-vm scripts#8394

Open
Cicolas wants to merge 8 commits into
usebruno:mainfrom
Cicolas:poc/esm-node-vm-imports
Open

POC: support dynamic ESM imports in node-vm scripts#8394
Cicolas wants to merge 8 commits into
usebruno:mainfrom
Cicolas:poc/esm-node-vm-imports

Conversation

@Cicolas

@Cicolas Cicolas commented Jun 27, 2026

Copy link
Copy Markdown

Description

This PR adds proof-of-concept support for dynamic ESM imports in Bruno’s node-vm runtime.

Developer Mode scripts can now use await import() to load ESM modules, including local .mjs files and package dependencies.

Changes

  • Enables Node’s default dynamic import loader when compiling node-vm scripts.
  • Adds test coverage for importing a local .mjs helper from a Bruno script.
  • Keeps existing behavior that .mjs files cannot be loaded through require().

Example

Tested manually with a Bruno collection using:

const { makeUser } = await import('./libs/faker-helper.mjs');

where faker-helper.mjs imports a package dependency:

import { faker } from '@faker-js/faker';

This worked with a collection-local package.json dependency on @faker-js/faker.

Testing

npm run test --workspace=packages/bruno-js -- --runTestsByPath src/sandbox/node-vm/index.spec.js

Result:

PASS src/sandbox/node-vm/index.spec.js
Tests: 49 passed

Notes

This is intentionally scoped as a POC. Dynamic imports currently use Node’s default VM import loader. A follow-up can add a Bruno-aware ESM loader with parity to createCustomRequire.

The code editor keeps showing an error on import used as a function as you can see on this print: (fixed on 7cbe5572aef1a663f48b84df8519a6dd3a0a3f6b)

image

CustomImport

createCustomImport, works similarly in spirit to [import-in-the-middle] (https://github.com/nodejs/import-in-the-middle), but scoped to Bruno’s node-vm runtime instead of using a process-wide Node loader hook.

Node’s vm.Script supports importModuleDynamically as a function, which lets us customize how import() is resolved and evaluated when user scripts call dynamic import. That gives Bruno a path to implement ESM support with the same guarantees as createCustomRequire.

Related Issues

#3537

Summary by CodeRabbit

  • New Features

    • Enabled dynamic import() (including await import()) for .mjs modules when running code in the Node VM sandbox.
  • Bug Fixes

    • Improved ESM/CJS interop in the sandbox so imported results and exported values propagate correctly.
    • Updated JavaScript linting to ignore a specific JSHint false-positive related to await import(...) after top-level await.
    • Adjusted behavior to block unsupported .mjs usage via require() with clearer failure behavior.
  • Tests

    • Expanded Node VM and CodeMirror JavaScript lint test coverage, including access-control and multiple formatting cases for top-level await.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

node-vm now routes dynamic import() through a custom ESM loader, with expanded sandbox coverage for local, builtin, npm, and out-of-root modules. CodeMirror JavaScript linting also suppresses the matching JSHint top-level await import(...) warning and tests the updated behavior.

Changes

Dynamic import support

Layer / File(s) Summary
Loader core
packages/bruno-js/src/sandbox/node-vm/esm-loader.js, packages/bruno-js/src/sandbox/node-vm/utils.js, packages/bruno-js/src/sandbox/node-vm/cjs-loader.js
node-vm adds custom import resolution, local path resolution, module execution and caching helpers, builtin and npm module handling, and shared local-module resolution utilities.
VM script wiring
packages/bruno-js/src/sandbox/node-vm/index.js
runScriptInNodeVm imports createCustomImport and passes it to vm.Script through importModuleDynamically.
Node VM sandbox tests
packages/bruno-js/src/sandbox/node-vm/index.spec.js
The node-vm spec updates the .mjs rejection case and adds dynamic import coverage for local .mjs modules, nested static imports, collection npm dependencies, and blocked out-of-root access.
Lint filter and tests
packages/bruno-app/src/utils/codemirror/javascript-lint.js, packages/bruno-app/src/utils/codemirror/javascript-lint.spec.js
javascript-lint suppresses JSHint E033 for top-level await import(...), and the lint spec covers the dynamic-import cases plus an unrelated syntax error.

Estimated review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • usebruno/bruno#6491: Adds the earlier node-vm custom import plumbing and access-control behavior that this PR extends for dynamic import().

Suggested reviewers

  • helloanoop
  • lohit-bruno
  • bijin-bruno

Poem

A sandbox opened nested doors,
with imports sailing through its cores.
The linter blinked, then let them be,
while tests kept watch from A to Z.
A module breeze now صاف? no—spry and free.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding dynamic ESM import support in node-vm scripts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Cicolas

Cicolas commented Jun 27, 2026

Copy link
Copy Markdown
Author

also tested with in the pre-request script:

const { faker } = await import('@faker-js/faker');

function makeUser() {
  faker.seed(123);

  return {
    id: faker.datatype.uuid(),
    name: faker.name.fullName(),
    email: faker.internet.email().toLowerCase()
  };
}

const user = makeUser();

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bruno-js/src/sandbox/node-vm/index.js`:
- Around line 79-80: Dynamic import handling in node-vm currently bypasses
Bruno’s sandbox module policy by assigning
vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER in the vm setup. Update the sandbox
logic in the node-vm initializer so import() is either blocked for now or routed
through the same Bruno-controlled resolution policy used by require(), enforcing
additionalContextRootsAbsolute before any ESM module load. Use the existing
sandbox setup around scriptOptions.importModuleDynamically and the
module-loading path in the node-vm implementation to keep the behavior
consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 64da074c-ebf7-4b6a-87b7-4deefa5933cf

📥 Commits

Reviewing files that changed from the base of the PR and between 87f7426 and ec9ee26.

📒 Files selected for processing (2)
  • packages/bruno-js/src/sandbox/node-vm/index.js
  • packages/bruno-js/src/sandbox/node-vm/index.spec.js

Comment thread packages/bruno-js/src/sandbox/node-vm/index.js Outdated
@Cicolas Cicolas mentioned this pull request Jun 27, 2026
1 task

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bruno-app/src/utils/codemirror/javascript-lint.js`:
- Around line 17-25: The dynamic import check in isTopLevelDynamicImportError
only matches import( with no whitespace, so it can miss valid spaced forms.
Update the evidence check to accept optional whitespace between import and the
opening parenthesis while keeping the existing E033, a === 'import', and scope
=== '(main)' conditions. Use the same helper function to locate and adjust the
matching logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6d3339dc-71bd-43c5-9feb-8b1be1e488cf

📥 Commits

Reviewing files that changed from the base of the PR and between ec9ee26 and 7cbe557.

📒 Files selected for processing (2)
  • packages/bruno-app/src/utils/codemirror/javascript-lint.js
  • packages/bruno-app/src/utils/codemirror/javascript-lint.spec.js

Comment thread packages/bruno-app/src/utils/codemirror/javascript-lint.js Outdated
Comment thread packages/bruno-js/src/sandbox/node-vm/index.js Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bruno-app/src/utils/codemirror/javascript-lint.js`:
- Around line 73-80: The current E033 suppression in javascript-lint.js only
checks error.evidence on the import line, so multiline await/import() cases are
missed. Update the logic around the TOP_LEVEL_AWAIT_DYNAMIC_IMPORT_PATTERN check
to inspect surrounding source text or adjacent lines in the linting flow, using
the existing javascript-lint.js handling for JSHint errors and the E033/import
branch. Then add a test/spec covering the split-line form like await on one line
and import() on the next to verify the lint error is suppressed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aa4272bc-3905-4668-b08e-43251c880d5e

📥 Commits

Reviewing files that changed from the base of the PR and between 7cbe557 and a53814e.

📒 Files selected for processing (1)
  • packages/bruno-app/src/utils/codemirror/javascript-lint.js

Comment thread packages/bruno-app/src/utils/codemirror/javascript-lint.js
@pull-request-size pull-request-size Bot added size/L and removed size/M labels Jun 27, 2026
@Cicolas

Cicolas commented Jun 28, 2026

Copy link
Copy Markdown
Author

got rid of vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, and wrote a basic customImport function that handles the module creation through vm.Module interfaces

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bruno-js/src/sandbox/node-vm/esm-loader.js`:
- Around line 335-341: The synthetic module in esm-loader.js is re-exporting
CommonJS properties via SyntheticModule while also setting the synthetic default
export, so a CJS object’s own default key can overwrite it. Update the export
loop in the synthetic module callback to skip the default property when
iterating Object.keys(exportsValue), keeping setExport('default', exportsValue)
as the only default binding. Refer to the SyntheticModule setup and its
exportsValue/Object.keys handling to make the change safely.

In `@packages/bruno-js/src/sandbox/node-vm/index.spec.js`:
- Around line 850-875: The current test in node-vm/index.spec.js only covers a
CommonJS dependency being wrapped as a synthetic module, so it does not exercise
the true ESM path. Add a small node_modules fixture in the existing
runScriptInNodeVm test flow that represents a package with ESM syntax and a
package.json marked for module loading, then import it via the same dynamic
import assertion to verify shouldLoadAsEsm() and the collection dependency
resolution path. Use the existing test names and helpers such as
runScriptInNodeVm, collectionPath, and the dynamic import scenario to keep the
new coverage aligned with the PR objective.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7c6cd5fb-ece6-408b-a552-1938ca98533f

📥 Commits

Reviewing files that changed from the base of the PR and between 06842b6 and 5f1b9ae.

📒 Files selected for processing (5)
  • packages/bruno-js/src/sandbox/node-vm/cjs-loader.js
  • packages/bruno-js/src/sandbox/node-vm/esm-loader.js
  • packages/bruno-js/src/sandbox/node-vm/index.js
  • packages/bruno-js/src/sandbox/node-vm/index.spec.js
  • packages/bruno-js/src/sandbox/node-vm/utils.js

Comment thread packages/bruno-js/src/sandbox/node-vm/esm-loader.js
Comment thread packages/bruno-js/src/sandbox/node-vm/index.spec.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants