Generator performance (#13593)

* bench: setup benchmarks

* add charcodes

* perf: use string as buffer backend

baseline 256 empty statements: 3_718 ops/sec ±18.65% (0.269ms)
baseline 512 empty statements: 2_070 ops/sec ±0.96% (0.483ms)
baseline 1024 empty statements: 1_012 ops/sec ±1.76% (0.988ms)
baseline 2048 empty statements: 510 ops/sec ±1.53% (1.96ms)
current 256 empty statements: 3_965 ops/sec ±21.14% (0.252ms)
current 512 empty statements: 2_219 ops/sec ±1.29% (0.451ms)
current 1024 empty statements: 1_089 ops/sec ±1.53% (0.918ms)
current 2048 empty statements: 548 ops/sec ±1.87% (1.824ms)

* perf: add endsWithCharAndNewline

baseline 256 1-length identifiers: 1_947 ops/sec ±25.11% (0.514ms)
baseline 512 1-length identifiers: 1_115 ops/sec ±0.89% (0.897ms)
baseline 1024 1-length identifiers: 537 ops/sec ±1.71% (1.862ms)
baseline 2048 1-length identifiers: 273 ops/sec ±0.57% (3.669ms)
current 256 1-length identifiers: 2_178 ops/sec ±27.17% (0.459ms)
current 512 1-length identifiers: 1_250 ops/sec ±1.19% (0.8ms)
current 1024 1-length identifiers: 622 ops/sec ±0.71% (1.608ms)
current 2048 1-length identifiers: 308 ops/sec ±1.35% (3.251ms)

* perf: avoid one byte string compare

* perf: avoid scaning word for /

* perf: hoist babel type methods

baseline 256 25-length identifiers: 1_869 ops/sec ±29.4% (0.535ms)
baseline 512 25-length identifiers: 1_092 ops/sec ±1.62% (0.916ms)
baseline 1024 25-length identifiers: 537 ops/sec ±1.29% (1.862ms)
baseline 2048 25-length identifiers: 264 ops/sec ±2% (3.793ms)
current 256 25-length identifiers: 2_462 ops/sec ±23.38% (0.406ms)
current 512 25-length identifiers: 1_401 ops/sec ±0.73% (0.714ms)
current 1024 25-length identifiers: 671 ops/sec ±1.55% (1.491ms)
current 2048 25-length identifiers: 332 ops/sec ±1.44% (3.014ms)

* perf: hoist parens methods

baseline 256 1-length identifiers: 2_678 ops/sec ±24.96% (0.373ms)
baseline 512 1-length identifiers: 1_472 ops/sec ±2.33% (0.68ms)
baseline 1024 1-length identifiers: 737 ops/sec ±1.74% (1.357ms)
baseline 2048 1-length identifiers: 371 ops/sec ±0.79% (2.695ms)
current 256 1-length identifiers: 2_633 ops/sec ±32.44% (0.38ms)
current 512 1-length identifiers: 1_676 ops/sec ±1.49% (0.597ms)
current 1024 1-length identifiers: 803 ops/sec ±1.95% (1.246ms)
current 2048 1-length identifiers: 385 ops/sec ±2.22% (2.596ms)

* cleanup unused benchcase

* Update packages/babel-generator/src/buffer.ts

Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>

* cleanup benchmarks

Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
This commit is contained in:
Huáng Jùnliàng 2021-07-22 23:01:48 -04:00 committed by GitHub
parent 2b6acada3c
commit 03e8476b27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 11656 additions and 205 deletions

View File

@ -179,6 +179,10 @@ module.exports = function (api) {
plugins: ["babel-plugin-transform-charcodes"],
assumptions: parserAssumptions,
},
{
test: ["packages/babel-generator"].map(normalize),
plugins: ["babel-plugin-transform-charcodes"],
},
convertESM && {
test: [
"./packages/babel-cli",

View File

@ -0,0 +1,25 @@
import Benchmark from "benchmark";
import baseline from "@babel-baseline/generator";
import current from "../../lib/index.js";
import parser from "@babel/parser";
import { report } from "../util.mjs";
const suite = new Benchmark.Suite();
function createInput(length) {
return parser.parse(";".repeat(length));
}
function benchCases(name, implementation, options) {
for (const length of [256, 512, 1024, 2048]) {
const input = createInput(length);
suite.add(`${name} ${length} empty statements`, () => {
implementation(input, options);
});
}
}
benchCases("baseline", baseline.default);
benchCases("current", current.default);
suite.on("cycle", report).run();

View File

@ -0,0 +1,25 @@
import Benchmark from "benchmark";
import baseline from "@babel-baseline/generator";
import current from "../../lib/index.js";
import parser from "@babel/parser";
import { report } from "../util.mjs";
const suite = new Benchmark.Suite();
function createInput(length) {
return parser.parse("a;".repeat(length));
}
function benchCases(name, implementation, options) {
for (const length of [256, 512, 1024, 2048]) {
const input = createInput(length);
suite.add(`${name} ${length} 1-length identifiers`, () => {
implementation(input, options);
});
}
}
benchCases("baseline", baseline.default);
benchCases("current", current.default);
suite.on("cycle", report).run();

View File

@ -0,0 +1,25 @@
import Benchmark from "benchmark";
import baseline from "@babel-baseline/generator";
import current from "../../lib/index.js";
import parser from "@babel/parser";
import { report } from "../util.mjs";
const suite = new Benchmark.Suite();
function createInput(length) {
return parser.parse("parseMaybeImportAssertion;".repeat(length));
}
function benchCases(name, implementation, options) {
for (const length of [256, 512, 1024, 2048]) {
const input = createInput(length);
suite.add(`${name} ${length} 25-length identifiers`, () => {
implementation(input, options);
});
}
}
benchCases("baseline", baseline.default);
benchCases("current", current.default);
suite.on("cycle", report).run();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
import Benchmark from "benchmark";
import baseline from "@babel-baseline/generator";
import current from "../../lib/index.js";
import parser from "@babel/parser";
import { report } from "../util.mjs";
import { readFileSync } from "fs";
const suite = new Benchmark.Suite();
function createInput(length) {
return parser.parse(
readFileSync(new URL("./jquery-3.6.txt", import.meta.url), {
encoding: "utf-8",
}).repeat(length)
);
}
function benchCases(name, implementation, options) {
for (const length of [1, 2]) {
const input = createInput(length);
suite.add(`${name} ${length} jquery 3.6`, () => {
implementation(input, options);
});
}
}
benchCases("baseline", baseline.default);
benchCases("current", current.default);
suite.on("cycle", report).run();

View File

@ -0,0 +1,17 @@
export function report(event) {
const bench = event.target;
const timeMs = bench.stats.mean * 1000;
const time =
timeMs < 10
? `${Math.round(timeMs * 1000) / 1000}ms`
: `${Math.round(timeMs)}ms`;
const msg = `${bench.name}: ${formatNumber(bench.hz)} ops/sec ±${
Math.round(bench.stats.rme * 100) / 100
}% (${time})`;
console.log(msg);
}
function formatNumber(x) {
if (x < 100) return `${Math.round(x * 100) / 100}`;
return `${Math.round(x)}`.replace(/\d(?=(?:\d{3})+$)/g, "$&_");
}

View File

@ -24,10 +24,13 @@
"source-map": "^0.5.0"
},
"devDependencies": {
"@babel-baseline/generator": "npm:@babel/generator@7.14.5",
"@babel/helper-fixtures": "workspace:*",
"@babel/parser": "workspace:*",
"@types/jsesc": "^2.5.0",
"@types/source-map": "^0.5.0"
"@types/source-map": "^0.5.0",
"benchmark": "^2.1.4",
"charcodes": "^0.2.0"
},
"engines": {
"node": ">=6.9.0"

View File

@ -1,23 +1,16 @@
import type SourceMap from "./source-map";
import type * as t from "@babel/types";
import * as charcodes from "charcodes";
const SPACES_RE = /^[ \t]+$/;
/**
* The Buffer class exists to manage the queue of tokens being pushed onto the output string
* in such a way that the final string buffer is treated as write-only until the final .get()
* call. This allows V8 to optimize the output efficiently by not requiring it to store the
* string in contiguous memory.
*/
export default class Buffer {
constructor(map?: SourceMap | null) {
this._map = map;
}
_map: SourceMap = null;
_buf: Array<any> = [];
_last: string = "";
_buf: string = "";
_last: number = 0;
_queue: Array<
[
str: string,
@ -52,7 +45,7 @@ export default class Buffer {
const result = {
// Whatever trim is used here should not execute a regex against the
// source string since it may be arbitrarily large after all transformations
code: this._buf.join("").trimRight(),
code: this._buf.trimRight(),
map: null,
rawMappings: map?.getRawMappings(),
};
@ -125,8 +118,8 @@ export default class Buffer {
filename?: string | null,
force?: boolean,
): void {
this._buf.push(str);
this._last = str[str.length - 1];
this._buf += str;
this._last = str.charCodeAt(str.length - 1);
// Search for newline chars. We search only for `\n`, since both `\r` and
// `\r\n` are normalized to `\n` during parse. We exclude `\u2028` and
@ -187,29 +180,39 @@ export default class Buffer {
}
}
endsWith(suffix: string): boolean {
// Fast path to avoid iterating over this._queue.
if (suffix.length === 1) {
let last;
if (this._queue.length > 0) {
const str = this._queue[0][0];
last = str[str.length - 1];
getLastChar(): number {
let last;
if (this._queue.length > 0) {
const str = this._queue[0][0];
last = str.charCodeAt(0);
} else {
last = this._last;
}
return last;
}
/**
* check if current _last + queue ends with newline, return the character before newline
*
* @param {*} ch
* @memberof Buffer
*/
endsWithCharAndNewline(): number {
const queue = this._queue;
if (queue.length > 0) {
const last = queue[0][0];
// every element in queue is one-length whitespace string
const lastCp = last.charCodeAt(0);
if (lastCp !== charcodes.lineFeed) return;
if (queue.length > 1) {
const secondLast = queue[1][0];
return secondLast.charCodeAt(0);
} else {
last = this._last;
return this._last;
}
return last === suffix;
}
const end =
this._last + this._queue.reduce((acc, item) => item[0] + acc, "");
if (suffix.length <= end.length) {
return end.slice(-suffix.length) === suffix;
}
// We assume that everything being matched is at most a single token plus some whitespace,
// which everything currently is, but otherwise we'd have to expand _last or check _buf.
return false;
}
hasContent(): boolean {

View File

@ -1,5 +1,6 @@
import type Printer from "../printer";
import * as t from "@babel/types";
import * as charCodes from "charcodes";
export function File(this: Printer, node: t.File) {
if (node.program) {
@ -37,7 +38,7 @@ export function BlockStatement(this: Printer, node: t.BlockStatement) {
this.source("end", node.loc);
if (!this.endsWith("\n")) this.newline();
if (!this.endsWith(charCodes.lineFeed)) this.newline();
this.rightBrace();
} else {

View File

@ -1,5 +1,8 @@
import type Printer from "../printer";
import * as t from "@babel/types";
import * as charCodes from "charcodes";
const { isExportDefaultDeclaration, isExportNamedDeclaration } = t;
export function ClassDeclaration(
this: Printer,
@ -8,8 +11,7 @@ export function ClassDeclaration(
) {
if (
!this.format.decoratorsBeforeExport ||
(!t.isExportDefaultDeclaration(parent) &&
!t.isExportNamedDeclaration(parent))
(!isExportDefaultDeclaration(parent) && !isExportNamedDeclaration(parent))
) {
this.printJoin(node.decorators, node);
}
@ -68,7 +70,7 @@ export function ClassBody(this: Printer, node: t.ClassBody) {
this.printSequence(node.body, node);
this.dedent();
if (!this.endsWith("\n")) this.newline();
if (!this.endsWith(charCodes.lineFeed)) this.newline();
this.rightBrace();
}

View File

@ -2,6 +2,7 @@ import type Printer from "../printer";
import * as t from "@babel/types";
import * as n from "../node";
const { isCallExpression, isLiteral, isMemberExpression, isNewExpression } = t;
export function UnaryExpression(this: Printer, node: t.UnaryExpression) {
if (
node.operator === "void" ||
@ -77,9 +78,9 @@ export function NewExpression(
this.format.minified &&
node.arguments.length === 0 &&
!node.optional &&
!t.isCallExpression(parent, { callee: node }) &&
!t.isMemberExpression(parent) &&
!t.isNewExpression(parent)
!isCallExpression(parent, { callee: node }) &&
!isMemberExpression(parent) &&
!isNewExpression(parent)
) {
return;
}
@ -119,13 +120,13 @@ export function OptionalMemberExpression(
) {
this.print(node.object, node);
if (!node.computed && t.isMemberExpression(node.property)) {
if (!node.computed && isMemberExpression(node.property)) {
throw new TypeError("Got a MemberExpression for MemberExpression property");
}
let computed = node.computed;
// @ts-expect-error todo(flow->ts) maybe instead of typeof check specific literal types?
if (t.isLiteral(node.property) && typeof node.property.value === "number") {
if (isLiteral(node.property) && typeof node.property.value === "number") {
computed = true;
}
if (node.optional) {
@ -266,13 +267,13 @@ export {
export function MemberExpression(this: Printer, node: t.MemberExpression) {
this.print(node.object, node);
if (!node.computed && t.isMemberExpression(node.property)) {
if (!node.computed && isMemberExpression(node.property)) {
throw new TypeError("Got a MemberExpression for MemberExpression property");
}
let computed = node.computed;
// @ts-expect-error todo(flow->ts) maybe use specific literal types
if (t.isLiteral(node.property) && typeof node.property.value === "number") {
if (isLiteral(node.property) && typeof node.property.value === "number") {
computed = true;
}

View File

@ -2,6 +2,7 @@ import type Printer from "../printer";
import * as t from "@babel/types";
import { ExportAllDeclaration } from "./modules";
const { isDeclareExportDeclaration, isStatement } = t;
export function AnyTypeAnnotation(this: Printer) {
this.word("any");
}
@ -35,7 +36,7 @@ export function DeclareClass(
node: t.DeclareClass,
parent: t.Node,
) {
if (!t.isDeclareExportDeclaration(parent)) {
if (!isDeclareExportDeclaration(parent)) {
this.word("declare");
this.space();
}
@ -49,7 +50,7 @@ export function DeclareFunction(
node: t.DeclareFunction,
parent: any,
) {
if (!t.isDeclareExportDeclaration(parent)) {
if (!isDeclareExportDeclaration(parent)) {
this.word("declare");
this.space();
}
@ -119,7 +120,7 @@ export function DeclareOpaqueType(
node: t.DeclareOpaqueType,
parent: any,
) {
if (!t.isDeclareExportDeclaration(parent)) {
if (!isDeclareExportDeclaration(parent)) {
this.word("declare");
this.space();
}
@ -131,7 +132,7 @@ export function DeclareVariable(
node: t.DeclareVariable,
parent: any,
) {
if (!t.isDeclareExportDeclaration(parent)) {
if (!isDeclareExportDeclaration(parent)) {
this.word("declare");
this.space();
}
@ -261,7 +262,7 @@ function FlowExportDeclaration(node: any) {
if (node.declaration) {
const declar = node.declaration;
this.print(declar, node);
if (!t.isStatement(declar)) this.semicolon();
if (!isStatement(declar)) this.semicolon();
} else {
this.token("{");
if (node.specifiers.length) {

View File

@ -1,6 +1,7 @@
import type Printer from "../printer";
import * as t from "@babel/types";
const { isIdentifier } = t;
export function _params(this: Printer, node: any) {
this.print(node.typeParameters, node);
this.token("(");
@ -120,7 +121,7 @@ export function ArrowFunctionExpression(
!this.format.auxiliaryCommentBefore &&
!this.format.auxiliaryCommentAfter &&
node.params.length === 1 &&
t.isIdentifier(firstParam) &&
isIdentifier(firstParam) &&
!hasTypesOrComments(node, firstParam)
) {
this.print(firstParam, node);

View File

@ -1,6 +1,15 @@
import type Printer from "../printer";
import * as t from "@babel/types";
const {
isClassDeclaration,
isExportDefaultSpecifier,
isExportNamespaceSpecifier,
isImportDefaultSpecifier,
isImportNamespaceSpecifier,
isStatement,
} = t;
export function ImportSpecifier(this: Printer, node: t.ImportSpecifier) {
if (node.importKind === "type" || node.importKind === "typeof") {
this.word(node.importKind);
@ -78,7 +87,7 @@ export function ExportNamedDeclaration(
) {
if (
this.format.decoratorsBeforeExport &&
t.isClassDeclaration(node.declaration)
isClassDeclaration(node.declaration)
) {
this.printJoin(node.declaration.decorators, node);
}
@ -94,7 +103,7 @@ export function ExportDefaultDeclaration(
) {
if (
this.format.decoratorsBeforeExport &&
t.isClassDeclaration(node.declaration)
isClassDeclaration(node.declaration)
) {
this.printJoin(node.declaration.decorators, node);
}
@ -110,7 +119,7 @@ function ExportDeclaration(node: any) {
if (node.declaration) {
const declar = node.declaration;
this.print(declar, node);
if (!t.isStatement(declar)) this.semicolon();
if (!isStatement(declar)) this.semicolon();
} else {
if (node.exportKind === "type") {
this.word("type");
@ -124,8 +133,8 @@ function ExportDeclaration(node: any) {
for (;;) {
const first = specifiers[0];
if (
t.isExportDefaultSpecifier(first) ||
t.isExportNamespaceSpecifier(first)
isExportDefaultSpecifier(first) ||
isExportNamespaceSpecifier(first)
) {
hasSpecial = true;
this.print(specifiers.shift(), node);
@ -175,8 +184,8 @@ export function ImportDeclaration(this: Printer, node: t.ImportDeclaration) {
for (;;) {
const first = specifiers[0];
if (
t.isImportDefaultSpecifier(first) ||
t.isImportNamespaceSpecifier(first)
isImportDefaultSpecifier(first) ||
isImportNamespaceSpecifier(first)
) {
this.print(specifiers.shift(), node);
if (specifiers.length) {

View File

@ -1,6 +1,8 @@
import type Printer from "../printer";
import * as t from "@babel/types";
import * as charCodes from "charcodes";
const { isFor, isForStatement, isIfStatement, isStatement } = t;
export function WithStatement(this: Printer, node: t.WithStatement) {
this.word("with");
this.space();
@ -19,7 +21,7 @@ export function IfStatement(this: Printer, node: t.IfStatement) {
this.space();
const needsBlock =
node.alternate && t.isIfStatement(getLastStatement(node.consequent));
node.alternate && isIfStatement(getLastStatement(node.consequent));
if (needsBlock) {
this.token("{");
this.newline();
@ -35,7 +37,7 @@ export function IfStatement(this: Printer, node: t.IfStatement) {
}
if (node.alternate) {
if (this.endsWith("}")) this.space();
if (this.endsWith(charCodes.rightCurlyBrace)) this.space();
this.word("else");
this.space();
this.printAndIndentOnComments(node.alternate, node);
@ -44,7 +46,7 @@ export function IfStatement(this: Printer, node: t.IfStatement) {
// Recursively get the last statement.
function getLastStatement(statement) {
if (!t.isStatement(statement.body)) return statement;
if (!isStatement(statement.body)) return statement;
return getLastStatement(statement.body);
}
@ -229,14 +231,18 @@ function variableDeclarationIndent() {
// "let " or "var " indentation.
this.token(",");
this.newline();
if (this.endsWith("\n")) for (let i = 0; i < 4; i++) this.space(true);
if (this.endsWith(charCodes.lineFeed)) {
for (let i = 0; i < 4; i++) this.space(true);
}
}
function constDeclarationIndent() {
// "const " indentation.
this.token(",");
this.newline();
if (this.endsWith("\n")) for (let i = 0; i < 6; i++) this.space(true);
if (this.endsWith(charCodes.lineFeed)) {
for (let i = 0; i < 6; i++) this.space(true);
}
}
export function VariableDeclaration(
@ -255,7 +261,7 @@ export function VariableDeclaration(
let hasInits = false;
// don't add whitespace to loop heads
if (!t.isFor(parent)) {
if (!isFor(parent)) {
for (const declar of node.declarations as Array<any>) {
if (declar.init) {
// has an init so let's split it up over multiple lines
@ -288,9 +294,9 @@ export function VariableDeclaration(
this.printList(node.declarations, node, { separator });
if (t.isFor(parent)) {
if (isFor(parent)) {
// don't give semicolons to these nodes since they'll be inserted in the parent generator
if (t.isForStatement(parent)) {
if (isForStatement(parent)) {
if (parent.init === node) return;
} else {
if (parent.left === node) return;

View File

@ -2,6 +2,7 @@ import type Printer from "../printer";
import * as t from "@babel/types";
import jsesc from "jsesc";
const { isAssignmentPattern, isIdentifier } = t;
export function Identifier(this: Printer, node: t.Identifier) {
this.exactSource(node.loc, () => {
this.word(node.name);
@ -53,8 +54,8 @@ export function ObjectProperty(this: Printer, node: t.ObjectProperty) {
} else {
// print `({ foo: foo = 5 } = {})` as `({ foo = 5 } = {});`
if (
t.isAssignmentPattern(node.value) &&
t.isIdentifier(node.key) &&
isAssignmentPattern(node.value) &&
isIdentifier(node.key) &&
// @ts-expect-error todo(flow->ts) `.name` does not exist on some types in union
node.key.name === node.value.left.name
) {
@ -67,8 +68,8 @@ export function ObjectProperty(this: Printer, node: t.ObjectProperty) {
// shorthand!
if (
node.shorthand &&
t.isIdentifier(node.key) &&
t.isIdentifier(node.value) &&
isIdentifier(node.key) &&
isIdentifier(node.value) &&
node.key.name === node.value.name
) {
return;

View File

@ -1,7 +1,12 @@
import * as whitespace from "./whitespace";
import * as parens from "./parentheses";
import * as t from "@babel/types";
const {
isCallExpression,
isExpressionStatement,
isMemberExpression,
isNewExpression,
} = t;
function expandAliases(obj) {
const newObj = {};
@ -42,17 +47,17 @@ function find(obj, node, parent, printStack?) {
}
function isOrHasCallExpression(node) {
if (t.isCallExpression(node)) {
if (isCallExpression(node)) {
return true;
}
return t.isMemberExpression(node) && isOrHasCallExpression(node.object);
return isMemberExpression(node) && isOrHasCallExpression(node.object);
}
export function needsWhitespace(node, parent, type) {
if (!node) return 0;
if (t.isExpressionStatement(node)) {
if (isExpressionStatement(node)) {
node = node.expression;
}
@ -86,7 +91,7 @@ export function needsWhitespaceAfter(node, parent) {
export function needsParens(node, parent, printStack?) {
if (!parent) return false;
if (t.isNewExpression(parent) && parent.callee === node) {
if (isNewExpression(parent) && parent.callee === node) {
if (isOrHasCallExpression(node)) return true;
}

View File

@ -1,5 +1,54 @@
import * as t from "@babel/types";
const {
isArrayTypeAnnotation,
isArrowFunctionExpression,
isAssignmentExpression,
isAwaitExpression,
isBinary,
isBinaryExpression,
isCallExpression,
isClassDeclaration,
isClassExpression,
isConditional,
isConditionalExpression,
isExportDeclaration,
isExportDefaultDeclaration,
isExpressionStatement,
isFor,
isForInStatement,
isForOfStatement,
isForStatement,
isIfStatement,
isIndexedAccessType,
isIntersectionTypeAnnotation,
isLogicalExpression,
isMemberExpression,
isNewExpression,
isNullableTypeAnnotation,
isObjectPattern,
isOptionalCallExpression,
isOptionalMemberExpression,
isReturnStatement,
isSequenceExpression,
isSwitchStatement,
isTSArrayType,
isTSAsExpression,
isTSIntersectionType,
isTSNonNullExpression,
isTSOptionalType,
isTSRestType,
isTSTypeAssertion,
isTSUnionType,
isTaggedTemplateExpression,
isThrowStatement,
isTypeAnnotation,
isUnaryLike,
isUnionTypeAnnotation,
isVariableDeclarator,
isWhileStatement,
isYieldExpression,
} = t;
const PRECEDENCE = {
"||": 0,
"??": 0,
@ -29,21 +78,21 @@ const PRECEDENCE = {
};
const isClassExtendsClause = (node: any, parent: any): boolean =>
(t.isClassDeclaration(parent) || t.isClassExpression(parent)) &&
(isClassDeclaration(parent) || isClassExpression(parent)) &&
parent.superClass === node;
const hasPostfixPart = (node: any, parent: any) =>
((t.isMemberExpression(parent) || t.isOptionalMemberExpression(parent)) &&
((isMemberExpression(parent) || isOptionalMemberExpression(parent)) &&
parent.object === node) ||
((t.isCallExpression(parent) ||
t.isOptionalCallExpression(parent) ||
t.isNewExpression(parent)) &&
((isCallExpression(parent) ||
isOptionalCallExpression(parent) ||
isNewExpression(parent)) &&
parent.callee === node) ||
(t.isTaggedTemplateExpression(parent) && parent.tag === node) ||
t.isTSNonNullExpression(parent);
(isTaggedTemplateExpression(parent) && parent.tag === node) ||
isTSNonNullExpression(parent);
export function NullableTypeAnnotation(node: any, parent: any): boolean {
return t.isArrayTypeAnnotation(parent);
return isArrayTypeAnnotation(parent);
}
export function FunctionTypeAnnotation(
@ -53,15 +102,15 @@ export function FunctionTypeAnnotation(
): boolean {
return (
// (() => A) | (() => B)
t.isUnionTypeAnnotation(parent) ||
isUnionTypeAnnotation(parent) ||
// (() => A) & (() => B)
t.isIntersectionTypeAnnotation(parent) ||
isIntersectionTypeAnnotation(parent) ||
// (() => A)[]
t.isArrayTypeAnnotation(parent) ||
isArrayTypeAnnotation(parent) ||
// <T>(A: T): (T => T[]) => B => [A, B]
(t.isTypeAnnotation(parent) &&
(isTypeAnnotation(parent) &&
// Check grandparent
t.isArrowFunctionExpression(printStack[printStack.length - 3]))
isArrowFunctionExpression(printStack[printStack.length - 3]))
);
}
@ -94,7 +143,7 @@ export function DoExpression(
export function Binary(node: any, parent: any): boolean {
if (
node.operator === "**" &&
t.isBinaryExpression(parent, { operator: "**" })
isBinaryExpression(parent, { operator: "**" })
) {
return parent.left === node;
}
@ -105,13 +154,13 @@ export function Binary(node: any, parent: any): boolean {
if (
hasPostfixPart(node, parent) ||
t.isUnaryLike(parent) ||
t.isAwaitExpression(parent)
isUnaryLike(parent) ||
isAwaitExpression(parent)
) {
return true;
}
if (t.isBinary(parent)) {
if (isBinary(parent)) {
const parentOp = parent.operator;
const parentPos = PRECEDENCE[parentOp];
@ -122,7 +171,7 @@ export function Binary(node: any, parent: any): boolean {
// Logical expressions with the same precedence don't need parens.
(parentPos === nodePos &&
parent.right === node &&
!t.isLogicalExpression(parent)) ||
!isLogicalExpression(parent)) ||
parentPos > nodePos
) {
return true;
@ -132,17 +181,17 @@ export function Binary(node: any, parent: any): boolean {
export function UnionTypeAnnotation(node: any, parent: any): boolean {
return (
t.isArrayTypeAnnotation(parent) ||
t.isNullableTypeAnnotation(parent) ||
t.isIntersectionTypeAnnotation(parent) ||
t.isUnionTypeAnnotation(parent)
isArrayTypeAnnotation(parent) ||
isNullableTypeAnnotation(parent) ||
isIntersectionTypeAnnotation(parent) ||
isUnionTypeAnnotation(parent)
);
}
export { UnionTypeAnnotation as IntersectionTypeAnnotation };
export function OptionalIndexedAccessType(node: any, parent: any): boolean {
return t.isIndexedAccessType(parent, { objectType: node });
return isIndexedAccessType(parent, { objectType: node });
}
export function TSAsExpression() {
@ -155,26 +204,25 @@ export function TSTypeAssertion() {
export function TSUnionType(node: any, parent: any): boolean {
return (
t.isTSArrayType(parent) ||
t.isTSOptionalType(parent) ||
t.isTSIntersectionType(parent) ||
t.isTSUnionType(parent) ||
t.isTSRestType(parent)
isTSArrayType(parent) ||
isTSOptionalType(parent) ||
isTSIntersectionType(parent) ||
isTSUnionType(parent) ||
isTSRestType(parent)
);
}
export { TSUnionType as TSIntersectionType };
export function TSInferType(node: any, parent: any): boolean {
return t.isTSArrayType(parent) || t.isTSOptionalType(parent);
return isTSArrayType(parent) || isTSOptionalType(parent);
}
export function BinaryExpression(node: any, parent: any): boolean {
// let i = (1 in []);
// for ((1 in []);;);
return (
node.operator === "in" &&
(t.isVariableDeclarator(parent) || t.isFor(parent))
node.operator === "in" && (isVariableDeclarator(parent) || isFor(parent))
);
}
@ -184,14 +232,14 @@ export function SequenceExpression(node: any, parent: any): boolean {
// expressions in the head of for loops, traditional style
// dictates that e.g. i++, j++ should not be wrapped with
// parentheses.
t.isForStatement(parent) ||
t.isThrowStatement(parent) ||
t.isReturnStatement(parent) ||
(t.isIfStatement(parent) && parent.test === node) ||
(t.isWhileStatement(parent) && parent.test === node) ||
(t.isForInStatement(parent) && parent.right === node) ||
(t.isSwitchStatement(parent) && parent.discriminant === node) ||
(t.isExpressionStatement(parent) && parent.expression === node)
isForStatement(parent) ||
isThrowStatement(parent) ||
isReturnStatement(parent) ||
(isIfStatement(parent) && parent.test === node) ||
(isWhileStatement(parent) && parent.test === node) ||
(isForInStatement(parent) && parent.right === node) ||
(isSwitchStatement(parent) && parent.discriminant === node) ||
(isExpressionStatement(parent) && parent.expression === node)
) {
return false;
}
@ -203,11 +251,11 @@ export function SequenceExpression(node: any, parent: any): boolean {
export function YieldExpression(node: any, parent: any): boolean {
return (
t.isBinary(parent) ||
t.isUnaryLike(parent) ||
isBinary(parent) ||
isUnaryLike(parent) ||
hasPostfixPart(node, parent) ||
(t.isAwaitExpression(parent) && t.isYieldExpression(node)) ||
(t.isConditionalExpression(parent) && node === parent.test) ||
(isAwaitExpression(parent) && isYieldExpression(node)) ||
(isConditionalExpression(parent) && node === parent.test) ||
isClassExtendsClause(node, parent)
);
}
@ -228,7 +276,7 @@ export function ClassExpression(
export function UnaryLike(node: any, parent: any): boolean {
return (
hasPostfixPart(node, parent) ||
t.isBinaryExpression(parent, { operator: "**", left: node }) ||
isBinaryExpression(parent, { operator: "**", left: node }) ||
isClassExtendsClause(node, parent)
);
}
@ -245,17 +293,17 @@ export function FunctionExpression(
}
export function ArrowFunctionExpression(node: any, parent: any): boolean {
return t.isExportDeclaration(parent) || ConditionalExpression(node, parent);
return isExportDeclaration(parent) || ConditionalExpression(node, parent);
}
export function ConditionalExpression(node: any, parent?): boolean {
if (
t.isUnaryLike(parent) ||
t.isBinary(parent) ||
t.isConditionalExpression(parent, { test: node }) ||
t.isAwaitExpression(parent) ||
t.isTSTypeAssertion(parent) ||
t.isTSAsExpression(parent)
isUnaryLike(parent) ||
isBinary(parent) ||
isConditionalExpression(parent, { test: node }) ||
isAwaitExpression(parent) ||
isTSTypeAssertion(parent) ||
isTSAsExpression(parent)
) {
return true;
}
@ -265,15 +313,15 @@ export function ConditionalExpression(node: any, parent?): boolean {
export function OptionalMemberExpression(node: any, parent: any): boolean {
return (
t.isCallExpression(parent, { callee: node }) ||
t.isMemberExpression(parent, { object: node })
isCallExpression(parent, { callee: node }) ||
isMemberExpression(parent, { object: node })
);
}
export { OptionalMemberExpression as OptionalCallExpression };
export function AssignmentExpression(node: any, parent: any): boolean {
if (t.isObjectPattern(node.left)) {
if (isObjectPattern(node.left)) {
return true;
} else {
return ConditionalExpression(node, parent);
@ -283,12 +331,12 @@ export function AssignmentExpression(node: any, parent: any): boolean {
export function LogicalExpression(node: any, parent: any): boolean {
switch (node.operator) {
case "||":
if (!t.isLogicalExpression(parent)) return false;
if (!isLogicalExpression(parent)) return false;
return parent.operator === "??" || parent.operator === "&&";
case "&&":
return t.isLogicalExpression(parent, { operator: "??" });
return isLogicalExpression(parent, { operator: "??" });
case "??":
return t.isLogicalExpression(parent) && parent.operator !== "??";
return isLogicalExpression(parent) && parent.operator !== "??";
}
}
@ -303,11 +351,11 @@ export function Identifier(
// Some contexts only forbid `let [`, so check if the next token would
// be the left bracket of a computed member expression.
const isFollowedByBracket =
t.isMemberExpression(parent, {
isMemberExpression(parent, {
object: node,
computed: true,
}) ||
t.isOptionalMemberExpression(parent, {
isOptionalMemberExpression(parent, {
object: node,
computed: true,
optional: false,
@ -329,7 +377,7 @@ export function Identifier(
// some tools (including earlier Babel versions) can't parse
// `for await (async of [])` without them.
return (
node.name === "async" && t.isForOfStatement(parent) && node === parent.left
node.name === "async" && isForOfStatement(parent) && node === parent.left
);
}
@ -353,23 +401,23 @@ function isFirstInContext(
while (i >= 0) {
if (
(expressionStatement &&
t.isExpressionStatement(parent, { expression: node })) ||
isExpressionStatement(parent, { expression: node })) ||
(exportDefault &&
t.isExportDefaultDeclaration(parent, { declaration: node })) ||
(arrowBody && t.isArrowFunctionExpression(parent, { body: node })) ||
(forHead && t.isForStatement(parent, { init: node })) ||
(forInHead && t.isForInStatement(parent, { left: node })) ||
(forOfHead && t.isForOfStatement(parent, { left: node }))
isExportDefaultDeclaration(parent, { declaration: node })) ||
(arrowBody && isArrowFunctionExpression(parent, { body: node })) ||
(forHead && isForStatement(parent, { init: node })) ||
(forInHead && isForInStatement(parent, { left: node })) ||
(forOfHead && isForOfStatement(parent, { left: node }))
) {
return true;
}
if (
(hasPostfixPart(node, parent) && !t.isNewExpression(parent)) ||
(t.isSequenceExpression(parent) && parent.expressions[0] === node) ||
t.isConditional(parent, { test: node }) ||
t.isBinary(parent, { left: node }) ||
t.isAssignmentExpression(parent, { left: node })
(hasPostfixPart(node, parent) && !isNewExpression(parent)) ||
(isSequenceExpression(parent) && parent.expressions[0] === node) ||
isConditional(parent, { test: node }) ||
isBinary(parent, { left: node }) ||
isAssignmentExpression(parent, { left: node })
) {
node = parent;
i--;

View File

@ -1,5 +1,20 @@
import * as t from "@babel/types";
const {
isArrayExpression,
isAssignmentExpression,
isBinary,
isBlockStatement,
isCallExpression,
isFunction,
isIdentifier,
isLiteral,
isMemberExpression,
isObjectExpression,
isOptionalCallExpression,
isOptionalMemberExpression,
isStringLiteral,
} = t;
type WhitespaceObject = {
before?: boolean;
after?: boolean;
@ -17,18 +32,18 @@ function crawl(
node: t.Node,
state: { hasCall?: boolean; hasFunction?: boolean; hasHelper?: boolean } = {},
) {
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
if (isMemberExpression(node) || isOptionalMemberExpression(node)) {
crawl(node.object, state);
if (node.computed) crawl(node.property, state);
} else if (t.isBinary(node) || t.isAssignmentExpression(node)) {
} else if (isBinary(node) || isAssignmentExpression(node)) {
crawl(node.left, state);
crawl(node.right, state);
} else if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
} else if (isCallExpression(node) || isOptionalCallExpression(node)) {
state.hasCall = true;
crawl(node.callee, state);
} else if (t.isFunction(node)) {
} else if (isFunction(node)) {
state.hasFunction = true;
} else if (t.isIdentifier(node)) {
} else if (isIdentifier(node)) {
// @ts-expect-error todo(flow->ts): node.callee is not really expected here…
state.hasHelper = state.hasHelper || isHelper(node.callee);
}
@ -41,15 +56,15 @@ function crawl(
*/
function isHelper(node: t.Node): boolean {
if (t.isMemberExpression(node)) {
if (isMemberExpression(node)) {
return isHelper(node.object) || isHelper(node.property);
} else if (t.isIdentifier(node)) {
} else if (isIdentifier(node)) {
return node.name === "require" || node.name[0] === "_";
} else if (t.isCallExpression(node)) {
} else if (isCallExpression(node)) {
return isHelper(node.callee);
} else if (t.isBinary(node) || t.isAssignmentExpression(node)) {
} else if (isBinary(node) || isAssignmentExpression(node)) {
return (
(t.isIdentifier(node.left) && isHelper(node.left)) || isHelper(node.right)
(isIdentifier(node.left) && isHelper(node.left)) || isHelper(node.right)
);
} else {
return false;
@ -58,11 +73,11 @@ function isHelper(node: t.Node): boolean {
function isType(node) {
return (
t.isLiteral(node) ||
t.isObjectExpression(node) ||
t.isArrayExpression(node) ||
t.isIdentifier(node) ||
t.isMemberExpression(node)
isLiteral(node) ||
isObjectExpression(node) ||
isArrayExpression(node) ||
isIdentifier(node) ||
isMemberExpression(node)
);
}
@ -114,7 +129,7 @@ export const nodes: {
*/
LogicalExpression(node: t.LogicalExpression): WhitespaceObject | undefined {
if (t.isFunction(node.left) || t.isFunction(node.right)) {
if (isFunction(node.left) || isFunction(node.right)) {
return {
after: true,
};
@ -126,7 +141,7 @@ export const nodes: {
*/
Literal(node: t.Literal): WhitespaceObject | undefined | null {
if (t.isStringLiteral(node) && node.value === "use strict") {
if (isStringLiteral(node) && node.value === "use strict") {
return {
after: true,
};
@ -138,7 +153,7 @@ export const nodes: {
*/
CallExpression(node: t.CallExpression): WhitespaceObject | undefined | null {
if (t.isFunction(node.callee) || isHelper(node)) {
if (isFunction(node.callee) || isHelper(node)) {
return {
before: true,
after: true,
@ -149,7 +164,7 @@ export const nodes: {
OptionalCallExpression(
node: t.OptionalCallExpression,
): WhitespaceObject | undefined | null {
if (t.isFunction(node.callee)) {
if (isFunction(node.callee)) {
return {
before: true,
after: true,
@ -187,7 +202,7 @@ export const nodes: {
*/
IfStatement(node: t.IfStatement): WhitespaceObject | undefined | null {
if (t.isBlockStatement(node.consequent)) {
if (isBlockStatement(node.consequent)) {
return {
before: true,
after: true,

View File

@ -4,12 +4,16 @@ import * as t from "@babel/types";
import * as generatorFunctions from "./generators";
import type SourceMap from "./source-map";
import * as charCodes from "charcodes";
const SCIENTIFIC_NOTATION = /e/i;
const ZERO_DECIMAL_INTEGER = /\.0+$/;
const NON_DECIMAL_LITERAL = /^0[box]/;
const PURE_ANNOTATION_RE = /^\s*[@#]__PURE__\s*$/;
const { isProgram, isFile, isEmptyStatement } = t;
const { needsParens, needsWhitespaceAfter, needsWhitespaceBefore } = n;
export type Format = {
shouldPrintComment: (comment: string) => boolean;
retainLines: boolean;
@ -105,11 +109,13 @@ class Printer {
space(force: boolean = false): void {
if (this.format.compact) return;
if (
(this._buf.hasContent() && !this.endsWith(" ") && !this.endsWith("\n")) ||
force
) {
if (force) {
this._space();
} else if (this._buf.hasContent()) {
const lastCp = this.getLastChar();
if (lastCp !== charCodes.space && lastCp !== charCodes.lineFeed) {
this._space();
}
}
}
@ -119,7 +125,10 @@ class Printer {
word(str: string): void {
// prevent concatenating words and creating // comment out of division and regex
if (this._endsWithWord || (this.endsWith("/") && str.indexOf("/") === 0)) {
if (
this._endsWithWord ||
(this.endsWith(charCodes.slash) && str.charCodeAt(0) === charCodes.slash)
) {
this._space();
}
@ -143,7 +152,7 @@ class Printer {
!NON_DECIMAL_LITERAL.test(str) &&
!SCIENTIFIC_NOTATION.test(str) &&
!ZERO_DECIMAL_INTEGER.test(str) &&
str[str.length - 1] !== ".";
str.charCodeAt(str.length - 1) !== charCodes.dot;
}
/**
@ -153,13 +162,15 @@ class Printer {
token(str: string): void {
// space is mandatory to avoid outputting <!--
// http://javascript.spec.whatwg.org/#comment-syntax
const lastChar = this.getLastChar();
const strFirst = str.charCodeAt(0);
if (
(str === "--" && this.endsWith("!")) ||
(str === "--" && lastChar === charCodes.exclamationMark) ||
// Need spaces for operators of the same kind to avoid: `a+++b`
(str[0] === "+" && this.endsWith("+")) ||
(str[0] === "-" && this.endsWith("-")) ||
(strFirst === charCodes.plusSign && lastChar === charCodes.plusSign) ||
(strFirst === charCodes.dash && lastChar === charCodes.dash) ||
// Needs spaces to avoid changing '34' to '34.', which would still be a valid number.
(str[0] === "." && this._endsWithInteger)
(strFirst === charCodes.dot && this._endsWithInteger)
) {
this._space();
}
@ -172,7 +183,7 @@ class Printer {
* Add a newline (or many newlines), maintaining formatting.
*/
newline(i?: number): void {
newline(i: number = 1): void {
if (this.format.retainLines || this.format.compact) return;
if (this.format.concise) {
@ -180,13 +191,16 @@ class Printer {
return;
}
const charBeforeNewline = this.endsWithCharAndNewline();
// never allow more than two lines
if (this.endsWith("\n\n")) return;
if (charBeforeNewline === charCodes.lineFeed) return;
if (typeof i !== "number") i = 1;
i = Math.min(2, i);
if (this.endsWith("{\n") || this.endsWith(":\n")) i--;
if (
charBeforeNewline === charCodes.leftCurlyBrace ||
charBeforeNewline === charCodes.colon
) {
i--;
}
if (i <= 0) return;
for (let j = 0; j < i; j++) {
@ -194,8 +208,16 @@ class Printer {
}
}
endsWith(str: string): boolean {
return this._buf.endsWith(str);
endsWith(char: number): boolean {
return this.getLastChar() === char;
}
getLastChar(): number {
return this._buf.getLastChar();
}
endsWithCharAndNewline(): number {
return this._buf.endsWithCharAndNewline();
}
removeTrailingNewline(): void {
@ -241,7 +263,11 @@ class Printer {
_maybeIndent(str: string): void {
// we've got a newline before us so prepend on the indentation
if (this._indent && this.endsWith("\n") && str[0] !== "\n") {
if (
this._indent &&
this.endsWith(charCodes.lineFeed) &&
str.charCodeAt(0) !== charCodes.lineFeed
) {
this._buf.queue(this._getIndent());
}
}
@ -391,27 +417,27 @@ class Printer {
this._insideAux = !node.loc;
this._maybeAddAuxComment(this._insideAux && !oldInAux);
let needsParens = n.needsParens(node, parent, this._printStack);
let shouldPrintParens = needsParens(node, parent, this._printStack);
if (
this.format.retainFunctionParens &&
node.type === "FunctionExpression" &&
node.extra &&
node.extra.parenthesized
) {
needsParens = true;
shouldPrintParens = true;
}
if (needsParens) this.token("(");
if (shouldPrintParens) this.token("(");
this._printLeadingComments(node);
const loc = t.isProgram(node) || t.isFile(node) ? null : node.loc;
const loc = isProgram(node) || isFile(node) ? null : node.loc;
this.withSource("start", loc, () => {
printMethod.call(this, node, parent);
});
this._printTrailingComments(node);
if (needsParens) this.token(")");
if (shouldPrintParens) this.token(")");
// end
this._printStack.pop();
@ -504,7 +530,7 @@ class Printer {
printBlock(parent) {
const node = parent.body;
if (!t.isEmptyStatement(node)) {
if (!isEmptyStatement(node)) {
this.space();
}
@ -572,11 +598,11 @@ class Printer {
if (!leading) lines++; // always include at least a single line after
if (opts.addNewlines) lines += opts.addNewlines(leading, node) || 0;
const needs = leading ? n.needsWhitespaceBefore : n.needsWhitespaceAfter;
const needs = leading ? needsWhitespaceBefore : needsWhitespaceAfter;
if (needs(node, parent)) lines++;
}
this.newline(lines);
this.newline(Math.min(2, lines));
}
_getComments(leading, node) {
@ -606,7 +632,13 @@ class Printer {
if (printNewLines && this._buf.hasContent()) this.newline(1);
if (!this.endsWith("[") && !this.endsWith("{")) this.space();
const lastCharCode = this.getLastChar();
if (
lastCharCode !== charCodes.leftSquareBracket &&
lastCharCode !== charCodes.leftCurlyBrace
) {
this.space();
}
let val =
!isBlockComment && !this._noLineTerminator
@ -628,7 +660,7 @@ class Printer {
}
// Avoid creating //* comments
if (this.endsWith("/")) this._space();
if (this.endsWith(charCodes.slash)) this._space();
this.withSource("start", comment.loc, () => {
this._append(val);
@ -648,7 +680,7 @@ class Printer {
this._printComment(
comments[0],
// Keep newlines if the comment marks a standalone call
this._buf.hasContent() && !this.endsWith("\n"),
this._buf.hasContent() && !this.endsWith(charCodes.lineFeed),
);
} else {
for (const comment of comments) {

View File

@ -5,6 +5,17 @@ __metadata:
version: 4
cacheKey: 7
"@babel-baseline/generator@npm:@babel/generator@7.14.5, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.14.5, @babel/generator@npm:^7.7.2":
version: 7.14.5
resolution: "@babel/generator@npm:7.14.5"
dependencies:
"@babel/types": ^7.14.5
jsesc: ^2.5.1
source-map: ^0.5.0
checksum: 3ba48b75f7680d17b4c3657063339252cf63ea0038b05e24d1611dff2c8f136fc8ca5cb1c293fbc1abc79b153e264bc23a01dc5f440030282e4da0631f12e0b7
languageName: node
linkType: hard
"@babel-baseline/parser@npm:@babel/parser@^7.14.5":
version: 7.14.5
resolution: "@babel/parser@npm:7.14.5"
@ -331,26 +342,18 @@ __metadata:
languageName: unknown
linkType: soft
"@babel/generator@npm:^7.12.5, @babel/generator@npm:^7.14.5, @babel/generator@npm:^7.7.2":
version: 7.14.5
resolution: "@babel/generator@npm:7.14.5"
dependencies:
"@babel/types": ^7.14.5
jsesc: ^2.5.1
source-map: ^0.5.0
checksum: 3ba48b75f7680d17b4c3657063339252cf63ea0038b05e24d1611dff2c8f136fc8ca5cb1c293fbc1abc79b153e264bc23a01dc5f440030282e4da0631f12e0b7
languageName: node
linkType: hard
"@babel/generator@workspace:*, @babel/generator@workspace:^7.14.5, @babel/generator@workspace:^7.14.8, @babel/generator@workspace:packages/babel-generator":
version: 0.0.0-use.local
resolution: "@babel/generator@workspace:packages/babel-generator"
dependencies:
"@babel-baseline/generator": "npm:@babel/generator@7.14.5"
"@babel/helper-fixtures": "workspace:*"
"@babel/parser": "workspace:*"
"@babel/types": "workspace:^7.14.8"
"@types/jsesc": ^2.5.0
"@types/source-map": ^0.5.0
benchmark: ^2.1.4
charcodes: ^0.2.0
jsesc: "condition: BABEL_8_BREAKING ? ^3.0.2 : ^2.5.1"
source-map: ^0.5.0
languageName: unknown
@ -657,6 +660,7 @@ __metadata:
resolution: "@babel/helper-module-transforms@condition:BABEL_8_BREAKING?:workspace:^7.14.8#d03fdb"
dependencies:
"@babel/helper-module-transforms-BABEL_8_BREAKING-false": "npm:@babel/helper-module-transforms@workspace:^7.14.8"
checksum: dc554f178a54829c3510a740ff36c3bd4f29967c3daa7e22ee91eab4db892becafb538e433bcfada2cf82c57576c25ae7700fa29e5c780b7cc661697bd16de56
languageName: node
linkType: hard