Skip to content

Commit

Permalink
Hide internal @babel/core functions in config errors
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed May 12, 2020
1 parent 31b361b commit 686dfa8
Show file tree
Hide file tree
Showing 19 changed files with 487 additions and 57 deletions.
46 changes: 31 additions & 15 deletions packages/babel-core/src/config/config-chain.js
Expand Up @@ -13,6 +13,9 @@ import {
} from "./validation/options";
import pathPatternToRegex from "./pattern-to-regex";

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 @@ -281,23 +284,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 @@ -429,29 +432,37 @@ function makeChainWalker<ArgT: { options: ValidatedOptions, dirname: string }>({
ConfigContext,
Set<ConfigFile> | void,
) => Handler<ConfigChain | null> {
return function*(input, context, files = new Set()) {
return function* chainWalker(input, context, files = new Set()) {
const { dirname } = input;

const flattenedConfigs = [];

const rootOpts = root(input);
if (configIsApplicable(rootOpts, dirname, context)) {
if (configIsApplicable(rootOpts, dirname, context, input.filepath)) {
flattenedConfigs.push(rootOpts);

const envOpts = env(input, context.envName);
if (envOpts && configIsApplicable(envOpts, dirname, context)) {
if (
envOpts &&
configIsApplicable(envOpts, dirname, context, input.filepath)
) {
flattenedConfigs.push(envOpts);
}

(rootOpts.options.overrides || []).forEach((_, index) => {
const overrideOps = overrides(input, index);
if (configIsApplicable(overrideOps, dirname, context)) {
if (configIsApplicable(overrideOps, dirname, context, input.filepath)) {
flattenedConfigs.push(overrideOps);

const overrideEnvOpts = overridesEnv(input, index, context.envName);
if (
overrideEnvOpts &&
configIsApplicable(overrideEnvOpts, dirname, context)
configIsApplicable(
overrideEnvOpts,
dirname,
context,
input.filepath,
)
) {
flattenedConfigs.push(overrideEnvOpts);
}
Expand Down Expand Up @@ -620,25 +631,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 @@ -681,9 +694,10 @@ 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),
);
}

Expand All @@ -692,18 +706,20 @@ function matchPattern(
dirname,
pathToTest,
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
57 changes: 36 additions & 21 deletions packages/babel-core/src/config/files/configuration.js
Expand Up @@ -19,6 +19,9 @@ import type { CallerMetadata } from "../validation/options";
import * as fs from "../../gensync-utils/fs";
import resolve from "../../gensync-utils/resolve";

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

const debug = buildDebug("babel:config:loading:files:configuration");

export const ROOT_CONFIG_FILENAMES = [
Expand Down Expand Up @@ -113,7 +116,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 @@ -140,7 +143,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 @@ -194,38 +200,39 @@ const readConfigJS = makeStrongCache(function* readConfigJS(
"You appear to be using a native ECMAScript module configuration " +
"file, which is only supported when running Babel asynchronously.",
): mixed);
} catch (err) {
err.message = `${filepath}: Error while loading config - ${err.message}`;
throw err;
} finally {
LOADING_CONFIGS.delete(filepath);
}

let assertCache = false;
if (typeof options === "function") {
yield* []; // if we want to make it possible to use async configs
options = ((options: any): (api: PluginAPI) => {})(makeAPI(cache));
options = endHiddenCallStack(((options: any): (api: PluginAPI) => {}))(
makeAPI(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,
);
}

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()) throwCacheError(filepath);

return {
filepath,
Expand All @@ -241,7 +248,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 @@ -257,17 +264,19 @@ const readConfigJSON5 = makeStaticFileCache((filepath, content) => {
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);

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);
}

return {
Expand All @@ -286,7 +295,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 All @@ -299,9 +311,10 @@ const readIgnoreConfig = makeStaticFileCache((filepath, content) => {
};
});

function throwConfigError() {
throw new Error(`\
Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured
function throwCacheError(filepath) {
throw new ConfigError(
`\
Caching was left unconfigured. Babel's plugins, presets, and configuration files can be configured
for various types of caching, using the first param of their handler functions:
module.exports = function(api) {
Expand Down Expand Up @@ -333,5 +346,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.js
Expand Up @@ -3,6 +3,9 @@ import type { Handler } from "gensync";
import path from "path";
import { pathToFileURL } from "url";

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

let import_;
try {
// Node < 13.3 doesn't support import() syntax.
Expand All @@ -27,7 +30,7 @@ export default function* loadCjsOrMjsDefault(
if (yield* isAsync()) {
return yield* waitFor(loadMjsDefault(filepath));
}
throw new Error(asyncError);
throw new ConfigError(asyncError);
}
}

Expand All @@ -43,21 +46,22 @@ function guessJSModuleType(filename: string): "cjs" | "mjs" | "unknown" {
}

function loadCjsDefault(filepath: string) {
const module = (require(filepath): mixed);
const module = (endHiddenCallStack(require)(filepath): mixed);
// TODO (Babel 8): Remove "undefined" fallback
return module?.__esModule ? module.default || undefined : module;
}

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;
}
15 changes: 11 additions & 4 deletions packages/babel-core/src/config/files/package.js
Expand Up @@ -6,6 +6,8 @@ import { makeStaticFileCache } from "./utils";

import type { ConfigFile, FilePackageData } from "./types";

import ConfigError from "../../errors/config-error";

const PACKAGE_FILENAME = "package.json";

/**
Expand Down Expand Up @@ -41,15 +43,20 @@ const readConfigPackage = makeStaticFileCache(
try {
options = JSON.parse(content);
} catch (err) {
err.message = `${filepath}: Error while parsing JSON - ${err.message}`;
throw err;
throw new ConfigError(
`Error while parsing JSON - ${err.message}`,
filepath,
);
}

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);
}

return {
Expand Down
4 changes: 3 additions & 1 deletion packages/babel-core/src/config/files/plugins.js
Expand Up @@ -8,6 +8,8 @@ import buildDebug from "debug";
import resolve from "resolve";
import path from "path";

import ConfigError from "../../errors/config-error";

const debug = buildDebug("babel:config:loading:files:plugins");

const EXACT_RE = /^module:/;
Expand All @@ -33,7 +35,7 @@ export function loadPlugin(
): { filepath: string, value: mixed } {
const filepath = resolvePlugin(name, dirname);
if (!filepath) {
throw new Error(`Plugin ${name} not found relative to ${dirname}`);
throw new ConfigError(`Plugin ${name} not found relative to ${dirname}`);
}

const value = requireModule("plugin", filepath);
Expand Down

0 comments on commit 686dfa8

Please sign in to comment.