246 lines
6.4 KiB
TypeScript

import SourceMap from "./source-map";
import Printer from "./printer";
import type * as t from "@babel/types";
import type { Format } from "./printer";
/**
* Babel's code generator, turns an ast into code, maintaining sourcemaps,
* user preferences, and valid output.
*/
class Generator extends Printer {
constructor(ast: t.Node, opts: { sourceMaps?: boolean } = {}, code) {
const format = normalizeOptions(code, opts);
const map = opts.sourceMaps ? new SourceMap(opts, code) : null;
super(format, map);
this.ast = ast;
}
ast: t.Node;
/**
* Generate code and sourcemap from ast.
*
* Appends comments that weren't attached to any node to the end of the generated output.
*/
generate() {
return super.generate(this.ast);
}
}
/**
* Normalize generator options, setting defaults.
*
* - Detects code indentation.
* - If `opts.compact = "auto"` and the code is over 500KB, `compact` will be set to `true`.
*/
function normalizeOptions(code, opts): Format {
const format: Format = {
auxiliaryCommentBefore: opts.auxiliaryCommentBefore,
auxiliaryCommentAfter: opts.auxiliaryCommentAfter,
shouldPrintComment: opts.shouldPrintComment,
retainLines: opts.retainLines,
retainFunctionParens: opts.retainFunctionParens,
comments: opts.comments == null || opts.comments,
compact: opts.compact,
minified: opts.minified,
concise: opts.concise,
indent: {
adjustMultilineComment: true,
style: " ",
base: 0,
},
decoratorsBeforeExport: !!opts.decoratorsBeforeExport,
jsescOption: {
quotes: "double",
wrap: true,
minimal: process.env.BABEL_8_BREAKING ? true : false,
...opts.jsescOption,
},
recordAndTupleSyntaxType: opts.recordAndTupleSyntaxType,
};
if (!process.env.BABEL_8_BREAKING) {
format.jsonCompatibleStrings = opts.jsonCompatibleStrings;
}
if (format.minified) {
format.compact = true;
format.shouldPrintComment =
format.shouldPrintComment || (() => format.comments);
} else {
format.shouldPrintComment =
format.shouldPrintComment ||
(value =>
format.comments ||
value.indexOf("@license") >= 0 ||
value.indexOf("@preserve") >= 0);
}
if (format.compact === "auto") {
format.compact = code.length > 500_000; // 500KB
if (format.compact) {
console.error(
"[BABEL] Note: The code generator has deoptimised the styling of " +
`${opts.filename} as it exceeds the max of ${"500KB"}.`,
);
}
}
if (format.compact) {
format.indent.adjustMultilineComment = false;
}
return format;
}
export interface GeneratorOptions {
/**
* Optional string to add as a block comment at the start of the output file.
*/
auxiliaryCommentBefore?: string;
/**
* Optional string to add as a block comment at the end of the output file.
*/
auxiliaryCommentAfter?: string;
/**
* Function that takes a comment (as a string) and returns true if the comment should be included in the output.
* By default, comments are included if `opts.comments` is `true` or if `opts.minifed` is `false` and the comment
* contains `@preserve` or `@license`.
*/
shouldPrintComment?(comment: string): boolean;
/**
* Attempt to use the same line numbers in the output code as in the source code (helps preserve stack traces).
* Defaults to `false`.
*/
retainLines?: boolean;
/**
* Retain parens around function expressions (could be used to change engine parsing behavior)
* Defaults to `false`.
*/
retainFunctionParens?: boolean;
/**
* Should comments be included in output? Defaults to `true`.
*/
comments?: boolean;
/**
* Set to true to avoid adding whitespace for formatting. Defaults to the value of `opts.minified`.
*/
compact?: boolean | "auto";
/**
* Should the output be minified. Defaults to `false`.
*/
minified?: boolean;
/**
* Set to true to reduce whitespace (but not as much as opts.compact). Defaults to `false`.
*/
concise?: boolean;
/**
* Used in warning messages
*/
filename?: string;
/**
* Enable generating source maps. Defaults to `false`.
*/
sourceMaps?: boolean;
/**
* A root for all relative URLs in the source map.
*/
sourceRoot?: string;
/**
* The filename for the source code (i.e. the code in the `code` argument).
* This will only be used if `code` is a string.
*/
sourceFileName?: string;
/**
* Set to true to run jsesc with "json": true to print "\u00A9" vs. "©";
*/
jsonCompatibleStrings?: boolean;
/**
* Set to true to enable support for experimental decorators syntax before module exports.
* Defaults to `false`.
*/
decoratorsBeforeExport?: boolean;
/**
* Options for outputting jsesc representation.
*/
jsescOption?: {
/**
* The type of quote to use in the output. If omitted, autodetects based on `ast.tokens`.
*/
quotes?: "single" | "double";
/**
* When enabled, the output is a valid JavaScript string literal wrapped in quotes. The type of quotes can be specified through the quotes setting.
* Defaults to `true`.
*/
wrap?: boolean;
};
}
export interface GeneratorResult {
code: string;
map: {
version: number;
sources: string[];
names: string[];
sourceRoot?: string;
sourcesContent?: string[];
mappings: string;
file: string;
} | null;
}
/**
* We originally exported the Generator class above, but to make it extra clear that it is a private API,
* we have moved that to an internal class instance and simplified the interface to the two public methods
* that we wish to support.
*/
export class CodeGenerator {
private _generator: Generator;
constructor(ast: t.Node, opts?: GeneratorOptions, code?: string) {
this._generator = new Generator(ast, opts, code);
}
generate(): GeneratorResult {
return this._generator.generate();
}
}
/**
* Turns an AST into code, maintaining sourcemaps, user preferences, and valid output.
* @param ast - the abstract syntax tree from which to generate output code.
* @param opts - used for specifying options for code generation.
* @param code - the original source code, used for source maps.
* @returns - an object containing the output code and source map.
*/
export default function generate(
ast: t.Node,
opts?: GeneratorOptions,
code?: string | { [filename: string]: string },
): any {
const gen = new Generator(ast, opts, code);
return gen.generate();
}