mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
bb0ed44541
## 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)
556 lines
15 KiB
TypeScript
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;"]);
|
|
"
|
|
`);
|
|
});
|
|
});
|