Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hide internal @babel/core functions in config errors #11554

Merged
merged 13 commits into from Aug 27, 2022
56 changes: 38 additions & 18 deletions packages/babel-core/src/config/config-chain.ts
Expand Up @@ -14,6 +14,9 @@ import pathPatternToRegex from "./pattern-to-regex";
import { ConfigPrinter, ChainFormatter } from "./printer";
import type { ReadonlyDeepArray } from "./helpers/deep-array";

import { endHiddenCallStack } from "../errors/rewrite-stack-trace";
import ConfigError from "../errors/config-error";

const debug = buildDebug("babel:config:config-chain");

import {
Expand Down Expand Up @@ -51,7 +54,7 @@ export type PresetInstance = {
};

export type ConfigContext = {
filename: string | void;
filename: string | undefined;
cwd: string;
root: string;
envName: string;
Expand Down Expand Up @@ -329,23 +332,23 @@ const validateConfigFile = makeWeakCacheSync(
(file: ConfigFile): ValidatedFile => ({
filepath: file.filepath,
dirname: file.dirname,
options: validate("configfile", file.options),
options: validate("configfile", file.options, file.filepath),
}),
);

const validateBabelrcFile = makeWeakCacheSync(
(file: ConfigFile): ValidatedFile => ({
filepath: file.filepath,
dirname: file.dirname,
options: validate("babelrcfile", file.options),
options: validate("babelrcfile", file.options, file.filepath),
}),
);

const validateExtendFile = makeWeakCacheSync(
(file: ConfigFile): ValidatedFile => ({
filepath: file.filepath,
dirname: file.dirname,
options: validate("extendsfile", file.options),
options: validate("extendsfile", file.options, file.filepath),
}),
);

Expand Down Expand Up @@ -528,7 +531,11 @@ function buildOverrideEnvDescriptors(
}

function makeChainWalker<
ArgT extends { options: ValidatedOptions; dirname: string },
ArgT extends {
options: ValidatedOptions;
dirname: string;
filepath?: string;
},
>({
root,
env,
Expand Down Expand Up @@ -559,7 +566,7 @@ function makeChainWalker<
files?: Set<ConfigFile>,
baseLogger?: ConfigPrinter,
) => Handler<ConfigChain | null> {
return function* (input, context, files = new Set(), baseLogger) {
return function* chainWalker(input, context, files = new Set(), baseLogger) {
const { dirname } = input;

const flattenedConfigs: Array<{
Expand All @@ -569,15 +576,18 @@ function makeChainWalker<
}> = [];

const rootOpts = root(input);
if (configIsApplicable(rootOpts, dirname, context)) {
if (configIsApplicable(rootOpts, dirname, context, input.filepath)) {
flattenedConfigs.push({
config: rootOpts,
envName: undefined,
index: undefined,
});

const envOpts = env(input, context.envName);
if (envOpts && configIsApplicable(envOpts, dirname, context)) {
if (
envOpts &&
configIsApplicable(envOpts, dirname, context, input.filepath)
) {
flattenedConfigs.push({
config: envOpts,
envName: context.envName,
Expand All @@ -587,7 +597,7 @@ function makeChainWalker<

(rootOpts.options.overrides || []).forEach((_, index) => {
const overrideOps = overrides(input, index);
if (configIsApplicable(overrideOps, dirname, context)) {
if (configIsApplicable(overrideOps, dirname, context, input.filepath)) {
flattenedConfigs.push({
config: overrideOps,
index,
Expand All @@ -597,7 +607,12 @@ function makeChainWalker<
const overrideEnvOpts = overridesEnv(input, index, context.envName);
if (
overrideEnvOpts &&
configIsApplicable(overrideEnvOpts, dirname, context)
configIsApplicable(
overrideEnvOpts,
dirname,
context,
input.filepath,
)
) {
flattenedConfigs.push({
config: overrideEnvOpts,
Expand Down Expand Up @@ -789,25 +804,27 @@ function configIsApplicable(
{ options }: OptionsAndDescriptors,
dirname: string,
context: ConfigContext,
configName: string,
): boolean {
return (
(options.test === undefined ||
configFieldIsApplicable(context, options.test, dirname)) &&
configFieldIsApplicable(context, options.test, dirname, configName)) &&
(options.include === undefined ||
configFieldIsApplicable(context, options.include, dirname)) &&
configFieldIsApplicable(context, options.include, dirname, configName)) &&
(options.exclude === undefined ||
!configFieldIsApplicable(context, options.exclude, dirname))
!configFieldIsApplicable(context, options.exclude, dirname, configName))
);
}

function configFieldIsApplicable(
context: ConfigContext,
test: ConfigApplicableTest,
dirname: string,
configName: string,
): boolean {
const patterns = Array.isArray(test) ? test : [test];

return matchesPatterns(context, patterns, dirname);
return matchesPatterns(context, patterns, dirname, configName);
}

/**
Expand Down Expand Up @@ -872,29 +889,32 @@ function matchesPatterns(
context: ConfigContext,
patterns: IgnoreList,
dirname: string,
configName?: string,
): boolean {
return patterns.some(pattern =>
matchPattern(pattern, dirname, context.filename, context),
matchPattern(pattern, dirname, context.filename, context, configName),
);
}

function matchPattern(
pattern: IgnoreItem,
dirname: string,
pathToTest: unknown,
pathToTest: string | undefined,
context: ConfigContext,
configName?: string,
): boolean {
if (typeof pattern === "function") {
return !!pattern(pathToTest, {
return !!endHiddenCallStack(pattern)(pathToTest, {
dirname,
envName: context.envName,
caller: context.caller,
});
}

if (typeof pathToTest !== "string") {
throw new Error(
throw new ConfigError(
`Configuration contains string/RegExp pattern, but no filename was passed to Babel`,
configName,
);
}

Expand Down
54 changes: 34 additions & 20 deletions packages/babel-core/src/config/files/configuration.ts
Expand Up @@ -13,10 +13,12 @@ import loadCjsOrMjsDefault from "./module-types";
import pathPatternToRegex from "../pattern-to-regex";
import type { FilePackageData, RelativeConfig, ConfigFile } from "./types";
import type { CallerMetadata } from "../validation/options";
import ConfigError from "../../errors/config-error";

import * as fs from "../../gensync-utils/fs";

import { createRequire } from "module";
import { endHiddenCallStack } from "../../errors/rewrite-stack-trace";
const require = createRequire(import.meta.url);

const debug = buildDebug("babel:config:loading:files:configuration");
Expand Down Expand Up @@ -112,7 +114,7 @@ function* loadOneConfig(
);
const config = configs.reduce((previousConfig: ConfigFile | null, config) => {
if (config && previousConfig) {
throw new Error(
throw new ConfigError(
`Multiple configuration files found. Please remove one:\n` +
` - ${path.basename(previousConfig.filepath)}\n` +
` - ${config.filepath}\n` +
Expand All @@ -139,7 +141,10 @@ export function* loadConfig(

const conf = yield* readConfig(filepath, envName, caller);
if (!conf) {
throw new Error(`Config file ${filepath} contains no configuration data`);
throw new ConfigError(
`Config file contains no configuration data`,
filepath,
);
}

debug("Loaded config %o from %o.", name, dirname);
Expand Down Expand Up @@ -197,9 +202,6 @@ const readConfigJS = makeStrongCache(function* readConfigJS(
"You appear to be using a native ECMAScript module configuration " +
"file, which is only supported when running Babel asynchronously.",
);
} catch (err) {
err.message = `${filepath}: Error while loading config - ${err.message}`;
throw err;
} finally {
LOADING_CONFIGS.delete(filepath);
}
Expand All @@ -209,29 +211,33 @@ const readConfigJS = makeStrongCache(function* readConfigJS(
// @ts-expect-error - if we want to make it possible to use async configs
yield* [];

options = (options as any as (api: ConfigAPI) => {})(makeConfigAPI(cache));
options = endHiddenCallStack(options as any as (api: ConfigAPI) => {})(
makeConfigAPI(cache),
);

assertCache = true;
}

if (!options || typeof options !== "object" || Array.isArray(options)) {
throw new Error(
`${filepath}: Configuration should be an exported JavaScript object.`,
throw new ConfigError(
`Configuration should be an exported JavaScript object.`,
filepath,
);
}

// @ts-expect-error todo(flow->ts)
if (typeof options.then === "function") {
throw new Error(
throw new ConfigError(
`You appear to be using an async configuration, ` +
`which your current version of Babel does not support. ` +
`We may add support for this in the future, ` +
`but if you're on the most recent version of @babel/core and still ` +
`seeing this error, then you'll need to synchronously return your config.`,
filepath,
);
}

if (assertCache && !cache.configured()) throwConfigError();
if (assertCache && !cache.configured()) throwConfigError(filepath);

return {
filepath,
Expand All @@ -247,7 +253,7 @@ const packageToBabelConfig = makeWeakCacheSync(
if (typeof babel === "undefined") return null;

if (typeof babel !== "object" || Array.isArray(babel) || babel === null) {
throw new Error(`${file.filepath}: .babel property must be an object`);
throw new ConfigError(`.babel property must be an object`, file.filepath);
}

return {
Expand All @@ -263,17 +269,19 @@ const readConfigJSON5 = makeStaticFileCache((filepath, content): ConfigFile => {
try {
options = json5.parse(content);
} catch (err) {
err.message = `${filepath}: Error while parsing config - ${err.message}`;
throw err;
throw new ConfigError(
`Error while parsing config - ${err.message}`,
filepath,
);
}

if (!options) throw new Error(`${filepath}: No config detected`);
if (!options) throw new ConfigError(`No config detected`, filepath);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there is some lint rule to make it a normal string, but in this PR I think it's ok.🤫

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's @typescript-eslint/quotes, which can be configured with allowTemplateLiterals: false to avoid unnecessary template literals (despite the name, it does not forbid all usage of template literals). To be compatible with Prettier, avoidEscape: true must also be set (even if you extend eslint-config-prettier, individual rule configurations take precedence).


if (typeof options !== "object") {
throw new Error(`${filepath}: Config returned typeof ${typeof options}`);
throw new ConfigError(`Config returned typeof ${typeof options}`, filepath);
}
if (Array.isArray(options)) {
throw new Error(`${filepath}: Expected config object but found array`);
throw new ConfigError(`Expected config object but found array`, filepath);
}

delete options["$schema"];
Expand All @@ -294,7 +302,10 @@ const readIgnoreConfig = makeStaticFileCache((filepath, content) => {

for (const pattern of ignorePatterns) {
if (pattern[0] === "!") {
throw new Error(`Negation of file paths is not supported.`);
throw new ConfigError(
`Negation of file paths is not supported.`,
filepath,
);
}
}

Expand Down Expand Up @@ -324,8 +335,9 @@ export function* resolveShowConfigPath(
return null;
}

function throwConfigError(): never {
throw new Error(`\
function throwConfigError(filepath: string): never {
throw new ConfigError(
`\
Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured
for various types of caching, using the first param of their handler functions:

Expand Down Expand Up @@ -358,5 +370,7 @@ module.exports = function(api) {

// Return the value that will be cached.
return { };
};`);
};`,
filepath,
);
}
12 changes: 8 additions & 4 deletions packages/babel-core/src/config/files/module-types.ts
Expand Up @@ -5,6 +5,9 @@ import { pathToFileURL } from "url";
import { createRequire } from "module";
import semver from "semver";

import { endHiddenCallStack } from "../../errors/rewrite-stack-trace";
import ConfigError from "../../errors/config-error";

const require = createRequire(import.meta.url);

let import_: ((specifier: string | URL) => any) | undefined;
Expand Down Expand Up @@ -40,7 +43,7 @@ export default function* loadCjsOrMjsDefault(
if (yield* isAsync()) {
return yield* waitFor(loadMjsDefault(filepath));
}
throw new Error(asyncError);
throw new ConfigError(asyncError, filepath);
}
}

Expand All @@ -56,7 +59,7 @@ function guessJSModuleType(filename: string): "cjs" | "mjs" | "unknown" {
}

function loadCjsDefault(filepath: string, fallbackToTranspiledModule: boolean) {
const module = require(filepath) as any;
const module = endHiddenCallStack(require)(filepath) as any;
return module?.__esModule
? // TODO (Babel 8): Remove "module" and "undefined" fallback
module.default || (fallbackToTranspiledModule ? module : undefined)
Expand All @@ -65,14 +68,15 @@ function loadCjsDefault(filepath: string, fallbackToTranspiledModule: boolean) {

async function loadMjsDefault(filepath: string) {
if (!import_) {
throw new Error(
throw new ConfigError(
"Internal error: Native ECMAScript modules aren't supported" +
" by this platform.\n",
filepath,
);
}

// import() expects URLs, not file paths.
// https://github.com/nodejs/node/issues/31710
const module = await import_(pathToFileURL(filepath));
const module = await endHiddenCallStack(import_)(pathToFileURL(filepath));
return module.default;
}