Bogdan Savluk a647b9ea6b Convert @babel/core to TypeScript (#12929)
Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
2021-03-30 19:51:35 +02:00

466 lines
14 KiB
TypeScript

import type { InputTargets, Targets } from "@babel/helper-compilation-targets";
import type { ConfigItem } from "../item";
import Plugin from "../plugin";
import removed from "./removed";
import {
msg,
access,
assertString,
assertBoolean,
assertObject,
assertArray,
assertCallerMetadata,
assertInputSourceMap,
assertIgnoreList,
assertPluginList,
assertConfigApplicableTest,
assertConfigFileSearch,
assertBabelrcSearch,
assertFunction,
assertRootMode,
assertSourceMaps,
assertCompact,
assertSourceType,
assertTargets,
assertAssumptions,
} from "./option-assertions";
import type { ValidatorSet, Validator, OptionPath } from "./option-assertions";
import type { UnloadedDescriptor } from "../config-descriptors";
const ROOT_VALIDATORS: ValidatorSet = {
cwd: assertString as Validator<ValidatedOptions["cwd"]>,
root: assertString as Validator<ValidatedOptions["root"]>,
rootMode: assertRootMode as Validator<ValidatedOptions["rootMode"]>,
configFile: assertConfigFileSearch as Validator<
ValidatedOptions["configFile"]
>,
caller: assertCallerMetadata as Validator<ValidatedOptions["caller"]>,
filename: assertString as Validator<ValidatedOptions["filename"]>,
filenameRelative: assertString as Validator<
ValidatedOptions["filenameRelative"]
>,
code: assertBoolean as Validator<ValidatedOptions["code"]>,
ast: assertBoolean as Validator<ValidatedOptions["ast"]>,
cloneInputAst: assertBoolean as Validator<ValidatedOptions["cloneInputAst"]>,
envName: assertString as Validator<ValidatedOptions["envName"]>,
};
const BABELRC_VALIDATORS: ValidatorSet = {
babelrc: assertBoolean as Validator<ValidatedOptions["babelrc"]>,
babelrcRoots: assertBabelrcSearch as Validator<
ValidatedOptions["babelrcRoots"]
>,
};
const NONPRESET_VALIDATORS: ValidatorSet = {
extends: assertString as Validator<ValidatedOptions["extends"]>,
ignore: assertIgnoreList as Validator<ValidatedOptions["ignore"]>,
only: assertIgnoreList as Validator<ValidatedOptions["only"]>,
targets: assertTargets as Validator<ValidatedOptions["targets"]>,
browserslistConfigFile: assertConfigFileSearch as Validator<
ValidatedOptions["browserslistConfigFile"]
>,
browserslistEnv: assertString as Validator<
ValidatedOptions["browserslistEnv"]
>,
};
const COMMON_VALIDATORS: ValidatorSet = {
// TODO: Should 'inputSourceMap' be moved to be a root-only option?
// We may want a boolean-only version to be a common option, with the
// object only allowed as a root config argument.
inputSourceMap: assertInputSourceMap as Validator<
ValidatedOptions["inputSourceMap"]
>,
presets: assertPluginList as Validator<ValidatedOptions["presets"]>,
plugins: assertPluginList as Validator<ValidatedOptions["plugins"]>,
passPerPreset: assertBoolean as Validator<ValidatedOptions["passPerPreset"]>,
assumptions: assertAssumptions as Validator<ValidatedOptions["assumptions"]>,
env: assertEnvSet as Validator<ValidatedOptions["env"]>,
overrides: assertOverridesList as Validator<ValidatedOptions["overrides"]>,
// We could limit these to 'overrides' blocks, but it's not clear why we'd
// bother, when the ability to limit a config to a specific set of files
// is a fairly general useful feature.
test: assertConfigApplicableTest as Validator<ValidatedOptions["test"]>,
include: assertConfigApplicableTest as Validator<ValidatedOptions["include"]>,
exclude: assertConfigApplicableTest as Validator<ValidatedOptions["exclude"]>,
retainLines: assertBoolean as Validator<ValidatedOptions["retainLines"]>,
comments: assertBoolean as Validator<ValidatedOptions["comments"]>,
shouldPrintComment: assertFunction as Validator<
ValidatedOptions["shouldPrintComment"]
>,
compact: assertCompact as Validator<ValidatedOptions["compact"]>,
minified: assertBoolean as Validator<ValidatedOptions["minified"]>,
auxiliaryCommentBefore: assertString as Validator<
ValidatedOptions["auxiliaryCommentBefore"]
>,
auxiliaryCommentAfter: assertString as Validator<
ValidatedOptions["auxiliaryCommentAfter"]
>,
sourceType: assertSourceType as Validator<ValidatedOptions["sourceType"]>,
wrapPluginVisitorMethod: assertFunction as Validator<
ValidatedOptions["wrapPluginVisitorMethod"]
>,
highlightCode: assertBoolean as Validator<ValidatedOptions["highlightCode"]>,
sourceMaps: assertSourceMaps as Validator<ValidatedOptions["sourceMaps"]>,
sourceMap: assertSourceMaps as Validator<ValidatedOptions["sourceMap"]>,
sourceFileName: assertString as Validator<ValidatedOptions["sourceFileName"]>,
sourceRoot: assertString as Validator<ValidatedOptions["sourceRoot"]>,
parserOpts: assertObject as Validator<ValidatedOptions["parserOpts"]>,
generatorOpts: assertObject as Validator<ValidatedOptions["generatorOpts"]>,
};
if (!process.env.BABEL_8_BREAKING) {
Object.assign(COMMON_VALIDATORS, {
getModuleId: assertFunction,
moduleRoot: assertString,
moduleIds: assertBoolean,
moduleId: assertString,
});
}
export type InputOptions = ValidatedOptions;
export type ValidatedOptions = {
cwd?: string;
filename?: string;
filenameRelative?: string;
babelrc?: boolean;
babelrcRoots?: BabelrcSearch;
configFile?: ConfigFileSearch;
root?: string;
rootMode?: RootMode;
code?: boolean;
ast?: boolean;
cloneInputAst?: boolean;
inputSourceMap?: RootInputSourceMapOption;
envName?: string;
caller?: CallerMetadata;
extends?: string;
env?: EnvSet<ValidatedOptions>;
ignore?: IgnoreList;
only?: IgnoreList;
overrides?: OverridesList;
// Generally verify if a given config object should be applied to the given file.
test?: ConfigApplicableTest;
include?: ConfigApplicableTest;
exclude?: ConfigApplicableTest;
presets?: PluginList;
plugins?: PluginList;
passPerPreset?: boolean;
assumptions?: {
[name: string]: boolean;
};
// browserslists-related options
targets?: TargetsListOrObject;
browserslistConfigFile?: ConfigFileSearch;
browserslistEnv?: string;
// Options for @babel/generator
retainLines?: boolean;
comments?: boolean;
shouldPrintComment?: Function;
compact?: CompactOption;
minified?: boolean;
auxiliaryCommentBefore?: string;
auxiliaryCommentAfter?: string;
// Parser
sourceType?: SourceTypeOption;
wrapPluginVisitorMethod?: Function;
highlightCode?: boolean;
// Sourcemap generation options.
sourceMaps?: SourceMapsOption;
sourceMap?: SourceMapsOption;
sourceFileName?: string;
sourceRoot?: string;
// Deprecate top level parserOpts
parserOpts?: {};
// Deprecate top level generatorOpts
generatorOpts?: {};
};
export type NormalizedOptions = {
readonly targets: Targets;
} & Omit<ValidatedOptions, "targets">;
export type CallerMetadata = {
// If 'caller' is specified, require that the name is given for debugging
// messages.
name: string;
};
export type EnvSet<T> = {
[x: string]: T;
};
export type IgnoreItem = string | Function | RegExp;
export type IgnoreList = ReadonlyArray<IgnoreItem>;
export type PluginOptions = object | void | false;
export type PluginTarget = string | object | Function;
export type PluginItem =
| ConfigItem
| Plugin
| PluginTarget
| [PluginTarget, PluginOptions]
| [PluginTarget, PluginOptions, string | void];
export type PluginList = ReadonlyArray<PluginItem>;
export type OverridesList = Array<ValidatedOptions>;
export type ConfigApplicableTest = IgnoreItem | Array<IgnoreItem>;
export type ConfigFileSearch = string | boolean;
export type BabelrcSearch = boolean | IgnoreItem | IgnoreList;
export type SourceMapsOption = boolean | "inline" | "both";
export type SourceTypeOption = "module" | "script" | "unambiguous";
export type CompactOption = boolean | "auto";
export type RootInputSourceMapOption = {} | boolean;
export type RootMode = "root" | "upward" | "upward-optional";
export type TargetsListOrObject =
| Targets
| InputTargets
| InputTargets["browsers"];
export type OptionsSource =
| "arguments"
| "configfile"
| "babelrcfile"
| "extendsfile"
| "preset"
| "plugin";
export type RootPath = Readonly<{
type: "root";
source: OptionsSource;
}>;
type OverridesPath = Readonly<{
type: "overrides";
index: number;
parent: RootPath;
}>;
type EnvPath = Readonly<{
type: "env";
name: string;
parent: RootPath | OverridesPath;
}>;
export type NestingPath = RootPath | OverridesPath | EnvPath;
export const assumptionsNames = new Set<string>([
"arrayLikeIsIterable",
"constantReexports",
"constantSuper",
"enumerableModuleMeta",
"ignoreFunctionLength",
"ignoreToPrimitiveHint",
"iterableIsArray",
"mutableTemplateObject",
"noClassCalls",
"noDocumentAll",
"noNewArrows",
"objectRestNoSymbols",
"privateFieldsAsProperties",
"pureGetters",
"setClassMethods",
"setComputedProperties",
"setPublicClassFields",
"setSpreadProperties",
"skipForOfIteratorClosing",
"superIsCallableConstructor",
]);
function getSource(loc: NestingPath): OptionsSource {
return loc.type === "root" ? loc.source : getSource(loc.parent);
}
export function validate(type: OptionsSource, opts: {}): ValidatedOptions {
return validateNested(
{
type: "root",
source: type,
},
opts,
);
}
function validateNested(loc: NestingPath, opts: {}) {
const type = getSource(loc);
assertNoDuplicateSourcemap(opts);
Object.keys(opts).forEach((key: string) => {
const optLoc = {
type: "option",
name: key,
parent: loc,
} as const;
if (type === "preset" && NONPRESET_VALIDATORS[key]) {
throw new Error(`${msg(optLoc)} is not allowed in preset options`);
}
if (type !== "arguments" && ROOT_VALIDATORS[key]) {
throw new Error(
`${msg(optLoc)} is only allowed in root programmatic options`,
);
}
if (
type !== "arguments" &&
type !== "configfile" &&
BABELRC_VALIDATORS[key]
) {
if (type === "babelrcfile" || type === "extendsfile") {
throw new Error(
`${msg(
optLoc,
)} is not allowed in .babelrc or "extends"ed files, only in root programmatic options, ` +
`or babel.config.js/config file options`,
);
}
throw new Error(
`${msg(
optLoc,
)} is only allowed in root programmatic options, or babel.config.js/config file options`,
);
}
const validator =
COMMON_VALIDATORS[key] ||
NONPRESET_VALIDATORS[key] ||
BABELRC_VALIDATORS[key] ||
ROOT_VALIDATORS[key] ||
(throwUnknownError as Validator<void>);
validator(optLoc, opts[key]);
});
return opts;
}
function throwUnknownError(loc: OptionPath) {
const key = loc.name;
if (removed[key]) {
const { message, version = 5 } = removed[key];
throw new Error(
`Using removed Babel ${version} option: ${msg(loc)} - ${message}`,
);
} else {
// eslint-disable-next-line max-len
const unknownOptErr = new Error(
`Unknown option: ${msg(
loc,
)}. Check out https://babeljs.io/docs/en/babel-core/#options for more information about options.`,
);
// @ts-expect-error todo(flow->ts): consider creating something like BabelConfigError with code field in it
unknownOptErr.code = "BABEL_UNKNOWN_OPTION";
throw unknownOptErr;
}
}
function has(obj: {}, key: string) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
function assertNoDuplicateSourcemap(opts: {}): void {
if (has(opts, "sourceMap") && has(opts, "sourceMaps")) {
throw new Error(".sourceMap is an alias for .sourceMaps, cannot use both");
}
}
function assertEnvSet(
loc: OptionPath,
value: unknown,
): void | EnvSet<ValidatedOptions> {
if (loc.parent.type === "env") {
throw new Error(`${msg(loc)} is not allowed inside of another .env block`);
}
const parent: RootPath | OverridesPath = loc.parent;
const obj = assertObject(loc, value);
if (obj) {
// Validate but don't copy the .env object in order to preserve
// object identity for use during config chain processing.
for (const envName of Object.keys(obj)) {
const env = assertObject(access(loc, envName), obj[envName]);
if (!env) continue;
const envLoc = {
type: "env",
name: envName,
parent,
} as const;
validateNested(envLoc, env);
}
}
return obj;
}
function assertOverridesList(
loc: OptionPath,
value: unknown[],
): undefined | OverridesList {
if (loc.parent.type === "env") {
throw new Error(`${msg(loc)} is not allowed inside an .env block`);
}
if (loc.parent.type === "overrides") {
throw new Error(`${msg(loc)} is not allowed inside an .overrides block`);
}
const parent: RootPath = loc.parent;
const arr = assertArray(loc, value);
if (arr) {
for (const [index, item] of arr.entries()) {
const objLoc = access(loc, index);
const env = assertObject(objLoc, item);
if (!env) throw new Error(`${msg(objLoc)} must be an object`);
const overridesLoc = {
type: "overrides",
index,
parent,
} as const;
validateNested(overridesLoc, env);
}
}
// @ts-expect-error
return arr;
}
export function checkNoUnwrappedItemOptionPairs(
items: Array<UnloadedDescriptor>,
index: number,
type: "plugin" | "preset",
e: Error,
): void {
if (index === 0) return;
const lastItem = items[index - 1];
const thisItem = items[index];
if (
lastItem.file &&
lastItem.options === undefined &&
typeof thisItem.value === "object"
) {
e.message +=
`\n- Maybe you meant to use\n` +
`"${type}": [\n ["${lastItem.file.request}", ${JSON.stringify(
thisItem.value,
undefined,
2,
)}]\n]\n` +
`To be a valid ${type}, its name and options should be wrapped in a pair of brackets`;
}
}