Skip to content

feat(core): replace esbuild with rolldown for mutator bundling#3628

Open
thesebas wants to merge 3 commits into
orval-labs:masterfrom
thesebas:rolldown
Open

feat(core): replace esbuild with rolldown for mutator bundling#3628
thesebas wants to merge 3 commits into
orval-labs:masterfrom
thesebas:rolldown

Conversation

@thesebas

@thesebas thesebas commented Jun 19, 2026

Copy link
Copy Markdown

Summary

Replace esbuild with rolldown as the runtime bundler used by getMutatorInfo() to parse user-supplied mutator files. This is the only place in the codebase that uses a bundler at runtime — the project's own build pipeline (vp pack) already uses rolldown via vite-plus.

Motivation

getMutatorInfo() invokes a bundler to bundle the user's mutator source file before parsing its AST to determine the mutator's arity (number of parameters). Every orval invocation that uses custom mutators pays this cold-start cost. Rolldown's WASM-based native binding starts 11× faster than esbuild's Go binary, making CLI usage noticeably snappier.

Performance Comparison

All measurements from bun test mutator-info in packages/core/:

Metric esbuild (0.28.0) rolldown (1.0.0) Δ
Cold start (first bundle) 133 ms 12 ms −91%
Subsequent bundles (avg) ~5.6 ms ~1.7 ms −70%
Suite total (31 common tests) 302 ms 196 ms −35%
Test count 31 pass 36 pass (31 + 5 new) +5 tests

Dependency Footprint

Metric esbuild rolldown Δ
JS library code 156 KB 868 KB +712 KB
Native binary (darwin-arm64) 10 MB 19 MB +9 MB
Transitive deps none @oxc-project/types, @rolldown/pluginutils +2

Rolldown is 2× larger on disk but has fewer platform-specific variants (15 vs 26). Rolldown also removes esbuild's native postinstall script (install.js) — its native bindings use standard npm optionalDependencies instead.

Dependency Range

Uses "^1.0.0" — the minimum stable version with all required features. Rolldown is already a transitive dependency of the workspace (via @angular/build at 1.0.0-rc.4), so specifying ^1.0.0 lets the package manager deduplicate naturally while ensuring the features we need are available.

Functionality Verification

  • All 31 existing esbuild tests pass with rolldown — zero regressions.
  • Rolldown supports the same es2015+ target for transformed output via transform.target.
  • The getMutatorInfo() public API is unchanged — consumers are unaffected.
  • Verified with both rolldown 1.0.0 and 1.1.2.

New Capabilities

The rolldown implementation adds explicit support for features that esbuild handled implicitly:

  1. Alias resolution — resolve @mutator-alias imports via the alias option
  2. Wildcard external patterns — externalize *.scss and similar glob patterns
  3. Package subpath externalization — externalize @scope/pkg/subpath given external: ["@scope/pkg"]

These are covered by 5 new test cases in mutator-info.test.ts.

Rollup-compatible API

Rolldown's API is rollup-compatible, returning a bundle instance with .generate() / .write() / .close(). This enables:

  • Proper resource cleanup via try/finally (the esbuild version never called any close-equivalent)
  • Future integration with the rollup plugin ecosystem if needed

Summary by CodeRabbit

  • Tests

    • Expanded mutator import resolution coverage for alias handling, external wildcards, and external package subpaths, including scoped packages.
    • Added/updated fixture type declarations to support importing external subpaths and *.scss.
    • Refreshed the dynamic import() regression notes while keeping the same verification behavior.
  • Chores

    • Updated core bundling tooling by switching from the previous bundler to rolldown, including updated dependency configuration.
    • Preserved existing external/alias behavior during bundling.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a77a9d21-f466-41ff-bb2b-5db3aa4de34b

📥 Commits

Reviewing files that changed from the base of the PR and between 1d6eedf and c048f5b.

📒 Files selected for processing (1)
  • packages/core/src/generators/mutator-info.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/generators/mutator-info.ts

📝 Walkthrough

Walkthrough

Replaces esbuild with rolldown as the bundler in mutator-info.ts. The bundleFile function is rewritten to use rolldown's API, and two new helpers (getExternalOption and isPackagePattern) implement equivalent externalization logic. New test fixtures and test cases cover alias resolution, wildcard external patterns, and package subpath externalization.

Changes

Rolldown migration and new external/alias test coverage

Layer / File(s) Summary
Dependency swap and rolldown bundleFile implementation
packages/core/package.json, packages/core/src/generators/mutator-info.ts
Removes esbuild and adds rolldown ^1.0.0 in package.json. Updates imports in mutator-info.ts to use rolldown and adds normalizeTarget helper for target mapping. Rewrites bundleFile to use rolldown with ESM output, no tree-shaking, and guaranteed finally close. Implements getExternalOption to derive rolldown's external behavior from explicit patterns (with package-style and wildcard handling) or inferred rules. Adds isPackagePattern predicate. Updates parseFile comment from "esbuild output" to "bundled output".
New test fixtures for alias, wildcard, and subpath externalization
packages/core/src/generators/__tests__/mutator-test-files/alias-resolution-tests/*, packages/core/src/generators/__tests__/mutator-test-files/external-wildcard-tests/*, packages/core/src/generators/__tests__/mutator-test-files/external-package-subpath-tests/*, packages/core/src/generators/__tests__/mutator-test-files/call-expression-tests/factory-default-export.ts, packages/core/src/generators/__tests__/mutator-test-files/dynamic-import-tests/dynamic-import-named-export.ts
Adds alias-resolution fixtures with mutation.ts re-exporting via @mutator-alias and mutator-alias.d.ts ambient module. Adds external-wildcard fixtures with mutation.ts importing ./styles.scss and styles.d.ts. Adds external-package-subpath fixtures with mutation.ts, scoped-mutation.ts, and modules.d.ts for both unscoped and scoped packages. Updates two existing fixture comments to remove esbuild-specific wording.
New and updated tests in mutator-info.test.ts
packages/core/src/generators/mutator-info.test.ts
Adds alias-resolution tests with and without explicit external: []. Adds external-wildcard test for *.scss patterns. Adds external-package-subpath tests for unscoped and scoped packages. Updates the dynamic-import regression comment text.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • melloware

Poem

🐇 Hoppin' from esbuild to rolldown's new lane,
With wildcards and subpaths tamed in the chain.
Aliases resolved with a twitch of my nose,
The fixtures all planted in tidy neat rows.
No more esbuild-specific notes in the code —
This bunny has lightened the bundling road! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: replacing esbuild with rolldown for mutator bundling, which aligns with the core objective and the primary file modifications.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@socket-security

socket-security Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedrolldown@​1.0.0911007899100

View full report

@melloware melloware requested review from anymaniax and snebjorn June 19, 2026 14:45
@melloware

Copy link
Copy Markdown
Collaborator

@anymaniax and @snebjorn since you guys are the build masters please give your thoughts!

@melloware melloware added the enhancement New feature or request label Jun 19, 2026
@socket-security

socket-security Bot commented Jun 19, 2026

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

@thesebas

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thesebas thesebas force-pushed the rolldown branch 2 times, most recently from 69df9a7 to 4b6850b Compare June 19, 2026 14:56
@thesebas thesebas marked this pull request as ready for review June 19, 2026 15:05
Copilot AI review requested due to automatic review settings June 19, 2026 15:05

Copilot AI 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.

Pull request overview

This PR replaces esbuild with rolldown for runtime bundling inside getMutatorInfo() (used to bundle user-supplied mutator files before parsing their AST), aiming to reduce CLI cold-start overhead while preserving the existing public API.

Changes:

  • Swapped the runtime bundler implementation in bundleFile() from esbuild to rolldown, adding proper bundle.close() cleanup.
  • Added alias resolution and improved external handling (wildcard patterns + package subpath externalization) with new test coverage and fixtures.
  • Updated packages/core dependencies to remove esbuild and add rolldown.

Reviewed changes

Copilot reviewed 9 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/core/src/generators/mutator-info.ts Replaces esbuild bundling with rolldown, adds external-pattern matching and alias-aware externalization, and ensures bundle cleanup.
packages/core/src/generators/mutator-info.test.ts Adds new tests covering alias resolution and expanded external semantics.
packages/core/src/generators/tests/mutator-test-files/external-wildcard-tests/styles.d.ts Adds TS module declaration for *.scss fixture usage.
packages/core/src/generators/tests/mutator-test-files/external-wildcard-tests/mutation.ts Adds fixture importing ./styles.scss to validate wildcard externals.
packages/core/src/generators/tests/mutator-test-files/external-package-subpath-tests/scoped-mutation.ts Adds fixture importing a scoped package subpath to validate subpath externalization.
packages/core/src/generators/tests/mutator-test-files/external-package-subpath-tests/mutation.ts Adds fixture importing an unscoped package subpath to validate subpath externalization.
packages/core/src/generators/tests/mutator-test-files/external-package-subpath-tests/modules.d.ts Adds TS module declarations for external-package subpath fixtures.
packages/core/src/generators/tests/mutator-test-files/dynamic-import-tests/dynamic-import-named-export.ts Updates fixture comments to be bundler-agnostic.
packages/core/src/generators/tests/mutator-test-files/call-expression-tests/factory-default-export.ts Updates fixture comments to be bundler-agnostic.
packages/core/src/generators/tests/mutator-test-files/alias-resolution-tests/mutator-alias.d.ts Adds TS module declaration for alias-resolution fixture.
packages/core/src/generators/tests/mutator-test-files/alias-resolution-tests/mutation.ts Adds alias-resolution fixture that re-exports from the alias.
packages/core/package.json Removes esbuild, adds rolldown.
bun.lock Updates lockfile to reflect dependency changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/generators/mutator-info.ts
Comment on lines +52 to +54
transform: {
target: compilerOptions?.target ?? 'es2015',
},

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

🧹 Nitpick comments (1)
packages/core/src/generators/mutator-info.ts (1)

79-81: ⚡ Quick win

Precompute wildcard matchers once per external option.

matchesExternal creates a new RegExp per resolver call, and this path runs for every import resolution. Prebuilding predicates once in getExternalOption will reduce repeated allocations in a performance-sensitive path.

♻️ Suggested refactor
 function getExternalOption(
   entry: string,
   external?: string[],
   alias?: Record<string, string>,
 ): ExternalOption {
   if (external) {
-    return (id) => external.some((pattern) => matchesExternal(pattern, id));
+    const matchers = external.map((pattern) => {
+      if (isPackagePattern(pattern)) {
+        return (id: string) => id === pattern || id.startsWith(`${pattern}/`);
+      }
+
+      if (!pattern.includes('*')) {
+        return (id: string) => id === pattern;
+      }
+
+      const regex = new RegExp(
+        `^${pattern
+          .replaceAll(/[$()+./?[\\\]^{|}]/g, String.raw`\$&`)
+          .replaceAll('*', '.*')}$`,
+      );
+      return (id: string) => regex.test(id);
+    });
+
+    return (id) => matchers.some((match) => match(id));
   }
@@
-function matchesExternal(pattern: string, id: string): boolean {
+function matchesExternal(pattern: string, id: string): boolean {

Also applies to: 100-104

🤖 Prompt for 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.

In `@packages/core/src/generators/mutator-info.ts` around lines 79 - 81, The
return function in the external options handler is calling matchesExternal
inside the some() callback for every import resolution, causing new RegExp
objects to be created repeatedly. Move the regex pattern compilation outside
this loop by precomputing the matchers once when the external option is
initially processed in getExternalOption, then use the precomputed matchers in
the returned function instead of calling matchesExternal on each resolver call.
This will eliminate the repeated RegExp allocations in this performance-critical
path.
🤖 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.

Nitpick comments:
In `@packages/core/src/generators/mutator-info.ts`:
- Around line 79-81: The return function in the external options handler is
calling matchesExternal inside the some() callback for every import resolution,
causing new RegExp objects to be created repeatedly. Move the regex pattern
compilation outside this loop by precomputing the matchers once when the
external option is initially processed in getExternalOption, then use the
precomputed matchers in the returned function instead of calling matchesExternal
on each resolver call. This will eliminate the repeated RegExp allocations in
this performance-critical path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2fd00b9d-7234-4a35-8dbc-43f2b6ac86c6

📥 Commits

Reviewing files that changed from the base of the PR and between ac14578 and f53855d.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • packages/core/package.json
  • packages/core/src/generators/__tests__/mutator-test-files/alias-resolution-tests/mutation.ts
  • packages/core/src/generators/__tests__/mutator-test-files/alias-resolution-tests/mutator-alias.d.ts
  • packages/core/src/generators/__tests__/mutator-test-files/call-expression-tests/factory-default-export.ts
  • packages/core/src/generators/__tests__/mutator-test-files/dynamic-import-tests/dynamic-import-named-export.ts
  • packages/core/src/generators/__tests__/mutator-test-files/external-package-subpath-tests/modules.d.ts
  • packages/core/src/generators/__tests__/mutator-test-files/external-package-subpath-tests/mutation.ts
  • packages/core/src/generators/__tests__/mutator-test-files/external-package-subpath-tests/scoped-mutation.ts
  • packages/core/src/generators/__tests__/mutator-test-files/external-wildcard-tests/mutation.ts
  • packages/core/src/generators/__tests__/mutator-test-files/external-wildcard-tests/styles.d.ts
  • packages/core/src/generators/mutator-info.test.ts
  • packages/core/src/generators/mutator-info.ts

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

🧹 Nitpick comments (1)
packages/core/src/generators/mutator-info.ts (1)

87-102: 💤 Low value

ReDoS risk is minimal but consider documenting the assumption.

The static analysis flagged a potential ReDoS (CWE-1333) due to regex construction from variable input. However, the risk is low here because:

  1. The external patterns come from developer-controlled build configuration, not untrusted user input
  2. Patterns are typically simple globs like *.scss or lodash/*
  3. Module specifier strings being tested are short

The escaping covers all necessary regex special characters. No change is strictly required, but consider adding a brief code comment noting that patterns are assumed to be developer-configured build options.

🤖 Prompt for 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.

In `@packages/core/src/generators/mutator-info.ts` around lines 87 - 102, Add a
brief code comment above the matchers variable assignment in the function that
constructs the external pattern matchers, documenting the security assumption
that the external patterns come from developer-controlled build configuration
rather than untrusted user input. This will help future maintainers understand
the context of the regex construction and why the ReDoS risk is acceptable in
this specific case.

Source: Linters/SAST tools

🤖 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.

Nitpick comments:
In `@packages/core/src/generators/mutator-info.ts`:
- Around line 87-102: Add a brief code comment above the matchers variable
assignment in the function that constructs the external pattern matchers,
documenting the security assumption that the external patterns come from
developer-controlled build configuration rather than untrusted user input. This
will help future maintainers understand the context of the regex construction
and why the ReDoS risk is acceptable in this specific case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f0de1440-13a8-4971-9e8f-e4268db7573b

📥 Commits

Reviewing files that changed from the base of the PR and between f53855d and 1d6eedf.

📒 Files selected for processing (1)
  • packages/core/src/generators/mutator-info.ts

@melloware

Copy link
Copy Markdown
Collaborator

Build still failing.

@anymaniax

Copy link
Copy Markdown
Collaborator

Thanks for the PR and the benchmarks @thesebas 🙏

My main concern is the risk of side effects. Swapping the bundler also swaps the downleveling engine (oxc vs esbuild), and our mutator arity detection depends on how async/modern syntax gets lowered for a given tsconfig target. Different lowering can produce different generated client output in subtle ways, and for a codegen tool that kind of silent change is risky and hard to fully cover with tests.

On top of that it adds a lot of dependency footprint — a noticeably bigger bundler shipped to everyone — while the gain is small and only benefits users with custom mutators (~120 ms of cold start). So for me the trade-off isn't really worth it right now.

The new tests and the bundle.close() cleanup are genuinely nice though — would you be open to splitting those into a separate PR on the current esbuild path?

@snebjorn @melloware wdyt?

@melloware

Copy link
Copy Markdown
Collaborator

I agree with your assessment the trade offs aren't big enough

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants