Files
Boshen bb0ed44541 feat(transformer)!: disable styled-components transpileTemplateLiterals by default (#23171)
## Summary

Closes #13398.

The styled-components transform's `transpileTemplateLiterals` option rewrites tagged template literals into the array-call form:

```js
// in
styled.div`width: 100%;`
// out — transpileTemplateLiterals: true
styled.div(["width: 100%;"])
```

This form is only more compact when template literals are *also* down-levelled to ES5 — which is why `babel-plugin-styled-components` enables it by default, to pre-empt Babel's verbose `_taggedTemplateLiteral` output. Oxc does not down-level template literals below ES6, so the array-call form is strictly *larger* than leaving the tagged template untouched.

This PR flips the default to `false`, as agreed with @Dunqing in #13398.

## Changes

- `StyledComponentsOptions::transpile_template_literals` now defaults to `false` (both the `serde` field default and the `Default` impl), with docs explaining why.
- napi: `transpileTemplateLiterals` is documented `@default false` (`transformer.rs` + `index.d.ts`), and the styled-components napi test snapshot now reflects the non-transpiled default output.
- Conformance fixtures that relied on the old default and whose reference output is transpiled now pin `transpileTemplateLiterals: true`, so they keep matching `babel-plugin-styled-components`' reference output. The conformance snapshot is unchanged — `plugin-styled-components` stays at 25/40.

## Breaking change

`transpileTemplateLiterals` now defaults to `false`. Pass `transpileTemplateLiterals: true` to restore the previous behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-10 03:26:40 +00:00

556 lines
15 KiB
TypeScript

import { Worker } from "node:worker_threads";
import { describe, expect, it, test } from "vitest";
import { HelperMode, transformSync, transform } from "../index";
describe("simple", () => {
const code = "export class A<T> {}";
it("matches output", () => {
const ret = transformSync("test.ts", code, { sourcemap: true });
expect(ret).toMatchObject({
code: "export class A {}\n",
errors: [],
helpersUsed: {},
map: {
names: [],
sources: ["test.ts"],
sourcesContent: ["export class A<T> {}"],
version: 3,
},
});
});
it("uses the `lang` option", () => {
const ret = transformSync("test.vue", code, { lang: "ts" });
expect(ret.code).toEqual("export class A {}\n");
});
it("uses the `declaration` option", () => {
const ret = transformSync("test.ts", code, { typescript: { declaration: {} } });
expect(ret.declaration).toEqual("export declare class A<T> {}\n");
});
it("uses the `sourcemap` option", () => {
const ret = transformSync("test.ts", code, {
typescript: { declaration: {} },
sourcemap: true,
});
expect(ret.declarationMap).toMatchObject({
names: [],
sources: ["test.ts"],
sourcesContent: ["export class A<T> {}"],
version: 3,
});
});
});
describe("transform", () => {
it("should produce the same result as sync transform", async () => {
const sourceCode = `
const add = (a, b) => a + b;
console.log(add(1, 2));
`;
const syncResult = transformSync("test.js", sourceCode, { target: "es2015" });
const asyncResult = await transform("test.js", sourceCode, { target: "es2015" });
expect(asyncResult.code).toEqual(syncResult.code);
expect(asyncResult.errors).toEqual(syncResult.errors);
expect(asyncResult.helpersUsed).toEqual(syncResult.helpersUsed);
});
});
describe("transformSync", () => {
it("should not transform by default", () => {
const cases = [
"() => {};",
"a ** b;",
"async function foo() {}",
"({ ...x });",
"try {} catch {}",
"a ?? b;",
"a ||= b;",
"class foo {\n\tstatic {}\n}",
];
for (const code of cases) {
const ret = transformSync("test.ts", code);
expect(ret.code.trim()).toEqual(code);
}
});
});
describe("target", () => {
const data = [
["es6", "a ** b;\n"],
["es2015", "a ** b;\n"],
["es2016", "async function foo() {}\n"],
["es2017", "({ ...x });\n"],
["es2017", "try {} catch {}\n"],
["es2019", "a?.b;\n"],
["es2019", "a ?? b;\n"],
["es2021", "class foo {\n\tstatic {}\n}\n"],
["es2025", "using handlerSync = openSync();\n"],
];
test.each(data)("transform %s", (target, code) => {
// Also test array syntax. Use .mjs for explicit ESM.
const ret = transformSync("test.mjs", code, { target: [target] });
expect(ret.errors.length).toBe(0);
expect(ret.code).toBeDefined();
expect(ret.code).not.toEqual(code);
});
test.each(data)("no transform esnext: %s", (_target, code) => {
const ret = transformSync("test.mjs", code, { target: "esnext" });
expect(ret.errors.length).toBe(0);
expect(ret.code).toBeDefined();
expect(ret.code).toEqual(code);
});
it("should turn off class propertiers because plugin is not ready", () => {
const code = "class Foo {\n\t#a;\n}\n";
const ret = transformSync("test.mjs", code, { target: "es2015" });
expect(ret.errors.length).toBe(0);
expect(ret.code).toBeDefined();
expect(ret.code).toMatchInlineSnapshot(`
"import _classPrivateFieldInitSpec from "@oxc-project/runtime/helpers/classPrivateFieldInitSpec";
var _a = /* @__PURE__ */ new WeakMap();
class Foo {
constructor() {
_classPrivateFieldInitSpec(this, _a, void 0);
}
}
"
`);
});
});
describe("helpers", () => {
const data: Array<[HelperMode, string]> = [
[HelperMode.External, "babelHelpers.objectSpread2({}, x);\n"],
[
HelperMode.Runtime,
'import _objectSpread from "@oxc-project/runtime/helpers/objectSpread2";\n_objectSpread({}, x);\n',
],
];
test.each(data)("%s", (mode, expected) => {
const code = `({ ...x })`;
const ret = transformSync("test.mjs", code, {
target: "es2015",
helpers: { mode },
});
expect(ret.code).toEqual(expected);
expect(ret.helpersUsed).toStrictEqual({
objectSpread2: "@oxc-project/runtime/helpers/objectSpread2",
});
});
});
describe("modules", () => {
it("should transform `export =` and `import =`", () => {
const code = `
export = function foo (): void {}
import bar = require('bar')
console.log(bar)
`;
const ret = transformSync("test.ts", code, {
typescript: {
declaration: {},
},
});
expect(ret.code).toMatchInlineSnapshot(`
"module.exports = function foo() {};
const bar = require("bar");
console.log(bar);
"
`);
expect(ret.declaration).toEqual("declare const _default: () => void;\nexport = _default;\n");
});
});
describe("jsx", () => {
const code = `const foo: Foo = <div/>`;
it("enables jsx transform by default", () => {
const ret = transformSync("test.tsx", code, { sourceType: "module" });
expect(ret.code).toMatchInlineSnapshot(`
"import { jsx as _jsx } from "react/jsx-runtime";
const foo = /* @__PURE__ */ _jsx("div", {});
"
`);
});
it("configures jsx", () => {
const ret = transformSync("test.tsx", code, {
sourceType: "module",
jsx: {
importSource: "xxx",
},
});
expect(ret.code).toMatchInlineSnapshot(`
"import { jsx as _jsx } from "xxx/jsx-runtime";
const foo = /* @__PURE__ */ _jsx("div", {});
"
`);
});
it("can preserve jsx transform", () => {
const ret = transformSync("test.tsx", code, {
sourceType: "module",
jsx: "preserve",
});
expect(ret.code).toEqual("const foo = <div />;\n");
});
});
describe("react refresh plugin", () => {
const code = `import { useState } from "react";
export const App = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>count is {count}</button>;
};`;
it("matches output", () => {
const ret = transformSync("test.tsx", code, { jsx: { refresh: {} } });
expect(ret.code).toMatchInlineSnapshot(
`
"import { useState } from "react";
import { jsxs as _jsxs } from "react/jsx-runtime";
var _s = $RefreshSig$();
export const App = () => {
_s();
const [count, setCount] = useState(0);
return /* @__PURE__ */ _jsxs("button", {
onClick: () => setCount(count + 1),
children: ["count is ", count]
});
};
_s(App, "oDgYfYHkD9Wkv4hrAPCkI/ev3YU=");
_c = App;
var _c;
$RefreshReg$(_c, "App");
"
`,
);
});
});
describe("define plugin", () => {
it("matches output", () => {
const code = 'if (process.env.NODE_ENV === "production") { foo; }';
const ret = transformSync("test.tsx", code, {
define: {
"process.env.NODE_ENV": '"development"',
},
});
expect(ret.code).toEqual("");
});
it("handles typescript declare global", () => {
const code = "declare let __TEST_DEFINE__: string; console.log({ __TEST_DEFINE__ });";
const ret = transformSync("test.ts", code, {
define: {
__TEST_DEFINE__: '"replaced"',
},
});
expect(ret.code).toEqual('console.log({ __TEST_DEFINE__: "replaced" });\n');
});
it("replaces undefined", () => {
const code = "new Foo()";
const ret = transformSync("test.js", code, {
define: {
Foo: "undefined",
},
});
// Replaced `undefined` with `void 0` by DCE.
expect(ret.code).toEqual("new (void 0)();\n");
});
it("keeps debugger", () => {
const code = "Foo; debugger;";
const ret = transformSync("test.js", code, {
define: {
Foo: "Bar",
},
});
expect(ret.code).toEqual("Bar;\ndebugger;\n");
});
});
describe("inject plugin", () => {
const code = "let _ = Object.assign";
it("matches output", () => {
const ret = transformSync("test.tsx", code, {
inject: {
"Object.assign": "foo",
},
});
expect(ret.code).toEqual(
'import $inject_Object_assign from "foo";\nlet _ = $inject_Object_assign;\n',
);
});
it("escapes quotes in source module name", () => {
const code = "console.log(a)";
const ret = transformSync("test.tsx", code, {
inject: {
a: 'foo"',
},
});
expect(ret.code).toEqual('import a from "foo\\"";\nconsole.log(a);\n');
});
});
describe("legacy decorator", () => {
it("matches output", () => {
const code = `
export default @dce class C {
@dce
prop = 0;
method(@dce param) {}
}
`;
const ret = transformSync("test.tsx", code, {
decorator: {
legacy: true,
},
});
expect(ret.code).toMatchInlineSnapshot(`
"import _decorate from "@oxc-project/runtime/helpers/decorate";
import _decorateParam from "@oxc-project/runtime/helpers/decorateParam";
let C = class C {
prop = 0;
method(param) {}
};
_decorate([dce], C.prototype, "prop", void 0);
_decorate([_decorateParam(0, dce)], C.prototype, "method", null);
C = _decorate([dce], C);
export default C;
"
`);
});
describe("emitDecoratorMetadata", () => {
it("matches output", () => {
const code = `
export default @dce class C {
@dce
prop = 0;
method(@dce param) {}
}
`;
const ret = transformSync("test.tsx", code, {
decorator: {
legacy: true,
emitDecoratorMetadata: true,
},
});
expect(ret.code).toMatchInlineSnapshot(`
"import _decorateMetadata from "@oxc-project/runtime/helpers/decorateMetadata";
import _decorate from "@oxc-project/runtime/helpers/decorate";
import _decorateParam from "@oxc-project/runtime/helpers/decorateParam";
let C = class C {
prop = 0;
method(param) {}
};
_decorate([dce, _decorateMetadata("design:type", Object)], C.prototype, "prop", void 0);
_decorate([
_decorateParam(0, dce),
_decorateMetadata("design:type", Function),
_decorateMetadata("design:paramtypes", [Object]),
_decorateMetadata("design:returntype", void 0)
], C.prototype, "method", null);
C = _decorate([dce], C);
export default C;
"
`);
});
});
describe("strictNullChecks", () => {
const code = `
class Source {
@dce prop!: string | null;
}
`;
it("emits Object by default (strictNullChecks: true)", () => {
const ret = transformSync("test.ts", code, {
sourceType: "module",
decorator: {
legacy: true,
emitDecoratorMetadata: true,
},
});
expect(ret.code).toMatchInlineSnapshot(`
"import _decorateMetadata from "@oxc-project/runtime/helpers/decorateMetadata";
import _decorate from "@oxc-project/runtime/helpers/decorate";
class Source {
prop;
}
_decorate([dce, _decorateMetadata("design:type", Object)], Source.prototype, "prop", void 0);
"
`);
});
it("elides null/undefined from union when strictNullChecks: false", () => {
const ret = transformSync("test.ts", code, {
sourceType: "module",
decorator: {
legacy: true,
emitDecoratorMetadata: true,
strictNullChecks: false,
},
});
expect(ret.code).toMatchInlineSnapshot(`
"import _decorateMetadata from "@oxc-project/runtime/helpers/decorateMetadata";
import _decorate from "@oxc-project/runtime/helpers/decorate";
class Source {
prop;
}
_decorate([dce, _decorateMetadata("design:type", String)], Source.prototype, "prop", void 0);
"
`);
});
});
});
describe("worker", () => {
it("should run", async () => {
const code = await new Promise((resolve, reject) => {
const worker = new Worker("./test/worker.mjs");
worker.on("error", (err) => {
reject(err);
});
worker.on("exit", (code) => {
resolve(code);
});
});
expect(code).toBe(0);
});
});
describe("typescript", () => {
describe("options", () => {
test("removeClassFieldsWithoutInitializer", () => {
const code = `
class Foo {
a: number;
b: number = 1;
}
`;
const ret = transformSync("test.ts", code, {
typescript: {
removeClassFieldsWithoutInitializer: true,
},
});
expect(ret.code).toMatchInlineSnapshot(`
"class Foo {
b = 1;
}
"
`);
});
test("optimizeConstEnums", () => {
const code = `
const enum Color { Red = 1, Green, Blue }
console.log(Color.Red, Color.Green, Color.Blue);
`;
const ret = transformSync("test.ts", code, {
typescript: {
optimizeConstEnums: true,
},
});
expect(ret.code).toEqual("console.log(1, 2, 3);\n");
});
test("optimizeEnums", () => {
const code = `
enum Status { Active = 1, Inactive = 2 }
console.log(Status.Active, Status.Inactive);
`;
const ret = transformSync("test.ts", code, {
typescript: {
optimizeEnums: true,
},
});
expect(ret.code).toEqual("console.log(1, 2);\n");
});
test("align `useDefineForClassFields: false`", () => {
const code = `
class Foo {
a: number;
b: number = 1;
@dec
c: number;
}
class StaticFoo {
static a: number;
static b: number = 1;
@dec
static c: number;
}
`;
const ret = transformSync("test.ts", code, {
sourceType: "module",
assumptions: {
setPublicClassFields: true,
},
target: "es2020",
typescript: {
removeClassFieldsWithoutInitializer: true,
},
decorator: {
legacy: true,
},
});
expect(ret.code).toMatchInlineSnapshot(`
"import _decorate from "@oxc-project/runtime/helpers/decorate";
class Foo {
constructor() {
this.b = 1;
}
}
_decorate([dec], Foo.prototype, "c", void 0);
class StaticFoo {}
StaticFoo.b = 1;
_decorate([dec], StaticFoo, "c", void 0);
"
`);
});
});
});
describe("styled-components", () => {
test("matches output", () => {
const code = `
import styled, { css } from 'styled-components';
styled.div\`color: red;\`;
const v = css(["color: red;"]);
`;
const ret = transformSync("test.js", code, {
plugins: {
styledComponents: {
pure: true,
},
},
});
expect(ret.code).toMatchInlineSnapshot(`
"import styled, { css } from "styled-components";
styled.div.withConfig({
displayName: "test",
componentId: "sc-3q0sbi-0"
})\`color:red;\`;
const v = /* @__PURE__ */ css(["color: red;"]);
"
`);
});
});