Oxc and Rolldown currently duplicate the N-API conversion between a JavaScript `RegExp` and owned Rust data.
This moves that conversion to `oxc_napi::JsRegExp`. It copies `source`, `flags`, and `lastIndex`, supports converting back to a JavaScript `RegExp`, and keeps regex compilation in each consumer. `oxc_minify_napi::JsRegExp` remains available as a re-export.
This is a good place to put since Rolldown already used `oxc_napi` and crate is exactly for napi related struct
This follows up on #24741 and addresses API feedback received after it was merged.
### Feature
`mangleProps.include` and `mangleProps.exclude` now accept JavaScript `RegExp` values instead of strings.
The binding copies the regular expression source and flags before async minification starts, then compiles the pattern once with Rust's regex engine. Native and WASI bindings expose the same API. The core Rust minifier API is unchanged.
### Example
```js
mangleProps: {
include: /^_/,
exclude: /^__public/,
}
```
### Limitation
Matching uses Rust regex syntax. The `i`, `m`, `s`, and `u` flags are supported. Unsupported JavaScript syntax or flags are returned as option errors.
### AI assistance
OpenAI Codex was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
The property mangler added in #24740 is not available through `oxc-minify`. This adds `mangleProps` for configuring it and `mangleCache` for reusing chosen property names.
### Feature
- `include` selects property names to mangle. `exclude` and `reserved` keep names unchanged.
- `quoted` also mangles quoted property names. `debug` produces readable output names.
- `cache` fixes a property to a chosen name. A `false` value keeps the original name.
- Property mangling works in both sync and async APIs. It is separate from identifier mangling.
- When property mangling is enabled and the call succeeds, the result includes the updated `mangleCache`.
- Invalid `mangleProps` regexes and cache entries are returned in `result.errors`.
### Design
The binding converts JavaScript values into core minifier options and results. The core minifier still chooses names and rewrites properties, so both APIs follow the same rules.
A parser error may leave an incomplete AST. In that case, property mangling is skipped and no partial cache is returned.
### Example
```js
const result = minifySync(
"test.js",
"let local; obj._often; obj._rare; obj._often;",
{
compress: false,
mangle: false,
mangleProps: { include: "^_" },
},
);
result.code; // "let local;obj.e;obj.t;obj.e;"
result.mangleCache; // { _often: "e", _rare: "t" }
```
### Limitation
- `include` and `exclude` use Rust regex syntax. They match anywhere unless the pattern includes anchors.
- Do not reuse the cache on already-mangled output. A mapping is not always safe to apply twice.
- The cache only remembers recorded mappings. It cannot find unchanged names in another input or in external code. Reserve, exclude, or pin those names before the first call.
- String cache targets must be valid JavaScript `IdentifierName` values and cannot be `__proto__`, `constructor`, or `prototype`. The original name `__proto__` cannot be a cache key.
AI assistance: OpenAI Codex and Claude were used for research, implementation, adversarial review, and verification. The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
Oxc can shorten variable names and private class names, but it cannot shorten property names such as `obj._field`. This change adds optional property mangling. It replaces #23772 and gives bundlers a way to use the same property names across several files.
### Feature
- `include` selects names to mangle. `exclude` and `reserved` keep names unchanged.
- The cache can give a property a fixed output name or keep it unchanged.
- The mangler supports member access, object and class keys, destructuring, optional chains, the left side of `in`, and JSX property names.
- `mangle_quoted` also handles strings and template literals used directly as property keys. `/* @__KEY__ */` and `/* #__KEY__ */` can mark one other string or template literal as a property key.
- Short names are assigned in a stable order. More common properties get shorter names. Existing and reserved names are avoided.
- Bundlers can collect names from several programs, create one shared map, and apply it to every program.
- Numeric keys, `__proto__`, `constructor`, `prototype`, TypeScript-only keys, and private names are never changed by this pass.
### Design
Property names are part of JavaScript syntax, not local scope. For this reason, the code lives in `oxc_minifier`. It only reuses the short-name generator from `oxc_mangler`.
For one program, the order is:
```text
collect names → assign names → rewrite once → compress → mangle variables and private names
```
Property names are rewritten before compression because compression can remove information about quotes and comments.
For several programs, a bundler collects names from each program, merges the results, assigns one shared map, and rewrites each program once. Collection can run in parallel. All programs must use the same options.
The shared map keeps property names consistent inside one build. The cache has a different job: it keeps fixed names and reservations across separate minify calls.
### Example
With `include: "^_"` and `mangle_quoted: true`:
```js
obj[`_field`] = 1;
obj._field = 2;
```
becomes:
```js
obj[`e`] = 1;
obj.e = 2;
```
### Limitation
- Property mangling is unsafe when other code still uses the old property name. Public APIs and names shared with external code, host APIs, imported namespace objects, or separate builds must be excluded or reserved.
- Quote handling is per use. Without `mangle_quoted`, `obj._field` may change while `obj["_field"]` does not.
- The mangler cannot find names built at runtime or stored in arbitrary strings. Code that uses `eval`, `Function`, or `with` must reserve any affected names.
- Compression may create a matching property after this pass has finished. For example, `'f' + 'oo_'` may later become `foo_` and will not be mangled.
- TypeScript enum reverse mappings are not supported because the transform creates quoted strings.
- Multi-program support is a caller API. The caller must collect every program that shares mangled properties before assigning names.
The public `oxc-minify` API is stacked in #24741. The downstream multi-chunk integration is shown in rolldown/rolldown#10374.
Supersedes #23772.
AI assistance: OpenAI Codex and Claude were used for research, implementation, review, and testing. The contributor is responsible for reviewing and understanding the changes before submission.
Adds `MangleOptions::reserved` — equivalent to terser / uglify-js / swc `mangle.reserved`: names in the set are never used as mangled names, and bindings that already carry them keep them. Default empty; default output is byte-identical to main (minsize snapshot unchanged).
## Motivation
Node's cjs-module-lexer detects a CommonJS module's named exports by **lexically** scanning for `exports.<name> =` / `module.exports` token patterns — no scope analysis ([its README documents renamed identifiers as "DETECTS: NO EXPORTS"](https://github.com/nodejs/cjs-module-lexer)). UMD / CommonJS wrappers bind `exports` / `module` as ordinary function parameters, and the mangler renames them, erasing every named export the lexer can see.
Surfaced by the monitor-oxc mangler runtime test ([failing run](https://github.com/oxc-project/monitor-oxc/actions/runs/28569108563/job/84702922472), previously masked by #24033 / #24036): `archiver` does `import { queue } from "async"`, and `async`'s dist is a UMD bundle — mangled, the factory parameter `exports` became `e` and node fails with `Named export 'queue' not found`. With `reserved: ["exports", "module"]` the output imports correctly (verified end-to-end in node, including running a queue task).
## Why an opt-in list instead of always keeping these names
Renaming them is ecosystem-standard. Verified empirically — minifying the same UMD sample and importing it in node:
| minifier | renames `exports` param? | `import { queue }` from its output | escape hatch |
| --- | --- | --- | --- |
| terser 5.48 | yes | fails | `mangle.reserved` (verified working) |
| uglify-js 3.19 | yes | fails | `reserved` (verified working) |
| esbuild 0.28 `--minify` | yes | fails | none for identifier mangling |
| swc | yes | fails | terser-style `mangle.reserved` ([swc#3424](https://github.com/swc-project/swc/issues/3424)) |
An earlier revision of this PR kept `exports` / `module` unconditionally; the Minification Size check showed the real cost: ~1% minified on UMD-heavy files (d3: +3.3 kB from ~590 `exports` references, flipping it to *larger than esbuild*), because the point is keeping the literal 7-char token at every export site. No other minifier pays this by default — and rolldown bundles bind `exports` / `module` in every CJS interop wrapper, where node's lexer never looks. So oxc gets the same escape hatch as terser/swc, with the same name.
`MangleOptions` loses `Copy` (it now holds the set); construction sites switch to `..MangleOptions::default()` / `clone()`. Exposed in `oxc-minify` NAPI options (`mangle.reserved: string[]`) and the mangler example (`--reserve-exports`).
monitor-oxc will pass `["exports", "module"]` for its mangler runtime test (its corpus imports prebuilt CJS/UMD dists directly).
For reference, esbuild solves this for its *own* node-platform CJS bundles differently — a dead-code annotation `0 && (module.exports = { a, b })` that cjs-module-lexer recognizes ([esbuild#960](https://github.com/evanw/esbuild/issues/960), [cjs-module-lexer#44](https://github.com/nodejs/cjs-module-lexer/issues/44)); verified that pattern also works appended to fully-mangled output. That could be a future zero-size-cost alternative at the minifier level.
## Summary
Introduces `oxc_diagnostics::Diagnostics` — a `Vec<OxcDiagnostic>` newtype that knows the difference between errors and warnings — and makes it the standard diagnostics return value across oxc.
- **New type**: `Diagnostics` with `has_errors()`, `has_warnings()`, `errors()`, `warnings()`, plus `Vec`-like ergonomics (Deref, `IntoIterator`, `Extend`, `From`/`Into<Vec>`) so most call sites are unchanged.
- **Return types**: `ParserReturn`, `SemanticBuilderReturn`, `IsolatedDeclarationsReturn`, the React Compiler `TransformResult`/`LintResult`, and `TransformerReturn` now expose `diagnostics: Diagnostics` instead of `errors: Vec<OxcDiagnostic>` (the React Compiler's separate `errors`/`warnings` are merged into one severity-tagged list).
- **Behaviour**: a React Compiler **error** now stops the transform (no output), while **warnings** flow through and the transform continues. `compile()` aborts codegen only when `diagnostics.has_errors()` — warnings alone no longer abort (previously any diagnostic did).
## Breaking change
Consumers reading `*.errors` on these return values must read `*.diagnostics`. React Compiler consumers that read `.errors`/`.warnings` separately should use `diagnostics.has_errors()` / `diagnostics.errors()` / `diagnostics.warnings()`.
Parser/semantic/isolated-declarations fatality is otherwise unchanged. Builds on the React Compiler transform feature (#23201).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Add `legalComments` and `legalCommentsPath` fields to the NAPI `CodegenOptions` struct, accepting `"none"`, `"inline"`, `"eof"`, `"linked"`, `"external"`
- Add `legalComments: Vec<String>` to `MinifyResult`, populated from `CodegenReturn::legal_comments` when using `"linked"` or `"external"` mode
- Replace the `From<&CodegenOptions>` impl with a `to_codegen_options()` method that validates and maps string values to `oxc_codegen::LegalComment` variants
This enables downstream consumers (e.g. `oxc-webpack-plugin`) to use oxc's built-in legal comment extraction instead of regex-based post-processing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Add `property_write_side_effects` option to `TreeShakeOptions` (default `true`). When set to `false`, property assignments like `A.from = () => {}` are considered side-effect-free, enabling the minifier to drop declarations and fresh value bindings with property assignments when unused.
### Examples
```js
// Function declaration + property assignments → eliminated
function A() {}
A.from = () => {}
A.to = () => {}
// Output: (empty)
// Class declaration + property assignment → eliminated
class A {}
A.foo = 1
// Output: (empty)
// Const variable with arrow function → eliminated
const A = () => {}
A.foo = 1
// Output: (empty)
// Const variable with object literal → eliminated
const B = {}
B.foo = 1
// Output: (empty)
// Const variable with function expression → eliminated
const C = function() {}
C.foo = 1
// Output: (empty)
```
### How it works
1. Wires the existing `property_write_side_effects` from `oxc_ecmascript`'s `MayHaveSideEffectsContext` through the minifier's compress options
2. For member expression assignments, checks `may_have_side_effects()` then `is_member_assign_to_unused_binding()`
3. `is_member_assign_to_unused_binding` verifies:
- Single-level member expression only (`A.foo`, not `a.b.c`)
- All references to the symbol have the `MemberWriteTarget` flag (from parent PR)
- Symbol creates a fresh value (`is_fresh_value`) and is not exported
4. Fixed-point loop handles cascading: first pass drops assignments, `exit_program` cleans up references, second pass drops declarations
### Fresh value detection
A symbol is considered "fresh" (cannot alias another binding and property writes are side-effect-free) if it is:
- A **function declaration**
- A **class declaration** without static setters, static accessor properties, or static property definitions with values
- A variable initialized with an **array/arrow function/function expression** literal
- A variable initialized with an **object literal** without setter/getter properties or property values containing setters/getters
- A variable initialized with a **class expression** (same rules as class declarations above)
Non-fresh values (e.g. `const b = a`) are conservatively preserved to prevent breaking aliased references.
#### Setter/getter detection (addresses review feedback)
Following how esbuild, Terser, Rollup, and SWC handle setter cases — all tools preserve property writes when setters are visible in the AST:
- **Static setter methods**: `class A { static set foo(v) { ... } }` → not fresh
- **Static accessor properties**: `class A { static accessor foo = 0 }` → not fresh (auto-generates getter+setter)
- **Static property definitions with values**: `class A { static b = 0 }` → not fresh (matches SWC's conservative approach)
- **Static property values containing setters/getters**: `class A { static b = { set x(v) { ... } } }` → not fresh
- **Object literal setters/getters**: `{ set foo(v) { ... } }` or `{ get foo() { ... } }` → not fresh
- **Nested setters/getters in object property values**: `{ bar: { set x(v) { ... } } }` → not fresh
- **Non-static setters are fine**: `class A { set foo(v) { ... } }` → fresh (instance setters don't affect class-level property writes)
### Limitations
**Only single-level member expressions are supported** (`A.foo`, not `a.b.c`). Deep nested assignments like `a.b.c = 1` are always preserved because intermediate properties could alias external objects:
```js
const exported = {};
const a = { b: exported };
a.b.c = 1; // writes to exported.c — unsafe to drop!
export { exported };
```
Supporting deeper nesting would require full object graph tracking (like Rollup's `ObjectEntity` model), which is a much larger effort for marginal gain.
### Safety guards
Addresses concerns raised in [PR #20730 review](https://github.com/oxc-project/oxc/pull/20730#pullrequestreview-4011376657) and [comment](https://github.com/oxc-project/oxc/pull/20730#issuecomment-4131710117):
- **Aliases**: `const b = a; b.add = 1; export { a }` → kept (not fresh, could mutate exported `a` through alias)
- **Chained access**: `const b = { a }; b.a.add = 1; export { a }` → kept (only single-level member expressions allowed)
- **Exported symbols**: `export function A() {} A.foo = 1;` → kept (observable by importers)
- **Other reads**: `function A() {} A.foo = 1; console.log(A)` → kept (symbol has non-member-write references)
- **Side-effectful RHS**: `function A() {} A.foo = sideEffect()` → kept (`may_have_side_effects` returns true)
- **Globals**: `globalObj.foo = 1` → kept (no local binding)
- **Script mode top-level**: top-level bindings in script mode are kept (they're globals visible to other scripts)
- **Setters**: `class A { static set foo(v) { ... } } A.foo = 1` → kept (setter triggers side effects)
- **Static properties**: `class A { static b = 0 } A.b = 1` → kept (matches SWC's conservative approach)
### Comparison with other tools
| Tool | Approach | Option needed? |
|------|----------|---------------|
| **Rollup** | Structural aliasing analysis — tracks object identity through `LocalVariable.isReassigned` + `ObjectEntity` setter detection. Drops `A.foo = 1` automatically when `A` is a local unreassigned object with no setters. | No (always active) |
| **Rolldown** | Adds explicit [`treeshake.propertyWriteSideEffects`](https://rolldown.rs/reference/InputOptions.treeshake#propertywritesideeffects) option (default `'always'`). Not in upstream Rollup. | Yes |
| **Terser** | Checks `is_constant_expression` on the base object after unwrapping property chain. If the base is constant within scope, drops the assignment. | No (always active, but less precise) |
| **esbuild** | All property assignments always preserved. Rationale: `Object.defineProperty` could install prototype setters. Only escape hatch is `/* @__PURE__ */` IIFE wrapping. | No option available |
| **SWC** | `may_have_side_effects` returns `true` for all assignments unconditionally. Classes with static property definitions are conservatively preserved. | No option available |
| **Oxc (this PR)** | Fresh value tracking (`is_fresh_value`) + `MemberWriteTarget` reference flags + explicit option. Closest to Rollup's aliasing analysis but gated behind an option like Rolldown. | Yes (`property_write_side_effects: false`) |
Closes#14207
## Test plan
- [x] 20 test cases covering all safety scenarios including setter/getter edge cases
- [x] All minifier tests pass (476/476)
- [x] No minsize regressions
- [x] Clippy clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Disable mimalloc as the global allocator on Windows for all NAPI packages (`oxc-parser`, `oxc-minify`, `oxc-transform`)
- On Windows, packages now use the system allocator instead
Fixes#15518
## Root Cause
mimalloc has known issues with DLL unloading on Windows when the DLL is loaded by the main thread but unloaded by a worker thread ([mimalloc issue #288](https://github.com/microsoft/mimalloc/issues/288)). The cleanup code has `_mi_is_main_thread()` checks that prevent proper memory cleanup when called from a non-main thread, causing `STATUS_ACCESS_VIOLATION` crashes.
## Platforms with mimalloc enabled
| Platform | mimalloc |
|----------|----------|
| macOS | ✅ |
| Linux x86_64 | ✅ |
| Linux aarch64 | ✅ |
| Windows | ❌ (disabled) |
| FreeBSD | ❌ |
| ARM 32-bit | ❌ |
| WASM | ❌ |
🤖 Generated with [Claude Code](https://claude.ai/code)
feat(napi/minify): expose `treeshake` options
## Completed:
- [x] Understand the problem: Need to expose TreeShakeOptions to JavaScript users
- [x] Explore existing code structure and TreeShakeOptions in oxc_minifier
- [x] Build the project to ensure clean state
- [x] Add TreeShakeOptions struct with all fields
- [x] Update CompressOptions to accept optional treeshake field
- [x] Implement conversion from NAPI TreeShakeOptions to oxc_minifier::TreeShakeOptions
- [x] Build and verify TypeScript definitions are generated correctly
- [x] Add tests for treeshake options
- [x] Run all tests successfully
- [x] Format code with just fmt
- [x] Address all review feedback:
- Renamed TreeShakeOptionsNapi to TreeShakeOptions
- Changed property_read_side_effects to accept boolean | 'always'
- Changed @default from true to 'always' for property_read_side_effects
- Reverted From implementation back to use reference (From<&TreeShakeOptions>)
- Changed to TryFrom to properly handle validation errors
- Added validation for propertyReadSideEffects string values
- Fixed clippy lint for inline format args
## Changes Made:
1. Added `TreeShakeOptions` struct with fields:
- `annotations`: Whether to respect pure annotations (@__PURE__, #__NO_SIDE_EFFECTS__)
- `manual_pure_functions`: List of function names to treat as pure
- `property_read_side_effects`: Whether property reads have side effects (boolean | 'always', default: 'always')
- `unknown_global_side_effects`: Whether accessing global variables has side effects
2. Updated `CompressOptions` to include optional `treeshake` field
3. Implemented `TryFrom<&TreeShakeOptions>` trait with proper error handling for invalid values
4. Added validation that rejects invalid string values for propertyReadSideEffects
5. Added tests covering treeshake functionality including error cases
6. Fixed all lint issues
All tests pass successfully!
<!-- START COPILOT CODING AGENT SUFFIX -->
<details>
<summary>Original prompt</summary>
> Add bindings for treeshake option here:
> https://github.com/oxc-project/oxc/blob/3fbb307367a31817a297dee299fec580912675db/napi/minify/src/options.rs#L90
</details>
<!-- START COPILOT CODING AGENT TIPS -->
---
💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).
fixes https://github.com/rolldown/rolldown/issues/4118fixes#10626
At present, there is no option to keep legal comments when minification is turn on.
This PR allows Codegen to set legal comments with minfication turned on, with the following behavior:
* Inline: only if the attached code is not removed
* Eof: move all legal comments to the end of file, regardless of where they appear
* Linked and External: saved into `CodegenReturn::legal_comments` for the caller to use, regardless of where they appear
Code gets moved around or removed if minifier is turned on, so some legal comments may disappear from the final output.
The recommended legal comment option is Eof.
---
For Rolldown to keep legal comments, it should set `LegalComment::Inline` for chunk codegen and `LegalComment::Eof` for minification codegen.
Added support for `keep_names: { function: true, class: true }`.
Setting `.name` is makes it difficult to treeshake later on (https://github.com/evanw/esbuild/issues/3965), so it is better to avoid if possible.
This PR only adds support for the compressor. I'll add support for the mangler later on (probably first add required information to the semantics).
refs #9711
Replace `#[allow]` with `#[expect]` through the whole codebase.
This also surfaced many places where the `#[allow]` attributes were outdated/pointless and could be removed entirely.
Only places `#[allow]` remains are:
1. In generated code and macros, where lint errors may or may not be triggered depending on the generated code.
2. A few places where clippy malfunctions. These use e.g. `#[allow(unused, clippy::allow_attributes)]`.
It seems to be that the easiest way to rename variables is to use
rename_symbol from SymbolTable and to let it be applied during codegen -
this is what Mangler does.
This PR changes Codegen to accept (any) SymbolTable instead of just
Mangler itself, and for Mangler build output to be a new SymbolTable,
after consuming itself (which makes it clear at the type level if a
Mangler has been built or not).
This also moves the few symbol table helper methods from Mangler to
SymbolTable itself, and adds an example of Mangler use to it's
documentation.
Codegen dependencies now include semantic (which was transient by
mangler before). It's dependency on Mangler could be removed, though
I've left it to allow easy linking of docs (with_symbol_table points
users to Mangler, hopefully helps with migration?)
Adds `top_level` option which is similar to [terser's `toplevel`
option](https://terser.org/docs/cli-usage/#cli-mangle-options).
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Pure refactor. Re-order imports for clarity:
1. `std`
2. External crates
3. `oxc_*` crates
4. Current crate `use crate::...`
5. Super `use super::...`
6. Local modules
This order is from "furthest away" to "closest". This makes it clearer to see what is coming from where.
`cargo +nightly fmt` (#7877) did a lot of the work, but unfortunately `rustfmt` does not have an option to (a) put workspace crates in a separate block from external crates and (b) move `mod` statements to after `use` statements.