Support TypeScript const enums (#13324)
This commit is contained in:
parent
0542f0d7da
commit
b707842dd0
89
packages/babel-plugin-transform-typescript/src/const-enum.ts
Normal file
89
packages/babel-plugin-transform-typescript/src/const-enum.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import type * as t from "@babel/types";
|
||||
import type { NodePath } from "@babel/traverse";
|
||||
|
||||
import { translateEnumValues } from "./enum";
|
||||
|
||||
export default function transpileConstEnum(
|
||||
path: NodePath<t.TSEnumDeclaration & { const: true }>,
|
||||
t: typeof import("@babel/types"),
|
||||
) {
|
||||
const { name } = path.node.id;
|
||||
|
||||
const parentIsExport = path.parentPath.isExportNamedDeclaration();
|
||||
let isExported = parentIsExport;
|
||||
if (!isExported && t.isProgram(path.parent)) {
|
||||
isExported = path.parent.body.some(
|
||||
stmt =>
|
||||
t.isExportNamedDeclaration(stmt) &&
|
||||
!stmt.source &&
|
||||
stmt.specifiers.some(
|
||||
spec => t.isExportSpecifier(spec) && spec.local.name === name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const entries = translateEnumValues(path, t);
|
||||
|
||||
if (isExported) {
|
||||
const obj = t.objectExpression(
|
||||
entries.map(([name, value]) =>
|
||||
t.objectProperty(
|
||||
t.isValidIdentifier(name)
|
||||
? t.identifier(name)
|
||||
: t.stringLiteral(name),
|
||||
value,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (path.scope.hasOwnBinding(name)) {
|
||||
(parentIsExport ? path.parentPath : path).replaceWith(
|
||||
t.expressionStatement(
|
||||
t.callExpression(
|
||||
t.memberExpression(t.identifier("Object"), t.identifier("assign")),
|
||||
[path.node.id, obj],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
path.replaceWith(
|
||||
t.variableDeclaration("var", [t.variableDeclarator(path.node.id, obj)]),
|
||||
);
|
||||
path.scope.registerDeclaration(path);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const entriesMap = new Map(entries);
|
||||
|
||||
// TODO: After fixing https://github.com/babel/babel/pull/11065, we can
|
||||
// use path.scope.getBinding(name).referencePaths rather than doing
|
||||
// a full traversal.
|
||||
path.scope.path.traverse({
|
||||
Scope(path) {
|
||||
if (path.scope.hasOwnBinding(name)) path.skip();
|
||||
},
|
||||
MemberExpression(path) {
|
||||
if (!t.isIdentifier(path.node.object, { name })) return;
|
||||
|
||||
let key: string;
|
||||
if (path.node.computed) {
|
||||
if (t.isStringLiteral(path.node.property)) {
|
||||
key = path.node.property.value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (t.isIdentifier(path.node.property)) {
|
||||
key = path.node.property.name;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (!entriesMap.has(key)) return;
|
||||
|
||||
path.replaceWith(t.cloneNode(entriesMap.get(key)));
|
||||
},
|
||||
});
|
||||
|
||||
path.remove();
|
||||
}
|
||||
@ -5,9 +5,6 @@ import type { NodePath } from "@babel/traverse";
|
||||
|
||||
export default function transpileEnum(path, t) {
|
||||
const { node } = path;
|
||||
if (node.const) {
|
||||
throw path.buildCodeFrameError("'const' enums are not supported.");
|
||||
}
|
||||
|
||||
if (node.declare) {
|
||||
path.remove();
|
||||
@ -105,7 +102,10 @@ type PreviousEnumMembers = {
|
||||
[name: string]: number | string;
|
||||
};
|
||||
|
||||
function translateEnumValues(path, t) {
|
||||
export function translateEnumValues(
|
||||
path: NodePath<t.TSEnumDeclaration>,
|
||||
t: typeof import("@babel/types"),
|
||||
): Array<[name: string, value: t.Expression]> {
|
||||
const seen: PreviousEnumMembers = Object.create(null);
|
||||
// Start at -1 so the first enum member is its increment, 0.
|
||||
let prev: number | typeof undefined = -1;
|
||||
|
||||
@ -3,6 +3,7 @@ import syntaxTypeScript from "@babel/plugin-syntax-typescript";
|
||||
import { types as t, template } from "@babel/core";
|
||||
import { injectInitialization } from "@babel/helper-create-class-features-plugin";
|
||||
|
||||
import transpileConstEnum from "./const-enum";
|
||||
import transpileEnum from "./enum";
|
||||
import transpileNamespace from "./namespace";
|
||||
import type { NodePath } from "@babel/traverse";
|
||||
@ -60,6 +61,7 @@ export default declare((api, opts) => {
|
||||
jsxPragma = "React.createElement",
|
||||
jsxPragmaFrag = "React.Fragment",
|
||||
onlyRemoveTypeImports = false,
|
||||
optimizeConstEnums = false,
|
||||
} = opts;
|
||||
|
||||
if (!process.env.BABEL_8_BREAKING) {
|
||||
@ -462,7 +464,11 @@ export default declare((api, opts) => {
|
||||
},
|
||||
|
||||
TSEnumDeclaration(path) {
|
||||
transpileEnum(path, t);
|
||||
if (optimizeConstEnums && path.node.const) {
|
||||
transpileConstEnum(path, t);
|
||||
} else {
|
||||
transpileEnum(path, t);
|
||||
}
|
||||
},
|
||||
|
||||
TSImportEqualsDeclaration(path: NodePath<t.TSImportEqualsDeclaration>) {
|
||||
|
||||
@ -1 +0,0 @@
|
||||
{ "throws": "'const' enums are not supported." }
|
||||
@ -0,0 +1,3 @@
|
||||
export declare enum A {}
|
||||
|
||||
;
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"sourceType": "module"
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
;
|
||||
@ -1 +1,2 @@
|
||||
const enum E {}
|
||||
// With --isolatedModules, TSC ignores the "const" modifier when compiling enums
|
||||
const enum E {}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
{ "throws": "'const' enums are not supported." }
|
||||
4
packages/babel-plugin-transform-typescript/test/fixtures/enum/const/output.js
vendored
Normal file
4
packages/babel-plugin-transform-typescript/test/fixtures/enum/const/output.js
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
// With --isolatedModules, TSC ignores the "const" modifier when compiling enums
|
||||
var E;
|
||||
|
||||
(function (E) {})(E || (E = {}));
|
||||
@ -0,0 +1,11 @@
|
||||
export const enum A {
|
||||
x = 3,
|
||||
y = "f",
|
||||
z = 4 << 2,
|
||||
w = y
|
||||
}
|
||||
|
||||
A.x;
|
||||
A.y;
|
||||
A.z;
|
||||
A.w;
|
||||
@ -0,0 +1,10 @@
|
||||
export var A = {
|
||||
x: 3,
|
||||
y: "f",
|
||||
z: 16,
|
||||
w: "f"
|
||||
};
|
||||
A.x;
|
||||
A.y;
|
||||
A.z;
|
||||
A.w;
|
||||
@ -0,0 +1,11 @@
|
||||
const enum A {
|
||||
x = 3,
|
||||
y = "f",
|
||||
z = 4 << 2,
|
||||
w = y
|
||||
}
|
||||
|
||||
A.x;
|
||||
A.y;
|
||||
A.z;
|
||||
A.w;
|
||||
@ -0,0 +1,4 @@
|
||||
3;
|
||||
"f";
|
||||
16;
|
||||
"f";
|
||||
@ -0,0 +1,3 @@
|
||||
declare const enum A { x }
|
||||
|
||||
A.x;
|
||||
@ -0,0 +1 @@
|
||||
0;
|
||||
@ -0,0 +1,3 @@
|
||||
export const enum A { y }
|
||||
|
||||
let x = A.y;
|
||||
@ -0,0 +1,4 @@
|
||||
export var A = {
|
||||
y: 0
|
||||
};
|
||||
let x = A.y;
|
||||
@ -0,0 +1,5 @@
|
||||
const enum A { y }
|
||||
|
||||
let x = A.y;
|
||||
|
||||
export { A };
|
||||
@ -0,0 +1,5 @@
|
||||
var A = {
|
||||
y: 0
|
||||
};
|
||||
let x = A.y;
|
||||
export { A };
|
||||
@ -0,0 +1,8 @@
|
||||
const enum A { x }
|
||||
|
||||
{
|
||||
let A = {};
|
||||
A.x;
|
||||
}
|
||||
|
||||
A.x;
|
||||
@ -0,0 +1,5 @@
|
||||
{
|
||||
let A = {};
|
||||
A.x;
|
||||
}
|
||||
0;
|
||||
8
packages/babel-plugin-transform-typescript/test/fixtures/optimize-const-enums/local/input.ts
vendored
Normal file
8
packages/babel-plugin-transform-typescript/test/fixtures/optimize-const-enums/local/input.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
const enum A {
|
||||
x, y
|
||||
}
|
||||
|
||||
A.x;
|
||||
A["y"];
|
||||
A.z;
|
||||
A;
|
||||
@ -0,0 +1,4 @@
|
||||
0;
|
||||
1;
|
||||
A.z;
|
||||
A;
|
||||
@ -0,0 +1,13 @@
|
||||
export const enum A {
|
||||
x, y
|
||||
}
|
||||
|
||||
export const enum A {
|
||||
z
|
||||
}
|
||||
|
||||
A.x;
|
||||
A["y"];
|
||||
A.z;
|
||||
A.w;
|
||||
A;
|
||||
@ -0,0 +1,12 @@
|
||||
export var A = {
|
||||
x: 0,
|
||||
y: 1
|
||||
};
|
||||
Object.assign(A, {
|
||||
z: 0
|
||||
});
|
||||
A.x;
|
||||
A["y"];
|
||||
A.z;
|
||||
A.w;
|
||||
A;
|
||||
13
packages/babel-plugin-transform-typescript/test/fixtures/optimize-const-enums/merged/input.ts
vendored
Normal file
13
packages/babel-plugin-transform-typescript/test/fixtures/optimize-const-enums/merged/input.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
const enum A {
|
||||
x, y
|
||||
}
|
||||
|
||||
const enum A {
|
||||
z
|
||||
}
|
||||
|
||||
A.x;
|
||||
A["y"];
|
||||
A.z;
|
||||
A.w;
|
||||
A;
|
||||
@ -0,0 +1,5 @@
|
||||
0;
|
||||
1;
|
||||
0;
|
||||
A.w;
|
||||
A;
|
||||
4
packages/babel-plugin-transform-typescript/test/fixtures/optimize-const-enums/options.json
vendored
Normal file
4
packages/babel-plugin-transform-typescript/test/fixtures/optimize-const-enums/options.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"plugins": [["transform-typescript", { "optimizeConstEnums": true }]],
|
||||
"sourceType": "module"
|
||||
}
|
||||
@ -12,6 +12,7 @@ export default declare((api, opts) => {
|
||||
jsxPragma,
|
||||
jsxPragmaFrag,
|
||||
onlyRemoveTypeImports,
|
||||
optimizeConstEnums,
|
||||
} = normalizeOptions(opts);
|
||||
|
||||
const pluginOptions = process.env.BABEL_8_BREAKING
|
||||
@ -21,6 +22,7 @@ export default declare((api, opts) => {
|
||||
jsxPragma,
|
||||
jsxPragmaFrag,
|
||||
onlyRemoveTypeImports,
|
||||
optimizeConstEnums,
|
||||
})
|
||||
: isTSX => ({
|
||||
allowDeclareFields: opts.allowDeclareFields,
|
||||
@ -29,6 +31,7 @@ export default declare((api, opts) => {
|
||||
jsxPragma,
|
||||
jsxPragmaFrag,
|
||||
onlyRemoveTypeImports,
|
||||
optimizeConstEnums,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -4,15 +4,17 @@ const v = new OptionValidator("@babel/preset-typescript");
|
||||
export default function normalizeOptions(options = {}) {
|
||||
let { allowNamespaces = true, jsxPragma, onlyRemoveTypeImports } = options;
|
||||
|
||||
const TopLevelOptions = {
|
||||
allExtensions: "allExtensions",
|
||||
allowNamespaces: "allowNamespaces",
|
||||
isTSX: "isTSX",
|
||||
jsxPragma: "jsxPragma",
|
||||
jsxPragmaFrag: "jsxPragmaFrag",
|
||||
onlyRemoveTypeImports: "onlyRemoveTypeImports",
|
||||
optimizeConstEnums: "optimizeConstEnums",
|
||||
};
|
||||
|
||||
if (process.env.BABEL_8_BREAKING) {
|
||||
const TopLevelOptions = {
|
||||
allExtensions: "allExtensions",
|
||||
allowNamespaces: "allowNamespaces",
|
||||
isTSX: "isTSX",
|
||||
jsxPragma: "jsxPragma",
|
||||
jsxPragmaFrag: "jsxPragmaFrag",
|
||||
onlyRemoveTypeImports: "onlyRemoveTypeImports",
|
||||
};
|
||||
v.validateTopLevelOptions(options, TopLevelOptions);
|
||||
allowNamespaces = v.validateBooleanOption(
|
||||
TopLevelOptions.allowNamespaces,
|
||||
@ -32,23 +34,32 @@ export default function normalizeOptions(options = {}) {
|
||||
}
|
||||
|
||||
const jsxPragmaFrag = v.validateStringOption(
|
||||
"jsxPragmaFrag",
|
||||
TopLevelOptions.jsxPragmaFrag,
|
||||
options.jsxPragmaFrag,
|
||||
"React.Fragment",
|
||||
);
|
||||
|
||||
const allExtensions = v.validateBooleanOption(
|
||||
"allExtensions",
|
||||
TopLevelOptions.allExtensions,
|
||||
options.allExtensions,
|
||||
false,
|
||||
);
|
||||
|
||||
const isTSX = v.validateBooleanOption("isTSX", options.isTSX, false);
|
||||
|
||||
const isTSX = v.validateBooleanOption(
|
||||
TopLevelOptions.isTSX,
|
||||
options.isTSX,
|
||||
false,
|
||||
);
|
||||
if (isTSX) {
|
||||
v.invariant(allExtensions, "isTSX:true requires allExtensions:true");
|
||||
}
|
||||
|
||||
const optimizeConstEnums = v.validateBooleanOption(
|
||||
TopLevelOptions.optimizeConstEnums,
|
||||
options.optimizeConstEnums,
|
||||
false,
|
||||
);
|
||||
|
||||
return {
|
||||
allExtensions,
|
||||
allowNamespaces,
|
||||
@ -56,5 +67,6 @@ export default function normalizeOptions(options = {}) {
|
||||
jsxPragma,
|
||||
jsxPragmaFrag,
|
||||
onlyRemoveTypeImports,
|
||||
optimizeConstEnums,
|
||||
};
|
||||
}
|
||||
|
||||
3
packages/babel-preset-typescript/test/fixtures/opts/optimizeConstEnums/input.ts
vendored
Normal file
3
packages/babel-preset-typescript/test/fixtures/opts/optimizeConstEnums/input.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
const enum A { x }
|
||||
|
||||
A.x;
|
||||
1
packages/babel-preset-typescript/test/fixtures/opts/optimizeConstEnums/output.js
vendored
Normal file
1
packages/babel-preset-typescript/test/fixtures/opts/optimizeConstEnums/output.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
0;
|
||||
3
packages/babel-preset-typescript/test/fixtures/opts/options.json
vendored
Normal file
3
packages/babel-preset-typescript/test/fixtures/opts/options.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": [["typescript", { "optimizeConstEnums": true }]]
|
||||
}
|
||||
@ -11,6 +11,7 @@ describe("normalize options", () => {
|
||||
"allowNamespaces",
|
||||
"isTSX",
|
||||
"onlyRemoveTypeImports",
|
||||
"optimizeConstEnums",
|
||||
])("should throw when `%p` is not a boolean", optionName => {
|
||||
expect(() => normalizeOptions({ [optionName]: 0 })).toThrow(
|
||||
`@babel/preset-typescript: '${optionName}' option must be a boolean.`,
|
||||
@ -36,6 +37,7 @@ describe("normalize options", () => {
|
||||
"jsxPragma": "React",
|
||||
"jsxPragmaFrag": "React.Fragment",
|
||||
"onlyRemoveTypeImports": true,
|
||||
"optimizeConstEnums": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
@ -58,7 +60,7 @@ describe("normalize options", () => {
|
||||
expect(() => normalizeOptions({ [optionName]: 0 })).not.toThrowError();
|
||||
},
|
||||
);
|
||||
it.each(["allExtensions", "isTSX"])(
|
||||
it.each(["allExtensions", "isTSX", "optimizeConstEnums"])(
|
||||
"should throw when `%p` is not a boolean",
|
||||
optionName => {
|
||||
expect(() => normalizeOptions({ [optionName]: 0 })).toThrow(
|
||||
@ -83,6 +85,7 @@ describe("normalize options", () => {
|
||||
"jsxPragma": undefined,
|
||||
"jsxPragmaFrag": "React.Fragment",
|
||||
"onlyRemoveTypeImports": undefined,
|
||||
"optimizeConstEnums": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user