#15232 updated `vitest` package, which caused changes in transform conformance snapshots, which were not committed. Updated snapshots so conformance passes again.
- [x] Explore repository structure and understand the issue
- [x] Identify the bug in enum.rs (lines 430-434) - missing
number-to-string conversion in template literals
- [x] Create test case for enum template literal with numbers
- [x] Fix the bug by converting numbers to strings in template literal
evaluation
- [x] Verify the fix with manual testing (regular numbers, mixed types,
negative numbers, floats, Infinity, NaN)
- [x] Run unit tests - all passing
- [x] Run code review - reviewed, minor comment about exhaustiveness
(already handled)
- [x] Run security check - no issues found
- [x] Update transform conformance snapshots per CI feedback
- [x] Fix template literal quasi bug (use enumerate instead of zip)
- [x] Add test case for trailing quasi edge case
- [x] Task completed
## Summary
Fixed a bug where numeric enum values in TypeScript were not converted
to strings when used in template literals. The fix converts
`ConstantValue::Number` to strings using `to_js_string()` method.
Fixed an additional bug where the last quasi (suffix) in template
literals was being dropped due to using `zip` instead of properly
iterating over all quasis with conditional expression access.
Updated the transform conformance snapshots to include both test cases.
## Security Summary
No security vulnerabilities were found in the changes.
<!-- START COPILOT CODING AGENT SUFFIX -->
<details>
<summary>Original prompt</summary>
>
> ----
>
> *This section details on the original issue you should resolve*
>
> <issue_title>transformer: computed enum with number in string template
is incorrect</issue_title>
>
<issue_description>[playground](https://playground.oxc.rs/#eNptVMFOGzEQ/ZXI6gGkgELggFL1UAGRKgFJC5RLpOLdnQSD197aswlotf/eZyfeTQWneN945s28eU4jcjER9FZZxwMydTm4rcuM3FU4NgszGNw+3Pw5GXwbnIxGo2ECxgDG+8ApgNN94AzA2RZoF2Zh9ikubFnVTEVPcjG7mT/cX11GpqcvTd/EceRvjz5g4/YpknWpoaePqaefpJ7F1NhWbo23mo61XR3MshfK+XgtdU3+YL/Jw8OvCyOGwopJI1xtwo9WhsVkKbWnoVhaV0pmch3CThofYDFhVwNQ4JGod0m5lk6yAnN3e/OsmHwlc+qgUpqV7j9ztOPI9yk+txXC2+L+vcysTl/5crW71g5FJZ0PfTXYMpPx4MXG2b9hHqm13fwirp2Z1exVQdPa5KG1VCpwklvTXDrkJjTm/T7/YdgpVMy7gCe0zSq/cs66HYoeglaxB5z3pGpE7eleZv1QLLNHVfCzmIyHgkwxW14rgymFXqLdCmy8i5+PwKaCRD9ry71OL/7t7hP4b/iaO1uBS0h/ZIgKKlATe1Job4V1lzLEtAaKOVQ3LGbZpPkR38h3jyuZk/kr8R2WhvR0OaGypG3nO34bvfXoZIUaSdRAFHv9zhAyg9vm5P5Lw6shp0oyLPUMR8nWza1X2xUFhQQk7by2FZWlWxG8Kcjj2TFYIPMlLVF4at2Flt5PFemi29o+C+xpI00fLRV36A2xLCRDqd1qP7d1IzwmqmAQckbCmMmNuS1oRfEBmWCEzrMvvrD4K9pJbozlWCshmlbpbqySHkNwVHopmNxW17SmrugrUXWLVSSDxVSY1uopDBwy1uQy65PgYR4TFhVC3akFXET5Atyd2oAHmdNM7T8OrNw+)
> Original report: https://github.com/vitejs/rolldown-vite/issues/472
>
> ### Input
> ```ts
> export enum NumberEnum {
> NUM_1 = 1000,
> NUM_2 = 2000,
> NUM_3 = 3000,
> NUM_4 = 4000,
> }
>
> export enum ComputedEnum {
> COMPUTED_1 = `${NumberEnum.NUM_1}-${NumberEnum.NUM_2}`,
> COMPUTED_2 = `${NumberEnum.NUM_3}-${NumberEnum.NUM_4}`,
> }
>
> console.log(Object.values(ComputedEnum));
> ```
> ### Actual
> ```js
> export let NumberEnum = /* @__PURE__ */ function(NumberEnum) {
> NumberEnum[NumberEnum["NUM_1"] = 1e3] = "NUM_1";
> NumberEnum[NumberEnum["NUM_2"] = 2e3] = "NUM_2";
> NumberEnum[NumberEnum["NUM_3"] = 3e3] = "NUM_3";
> NumberEnum[NumberEnum["NUM_4"] = 4e3] = "NUM_4";
> return NumberEnum;
> }({});
> export let ComputedEnum = /* @__PURE__ */ function(ComputedEnum) {
> ComputedEnum["COMPUTED_1"] = "-";
> ComputedEnum["COMPUTED_2"] = "-";
> return ComputedEnum;
> }({});
> console.log(Object.values(ComputedEnum));
> ```
>
> ### Expected
> ```js
> export let NumberEnum = /* @__PURE__ */ function(NumberEnum) {
> NumberEnum[NumberEnum["NUM_1"] = 1e3] = "NUM_1";
> NumberEnum[NumberEnum["NUM_2"] = 2e3] = "NUM_2";
> NumberEnum[NumberEnum["NUM_3"] = 3e3] = "NUM_3";
> NumberEnum[NumberEnum["NUM_4"] = 4e3] = "NUM_4";
> return NumberEnum;
> }({});
> export let ComputedEnum = /* @__PURE__ */ function(ComputedEnum) {
> ComputedEnum["COMPUTED_1"] = "1000-2000";
> ComputedEnum["COMPUTED_2"] = "3000-4000";
> return ComputedEnum;
> }({});
> console.log(Object.values(ComputedEnum));
> ```
> </issue_description>
>
> <agent_instructions>We should take a look at
./crates/oxc_transformer/src/typescript/enum.rs to check if there is a
bug</agent_instructions>
>
> ## Comments on the Issue (you are @copilot in this section)
>
> <comments>
> </comments>
>
</details>
- Fixesoxc-project/oxc#15182
<!-- START COPILOT CODING AGENT TIPS -->
---
💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Dunqing <29533304+Dunqing@users.noreply.github.com>
* Fixes: #14493
Getter and setter methods should not have `design:returntype` metadata according to TypeScript's `emitDecoratorMetadata` behavior. They should only have `design:type` and `design:paramtypes`.
**And `design:type` is different for the getter and setter:**
Getter: `design:type` is the same as the return type of method.
Setter: `design:type` is the same as the type of the first parameter of the method
✅ **Implementation Complete: ES2020 Export Namespace From
Transformation**
Successfully implemented the transformation for `export * as ns from
"mod"` to convert it to:
```js
import * as _ns from "mod";
export { _ns as ns };
```
## Changes Made:
- [x] Core transformation in `es2020/export_namespace_from.rs`
- [x] ES2020 module integration and options
- [x] Browser compatibility mapping in `oxc_compat` (auto-generated from
custom compat data)
- [x] Babel plugin support (`transform-export-namespace-from`)
- [x] Target-based transformation (works with old browsers, skips modern
ones)
- [x] Code optimizations (removed clones, reserved capacity, used vec1)
- [x] Custom compat data infrastructure for features not in compat-table
- [x] Early return optimization to avoid unnecessary allocations
- [x] Implemented `GatherNodeParts` for `ModuleExportName` to avoid
string allocations
- [x] All tests passing ✅
## Code Review Addressed:
- Reserved capacity for statements vector
- Removed unnecessary clones via destructuring
- Used `ctx.ast.vec1()` for single-element vectors
- Created custom compat data system to avoid manual edits
- Imported `TransformCtx` directly
- Added early return when no export namespace declarations exist
- Implemented `GatherNodeParts` for `ModuleExportName` and use
`generate_uid_based_on_node`
## Example:
**Input:**
```js
export * as ns from "mod";
```
**Output:**
```js
import * as _ns from 'mod';
export { _ns as ns };
```
<!-- START COPILOT CODING AGENT SUFFIX -->
<details>
<summary>Original prompt</summary>
>
> ----
>
> *This section details on the original issue you should resolve*
>
> <issue_title>transformer: transform `export * as ns from "mod";` to
`import * as ns from "mod"; export { ns }`</issue_title>
> <issue_description>This transformation is supported by babel:
https://babeljs.io/docs/babel-plugin-transform-export-namespace-from
> ```js
> export * as ns from "mod";
>
> // to
>
> import * as _ns from "mod";
> export { _ns as ns };
> ```</issue_description>
>
> ## Comments on the Issue (you are @copilot in this section)
>
> <comments>
> </comments>
>
</details>
Fixesoxc-project/oxc#14257
<!-- START COPILOT CODING AGENT TIPS -->
---
✨ Let Copilot coding agent [set things up for
you](https://github.com/oxc-project/oxc/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.
---------
Signed-off-by: Dunqing <dengqing0821@gmail.com>
Co-authored-by: sapphi-red <49056869+sapphi-red@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Dunqing <29533304+Dunqing@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dunqing <dengqing0821@gmail.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
## Summary
- Closesoxc-project/backlog#150
- Replaces manual tab-to-space replacement hack with native indentation configuration
## Changes
- Configure `Driver::codegen_options()` to use spaces with width 2
- Configure `TestCase` Codegen instance to use spaces with width 2
- Remove three instances of `cow_replace("\t", " ")` hack
- Add `IndentChar` imports where needed
## Context
The `CodegenOptions` struct already had `indent_char` and `indent_width` fields available. This change leverages existing infrastructure to provide cleaner, more maintainable code instead of post-processing the output with string replacement.
This addresses the issue mentioned in oxc-project/backlog#150 where transformer conformance output was replacing tabs with spaces as a workaround.
🤖 Generated with [Claude Code](https://claude.ai/code)
Bump NodeJS version to latest active LTS version (v22.20.0).
This is to test that Oxlint plugins written in TS work as intended (#14009). Type-stripping is only supported in NodeJS 22.18.0+ (see #13878).
* Fixes#13733
Fixes the issue where `declare` fields were incorrectly transformed instead of being removed, causing inheritance issues where child `declare` fields would interfere with parent class property initialization.
Only in the `enter_*` phase do we have to skip the `declare` field; in the exit phase, the `declare` fields have already been removed in the `TypeScript` plugin.
Fixes https://github.com/rolldown/rolldown/issues/6079. Regressed introduced by https://github.com/oxc-project/oxc/pull/13227.
The root cause is that the class plugin will insert a constructor method in some cases, but metadata handling is processed before the constructor is inserted, which causes the method count not to be exactly the same as the method metadata count. Thus, decorator metadata is inserted in the incorrect place.
This PR refactors the core transformation logic to ensure that the new methods and properties inserted by other plugins won't affect the decorator transformation.
### Transformation Flow Changes
Before: Complex upfront analysis with separate processing paths
- Used check_class_has_decorated() to analyze entire class tree
- Separate handling for class vs element decorators
- Multiple disconnected stacks for state tracking
After: Unified incremental processing
- Process decorators as we traverse (exit_method_definition,
exit_property_definition, etc.)
- Accumulate decoration statements in single stack during traversal
- Unified processing in transform_class() using consolidated state
Remove leading whitespace when template literal starts with a block comment which contains interpolations.
Previously we didn't remove it because we checked if we're at start of the template literal with `quasi_index == 0`, but that wasn't right because the 1st quasi gets merged into the 2nd. So the 2nd quasi is the 1st *output* quasi. Introduce `is_first_output` var to track this state.
Input:
```js
styled.div` /* ${x} */ color: red; `;
```
Previous output post-minification:
```js
styled.div` color:red;`;
```
After this PR:
```js
styled.div`color:red;`;
```
Remove special logic for inserting spaces when removing comments. Instead, treat comments as equivalent to whitespace, and apply the same rules.
This fixes 2 problems:
1. Don't add unnecessary space when a block comment appears after a character like `{`.
2. *Do* add a space when a block comment is the whole quasi between 2 interpolations, to prevent them being merged.
Simplifying this logic also gives 1%-2% speed-up on transformer benchmarks.
Input:
```js
styled.div`.a {/* blah */color: green; }`;
styled.div`${x}/* blah */${y}`;
```
Previous output post-minification:
```js
styled.div`.a{ color:green;}`;
styled.div`${x}${y}`;
```
After this PR:
```js
styled.div`.a{color:green;}`;
styled.div`${x} ${y}`;
```
The change in approach to whitespace in #13379 means we can remove more escaped line breaks (`\r` and `\n`).
Previously when processing `\r\n`, the next character after `\r` was seen as `\`, and therefore a space was unnecessarily inserted.
Now we only look backwards, not forwards, so this problem goes away. We just treat `\r` and `\n` like any other whitespace.
Previously we included whitespace around block comments when the following character means it could have been removed. e.g.:
Input:
```js
styled.div`.a /* blah */ { color: blue }`
```
Previous output post-minification:
```js
styled.div`.a {color:blue}`
```
After this PR:
```js
styled.div`.a{color:blue}`
```
This is achieved by a significant change to how spaces are inserted.
Previous logic: When encountering whitespace, determine whether to print a space or not based on what came before it, and what's after it. Problem is that this doesn't work correctly when the whitespace is followed by a block comment, because next char is `/` (start of the comment) - whereas what you really want to know is what's *after* the comment.
New logic: When encountering whitespace, print a space if the preceding character isn't `:`, `;`, etc. Don't take into account what the next char is. But later on when encounter e.g. `{`, check if previous character is a space, and if so, remove it. So in the case of `.a /* blah */ {}`, a space gets printed after `.a`, but then it gets deleted again when `{` is found after the comment.
This also has the benefit of making the code simpler.
Previously we'd include an unnecessary space when removing a line comment in cases like:
Input:
```js
styled.div`
.a // comment
{ color: red; }
`
```
Previous output post-minification:
```js
styled.div`.a {color:red;}`
```
After this PR:
```js
styled.div`.a{color:red;}`
```
We still include excess spaces when removing block comments (e.g. ``styled.div`.a /* */ {}` `` -> ``styled.div`.a {}` ``), but that's more complicated to fix, so leaving it for now.
Fix handling block comments which contain an expression. Previously if that block comment ended exactly before another expression, the comment wouldn't be considered closed, and all remaining parts of the template literal would be removed.
Input:
```js
styled.div`${x}/* ${y} */${z}`
```
Previous output post-minification:
```js
styled.div`${x}`
```
After this PR:
```js
styled.div`${x}${z}`
```
This bug was introduced way back in #12224 (my fault!), but had gone unnoticed.
Follow-on after #13346. Remove multiple spaces preceding `{`, `}`, `,`, or `;`.
Previously we removed a single space, but wouldn't remove multiple whitespace characters in a row.
Fixes#13312
When minifying CSS in styled-components, spaces between consecutive interpolations were incorrectly removed, breaking CSS selectors.
**Before:** `&:hover ${A}${B}` (incorrect - space removed)
**After:** `&:hover ${A} ${B}` (correct - space preserved)
The fix modifies the whitespace handling logic to preserve spaces between consecutive interpolations while still removing unnecessary spaces elsewhere.
Fixes: https://github.com/oxc-project/oxc/pull/13215#pullrequestreview-3132979634
Collect metadata and store them in a `VecDeque`, and take them when transforming the class decorators in the `exit_class` side, which can eliminate the unreliable check of `span.is_unspanned` for metadata decorators. However, it introduces a little complexity.
* Fixes#13009
The optimization that converts `{...props}` to `props` when it's the only attribute was incorrectly applied even when `jsx_self` or `jsx_source` plugins are enabled. This caused `__self` and `__source` props to not be merged properly,
resulting in incorrect transformation output in development mode.
This fix disables the optimization when these dev plugins are active, ensuring spread props are preserved for proper merging.
We can now use `if let` chains. Previously we had to use an early return pattern in some places because one branch had multiple nested `if` statements. Now that they're combined into a single `if`, we don't need to use this pattern any more, and can convert to `if` / `else`. Usually this is shorter, but even when it's not, in my opinion it's a bit clearer.
When `remove_class_fields_without_initializer` is enabled, fields without initializers would be removed. However, if the key is computed, and it may have side effects, then we should extract it and place it before class, so that it can keep the same behavior as before.
**Example:**
Input:
```ts
import { Collection } from './collection.ts';
export class Obj {
public readonly [Collection.identifier] = true;
public readonly [Collection.identifier2];
public static readonly [Collection.identifier3] = true;
public static readonly [Collection.identifier4];
}
```
Options:
```json
{
"assumptions": {
"setPublicClassFields": true
},
"plugins": [
"transform-class-properties",
[
"transform-typescript",
{
"removeClassFieldsWithoutInitializer": true
}
]
]
}
```
Output:
```js
import { Collection } from "./collection.ts";
let _Collection$identifie, _Collection$identifie2;
_Collection$identifie = Collection.identifier;
Collection.identifier2;
_Collection$identifie2 = Collection.identifier3;
Collection.identifier4;
export class Obj {
constructor() {
this[_Collection$identifie] = true;
}
}
Obj[_Collection$identifie2] = true;
```
* close#12770
Both `typescript` and `class-properties` have the same transformation for class fields. They would conflict when both are run, so we have to stop transforming class fields in the `typescript` plugin when `class-properties` is enabled.
close: #12556
Fixed an issue where spaces before template literal interpolations were incorrectly removed during CSS minification in the styled-components plugin.
The fix ensures spaces are preserved (e.g., `padding: 0 ${PADDING}px` stays as `padding:0 ${PADDING}px` instead of becoming `padding:0${PADDING}px`).
* close https://github.com/oxc-project/oxc/issues/12460
Injecting top-level statements after the last import statement is unsound because once there is an import statement in the last line, it would be broken if other code relies on the injected statements. Learn more to see the following example.
I suppose the reason we did this is that we had a test case that misled us. I overrode that test with the expected output.
For example:
Input:
```js
import { observer } from 'mobx-react-lite'
import { useFoo } from './useFoo'
export const BazComponent = observer(function BazComponent_() {
const foo = useFoo()
return (
<>
{foo}
{bar}
</>
)
})
import { bar } from './bar'
```
Before output:
```js
import { observer } from "mobx-react-lite";
import { useFoo } from "./useFoo";
export const BazComponent = _s(observer(_c = _s(function BazComponent_() {
_s();
const foo = useFoo();
return /* @__PURE__ */ _jsxs(_Fragment, { children: [foo, bar] });
}, "useFoo{foo}", false, function() {
return [useFoo];
})), "useFoo{foo}", false, function() {
return [useFoo];
});
_c2 = BazComponent;
import { bar } from "./bar";
import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
var _s = $RefreshSig$();
var _c, _c2;
$RefreshReg$(_c, "BazComponent$observer");
$RefreshReg$(_c2, "BazComponent");
```
After output:
```js
import { observer } from "mobx-react-lite";
import { useFoo } from "./useFoo";
import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
var _s = $RefreshSig$();
export const BazComponent = _s(observer(_c = _s(function BazComponent_() {
_s();
const foo = useFoo();
return /* @__PURE__ */ _jsxs(_Fragment, { children: [foo, bar] });
}, "useFoo{foo}", false, function() {
return [useFoo];
})), "useFoo{foo}", false, function() {
return [useFoo];
});
_c2 = BazComponent;
import { bar } from "./bar";
var _c, _c2;
$RefreshReg$(_c, "BazComponent$observer");
$RefreshReg$(_c2, "BazComponent");
```
The difference is that `import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; var _s = $RefreshSig$();` has injected after `import { useFoo } from "./useFoo";`