From b61826dc9200da4e58759a6ec084027292ade7c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Wed, 13 May 2020 00:21:57 +0200 Subject: [PATCH] Hide internal `@babel/core` functions in config errors --- .../babel-core/src/config/config-chain.ts | 52 +++-- .../src/config/files/configuration.ts | 54 +++-- .../src/config/files/module-types.ts | 12 +- .../babel-core/src/config/files/package.ts | 15 +- packages/babel-core/src/config/full.ts | 3 +- .../src/config/validation/options.ts | 28 ++- .../babel-core/src/errors/config-error.ts | 9 + .../src/errors/rewrite-stack-trace.ts | 153 ++++++++++++++ packages/babel-core/src/parse.ts | 14 +- packages/babel-core/test/errors-stacks.js | 189 ++++++++++++++++++ .../babel.config.js | 12 ++ .../errors/error-config-file/babel.config.js | 4 + .../babel.config.js | 11 + .../error-config-function/babel.config.js | 3 + .../errors/invalid-json/babel.config.json | 3 + .../errors/invalid-option/babel.config.json | 3 + .../errors/invalid-pkg-json/package.json | 3 + .../use-exclude-in-preset/babel.config.js | 4 + .../errors/use-exclude-in-preset/my-preset.js | 3 + .../errors/use-exclude/babel.config.js | 4 + .../fixtures/errors/valid/babel.config.json | 1 + 21 files changed, 523 insertions(+), 57 deletions(-) create mode 100644 packages/babel-core/src/errors/config-error.ts create mode 100644 packages/babel-core/src/errors/rewrite-stack-trace.ts create mode 100644 packages/babel-core/test/errors-stacks.js create mode 100644 packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js create mode 100644 packages/babel-core/test/fixtures/errors/error-config-file/babel.config.js create mode 100644 packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js create mode 100644 packages/babel-core/test/fixtures/errors/error-config-function/babel.config.js create mode 100644 packages/babel-core/test/fixtures/errors/invalid-json/babel.config.json create mode 100644 packages/babel-core/test/fixtures/errors/invalid-option/babel.config.json create mode 100644 packages/babel-core/test/fixtures/errors/invalid-pkg-json/package.json create mode 100644 packages/babel-core/test/fixtures/errors/use-exclude-in-preset/babel.config.js create mode 100644 packages/babel-core/test/fixtures/errors/use-exclude-in-preset/my-preset.js create mode 100644 packages/babel-core/test/fixtures/errors/use-exclude/babel.config.js create mode 100644 packages/babel-core/test/fixtures/errors/valid/babel.config.json diff --git a/packages/babel-core/src/config/config-chain.ts b/packages/babel-core/src/config/config-chain.ts index b3f8e4139400..83853974f597 100644 --- a/packages/babel-core/src/config/config-chain.ts +++ b/packages/babel-core/src/config/config-chain.ts @@ -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 { @@ -329,7 +332,7 @@ const validateConfigFile = makeWeakCacheSync( (file: ConfigFile): ValidatedFile => ({ filepath: file.filepath, dirname: file.dirname, - options: validate("configfile", file.options), + options: validate("configfile", file.options, file.filepath), }), ); @@ -337,7 +340,7 @@ const validateBabelrcFile = makeWeakCacheSync( (file: ConfigFile): ValidatedFile => ({ filepath: file.filepath, dirname: file.dirname, - options: validate("babelrcfile", file.options), + options: validate("babelrcfile", file.options, file.filepath), }), ); @@ -345,7 +348,7 @@ const validateExtendFile = makeWeakCacheSync( (file: ConfigFile): ValidatedFile => ({ filepath: file.filepath, dirname: file.dirname, - options: validate("extendsfile", file.options), + options: validate("extendsfile", file.options, file.filepath), }), ); @@ -528,7 +531,11 @@ function buildOverrideEnvDescriptors( } function makeChainWalker< - ArgT extends { options: ValidatedOptions; dirname: string }, + ArgT extends { + options: ValidatedOptions; + dirname: string; + filepath?: string; + }, >({ root, env, @@ -559,7 +566,7 @@ function makeChainWalker< files?: Set, baseLogger?: ConfigPrinter, ) => Handler { - return function* (input, context, files = new Set(), baseLogger) { + return function* chainWalker(input, context, files = new Set(), baseLogger) { const { dirname } = input; const flattenedConfigs: Array<{ @@ -569,7 +576,7 @@ function makeChainWalker< }> = []; const rootOpts = root(input); - if (configIsApplicable(rootOpts, dirname, context)) { + if (configIsApplicable(rootOpts, dirname, context, input.filepath)) { flattenedConfigs.push({ config: rootOpts, envName: undefined, @@ -577,7 +584,10 @@ function makeChainWalker< }); 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, @@ -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, @@ -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, @@ -789,14 +804,15 @@ 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)) ); } @@ -804,10 +820,11 @@ 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); } /** @@ -872,9 +889,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), ); } @@ -883,9 +901,10 @@ function matchPattern( dirname: string, pathToTest: unknown, context: ConfigContext, + configName?: string, ): boolean { if (typeof pattern === "function") { - return !!pattern(pathToTest, { + return !!endHiddenCallStack(pattern)(pathToTest, { dirname, envName: context.envName, caller: context.caller, @@ -893,8 +912,9 @@ function matchPattern( } if (typeof pathToTest !== "string") { - throw new Error( + throw new ConfigError( `Configuration contains string/RegExp pattern, but no filename was passed to Babel`, + configName, ); } diff --git a/packages/babel-core/src/config/files/configuration.ts b/packages/babel-core/src/config/files/configuration.ts index 23104ea7ce4e..3632aaa0313a 100644 --- a/packages/babel-core/src/config/files/configuration.ts +++ b/packages/babel-core/src/config/files/configuration.ts @@ -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"); @@ -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` + @@ -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); @@ -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); } @@ -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, @@ -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 { @@ -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); 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"]; @@ -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, + ); } } @@ -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: @@ -358,5 +370,7 @@ module.exports = function(api) { // Return the value that will be cached. return { }; -};`); +};`, + filepath, + ); } diff --git a/packages/babel-core/src/config/files/module-types.ts b/packages/babel-core/src/config/files/module-types.ts index 389054d381bf..1550ec33e8c8 100644 --- a/packages/babel-core/src/config/files/module-types.ts +++ b/packages/babel-core/src/config/files/module-types.ts @@ -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; @@ -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); } } @@ -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) @@ -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; } diff --git a/packages/babel-core/src/config/files/package.ts b/packages/babel-core/src/config/files/package.ts index c583db3e38b8..e83c86f1701e 100644 --- a/packages/babel-core/src/config/files/package.ts +++ b/packages/babel-core/src/config/files/package.ts @@ -4,6 +4,8 @@ import { makeStaticFileCache } from "./utils"; import type { ConfigFile, FilePackageData } from "./types"; +import ConfigError from "../../errors/config-error"; + const PACKAGE_FILENAME = "package.json"; /** @@ -39,17 +41,22 @@ const readConfigPackage = makeStaticFileCache( try { options = JSON.parse(content) as unknown; } catch (err) { - err.message = `${filepath}: Error while parsing JSON - ${err.message}`; - throw err; + throw new ConfigError( + `Error while parsing JSON - ${err.message}`, + filepath, + ); } if (!options) throw new Error(`${filepath}: No config detected`); 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 { diff --git a/packages/babel-core/src/config/full.ts b/packages/babel-core/src/config/full.ts index e6c222b63e39..ebb41cf34991 100644 --- a/packages/babel-core/src/config/full.ts +++ b/packages/babel-core/src/config/full.ts @@ -30,6 +30,7 @@ import loadPrivatePartialConfig from "./partial"; import type { ValidatedOptions } from "./validation/options"; import * as Context from "./cache-contexts"; +import ConfigError from "../errors/config-error"; type LoadedDescriptor = { value: {}; @@ -411,7 +412,7 @@ const validateIfOptionNeedsFilename = ( const formattedPresetName = descriptor.name ? `"${descriptor.name}"` : "/* your preset */"; - throw new Error( + throw new ConfigError( [ `Preset ${formattedPresetName} requires a filename to be set when babel is called directly,`, `\`\`\``, diff --git a/packages/babel-core/src/config/validation/options.ts b/packages/babel-core/src/config/validation/options.ts index 588892994bda..cd62d884f725 100644 --- a/packages/babel-core/src/config/validation/options.ts +++ b/packages/babel-core/src/config/validation/options.ts @@ -30,6 +30,7 @@ import type { ValidatorSet, Validator, OptionPath } from "./option-assertions"; import type { UnloadedDescriptor } from "../config-descriptors"; import type { ParserOptions } from "@babel/parser"; import type { GeneratorOptions } from "@babel/generator"; +import ConfigError from "../../errors/config-error"; const ROOT_VALIDATORS: ValidatorSet = { cwd: assertString as Validator, @@ -286,14 +287,25 @@ 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, - ); +export function validate( + type: OptionsSource, + opts: {}, + filename?: string, +): ValidatedOptions { + try { + return validateNested( + { + type: "root", + source: type, + }, + opts, + ); + } catch (error) { + const configError = new ConfigError(error.message, filename); + // @ts-expect-error TODO: .code is not defined on ConfigError or Error + if (error.code) configError.code = error.code; + throw configError; + } } function validateNested(loc: NestingPath, opts: { [key: string]: unknown }) { diff --git a/packages/babel-core/src/errors/config-error.ts b/packages/babel-core/src/errors/config-error.ts new file mode 100644 index 000000000000..01c0cb16aaeb --- /dev/null +++ b/packages/babel-core/src/errors/config-error.ts @@ -0,0 +1,9 @@ +import { injcectVirtualStackFrame, expectedError } from "./rewrite-stack-trace"; + +export default class ConfigError extends Error { + constructor(message: string, filename?: string) { + super(message); + expectedError(this); + if (filename) injcectVirtualStackFrame(this, filename); + } +} diff --git a/packages/babel-core/src/errors/rewrite-stack-trace.ts b/packages/babel-core/src/errors/rewrite-stack-trace.ts new file mode 100644 index 000000000000..f909c4bcc76f --- /dev/null +++ b/packages/babel-core/src/errors/rewrite-stack-trace.ts @@ -0,0 +1,153 @@ +/** + * This file uses the iternal V8 Stack Trace API (https://v8.dev/docs/stack-trace-api) + * to provide utilities to rewrite the stack trace. + * When this API is not present, all the functions in this file become noops. + * + * beginHiddenCallStack(fn) and endHiddenCallStack(fn) wrap their parameter to + * mark an hidden portion of the stack trace. The function passed to + * beginHiddenCallStack is the first hidden function, while the function passed + * to endHiddenCallStack is the first shown function. + * + * When an error is thrown _outside_ of the hidden zone, everything between + * beginHiddenCallStack and endHiddenCallStack will not be shown. + * If an error is thrown _inside_ the hidden zone, then the whole stack trace + * will be visible: this is to avoid hiding real bugs. + * However, if an error inside the hidden zone is expected, it can be marked + * with the expectedError(error) function to keep the hidden frames hidden. + * + * Consider this call stack (the outer function is the bottom one): + * + * 1. a() + * 2. endHiddenCallStack(b)() + * 3. c() + * 4. beginHiddenCallStack(d)() + * 5. e() + * 6. f() + * + * - If a() throws an error, then its shown call stack will be "a, b, e, f" + * - If b() throws an error, then its shown call stack will be "b, e, f" + * - If c() throws an expected error, then its shown call stack will be "e, f" + * - If c() throws an unexpected error, then its shown call stack will be "c, d, e, f" + * - If d() throws an expected error, then its shown call stack will be "e, f" + * - If d() throws an unexpected error, then its shown call stack will be "d, e, f" + * - If e() throws an error, then its shown call stack will be "e, f" + * + * Additionally, an error can inject additional "virtual" stack frames using the + * injcectVirtualStackFrame(error, filename) function: those are added on the top + * of the existig stack, after hiding the possibly hidden frames. + * In the example above, if we called injcectVirtualStackFrame(error, "h") on the + * expected error thrown by c(), it's shown call stack would have been "h, e, f". + * This can be useful, for example, to report config validation errors as if they + * were directly thrown in the config file. + */ + +const ErrorToString = Function.call.bind(Error.prototype.toString); + +const SUPPORTED = !!Error.captureStackTrace; + +const START_HIDNG = "startHiding - secret - don't use this - v1"; +const STOP_HIDNG = "stopHiding - secret - don't use this - v1"; + +type CallSite = Parameters[1][number]; + +const expectedErrors = new WeakSet(); +const virtualFrames = new WeakMap(); + +function CallSite(filename: string): CallSite { + // We need to use a prototype otherwise it breaks source-map-support's internals + return Object.create({ + isNative: () => false, + isConstructor: () => false, + isToplevel: () => true, + getFileName: () => filename, + getLineNumber: () => undefined, + getColumnNumber: () => undefined, + getFunctionName: () => undefined, + getMethodName: () => undefined, + getTypeName: () => undefined, + toString: () => filename, + } as CallSite); +} + +export function injcectVirtualStackFrame(error: Error, filename: string) { + if (!SUPPORTED) return; + + let frames = virtualFrames.get(error); + if (!frames) virtualFrames.set(error, (frames = [])); + frames.push(CallSite(filename)); + + return error; +} + +export function expectedError(error: Error) { + if (!SUPPORTED) return; + expectedErrors.add(error); + return error; +} + +export function beginHiddenCallStack(fn: Fn): Fn { + if (!SUPPORTED) return fn; + + return Object.defineProperty( + function (this: any) { + setupPrepareStackTrace(); + return fn.apply(this, arguments); + } as any as Fn, + "name", + { value: STOP_HIDNG }, + ); +} + +export function endHiddenCallStack(fn: Fn): Fn { + if (!SUPPORTED) return fn; + + return Object.defineProperty( + function (this: any) { + return fn.apply(this, arguments); + } as any as Fn, + "name", + { value: START_HIDNG }, + ); +} + +function setupPrepareStackTrace() { + // This function is a singleton + // @ts-expect-error + // eslint-disable-next-line no-func-assign + setupPrepareStackTrace = () => {}; + + const { prepareStackTrace = defaultPrepareStackTrace } = Error; + + Error.prepareStackTrace = function stackTraceRewriter(err, trace) { + let newTrace = []; + + const isExpected = expectedErrors.has(err); + let status = isExpected ? "hiding" : "unknown"; + for (let i = 0; i < trace.length; i++) { + const name = trace[i].getFunctionName(); + if (name === START_HIDNG) { + status = "hiding"; + } else if (name === STOP_HIDNG) { + if (status === "hiding") { + status = "showing"; + if (virtualFrames.has(err)) { + newTrace.unshift(...virtualFrames.get(err)); + } + } else if (status === "unknown") { + // Unexpected internal error, show the full stack trace + newTrace = trace; + break; + } + } else if (status !== "hiding") { + newTrace.push(trace[i]); + } + } + + return prepareStackTrace(err, newTrace.slice(0, Error.stackTraceLimit)); + }; +} + +function defaultPrepareStackTrace(err: Error, trace: CallSite[]) { + if (trace.length === 0) return ErrorToString(err); + return `${ErrorToString(err)}\n at ${trace.join("\n at ")}`; +} diff --git a/packages/babel-core/src/parse.ts b/packages/babel-core/src/parse.ts index 02b003a49805..c62fcf5d40d2 100644 --- a/packages/babel-core/src/parse.ts +++ b/packages/babel-core/src/parse.ts @@ -7,6 +7,8 @@ import type { ParseResult } from "./parser"; import normalizeOptions from "./transformation/normalize-opts"; import type { ValidatedOptions } from "./config/validation/options"; +import { beginHiddenCallStack } from "./errors/rewrite-stack-trace"; + type FileParseCallback = { (err: Error, ast: null): void; (err: null, ast: ParseResult | null): void; @@ -54,12 +56,16 @@ export const parse: Parse = function parse( // console.warn( // "Starting from Babel 8.0.0, the 'parse' function will expect a callback. If you need to call it synchronously, please use 'parseSync'.", // ); - return parseRunner.sync(code, opts); + return parseSync(code, opts); } } - parseRunner.errback(code, opts, callback); + beginHiddenCallStack(parseRunner.errback)(code, opts, callback); }; -export const parseSync = parseRunner.sync; -export const parseAsync = parseRunner.async; +export function parseSync(...args: Parameters) { + return beginHiddenCallStack(parseRunner.sync)(...args); +} +export function parseAsync(...args: Parameters) { + return beginHiddenCallStack(parseRunner.async)(...args); +} diff --git a/packages/babel-core/test/errors-stacks.js b/packages/babel-core/test/errors-stacks.js new file mode 100644 index 000000000000..d01fc0f7ce52 --- /dev/null +++ b/packages/babel-core/test/errors-stacks.js @@ -0,0 +1,189 @@ +import * as babel from "../lib/index.js"; + +const replaceAll = "".replaceAll + ? Function.call.bind("".replaceAll) + : (str, find, replace) => str.split(find).join(replace); + +function expectError(run) { + try { + run(); + } catch (e) { + let { stack } = e; + stack = replaceAll(stack, import.meta.url, "").replace( + /(?:\n\s*at[^\n]+?[^\n]+)+/g, + "\n ... frames from this test file ...", + ); + // Remove jest-related stack frames + stack = stack.replace( + /(?:\n\s*at[^\n]+?node_modules\/(?:jest|piscina)[^\n]+)+/g, + "\n ... internal jest frames ...", + ); + stack = replaceAll(stack, process.cwd(), ""); + return expect(stack); + } + throw new Error("It should have thrown an error."); +} + +const fixture = name => + new URL(`./fixtures/errors/${name}`, import.meta.url).pathname; + +describe("@babel/core errors", function () { + it("error inside config function", function () { + expectError(() => { + babel.parseSync("foo;", { + root: fixture("error-config-function"), + }); + }).toMatchInlineSnapshot(` + "Error: Error inside config! + at myConfig (/packages/babel-core/test/fixtures/errors/error-config-function/babel.config.js:2:9) + at Module.parseSync (/packages/babel-core/lib/parse.js:58:72) + ... frames from this test file ... + ... internal jest frames ..." + `); + }); + + it("error inside config function with more frames", function () { + expectError(() => { + babel.parseSync("foo;", { + root: fixture("error-config-function-more-frames"), + }); + }).toMatchInlineSnapshot(` + "Error: Error inside config! + at f (/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js:6:9) + at g (/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js:10:3) + at myConfig (/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js:2:3) + at Module.parseSync (/packages/babel-core/lib/parse.js:58:72) + ... frames from this test file ... + ... internal jest frames ..." + `); + }); + + it("error inside config file", function () { + expectError(() => { + babel.parseSync("foo;", { + root: fixture("error-config-file"), + }); + }).toMatchInlineSnapshot(` + "Error: Error inside config! + at Object. (/packages/babel-core/test/fixtures/errors/error-config-file/babel.config.js:4:7) + at Module._compile (node:internal/modules/cjs/loader:1120:14) + at Module._extensions..js (node:internal/modules/cjs/loader:1174:10) + at Module.load (node:internal/modules/cjs/loader:998:32) + at Module._load (node:internal/modules/cjs/loader:839:12) + at Module.require (node:internal/modules/cjs/loader:1022:19) + at require (node:internal/modules/cjs/helpers:102:18) + at Module.parseSync (/packages/babel-core/lib/parse.js:58:72) + ... frames from this test file ... + ... internal jest frames ..." + `); + }); + + it("error inside config file with more frames", function () { + expectError(() => { + babel.parseSync("foo;", { + root: fixture("error-config-file-more-frames"), + }); + }).toMatchInlineSnapshot(` + "Error: Error inside config! + at f (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:7:9) + at g (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:11:3) + at Object. (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:1:1) + at Module._compile (node:internal/modules/cjs/loader:1120:14) + at Module._extensions..js (node:internal/modules/cjs/loader:1174:10) + at Module.load (node:internal/modules/cjs/loader:998:32) + at Module._load (node:internal/modules/cjs/loader:839:12) + at Module.require (node:internal/modules/cjs/loader:1022:19) + at require (node:internal/modules/cjs/helpers:102:18) + at Module.parseSync (/packages/babel-core/lib/parse.js:58:72) + ... frames from this test file ... + ... internal jest frames ..." + `); + }); + + it("invalid JSON config file", function () { + expectError(() => { + babel.parseSync("foo;", { + root: fixture("invalid-json"), + }); + }).toMatchInlineSnapshot(` + "Error: Error while parsing config - JSON5: invalid character '}' at 3:1 + at /packages/babel-core/test/fixtures/errors/invalid-json/babel.config.json + at Module.parseSync (/packages/babel-core/lib/parse.js:58:72) + ... frames from this test file ... + ... internal jest frames ..." + `); + }); + + it("use 'exclude' without filename", function () { + expectError(() => { + babel.parseSync("foo;", { + root: fixture("use-exclude"), + }); + }).toMatchInlineSnapshot(` + "Error: Configuration contains string/RegExp pattern, but no filename was passed to Babel + at /packages/babel-core/test/fixtures/errors/use-exclude/babel.config.js + at Module.parseSync (/packages/babel-core/lib/parse.js:58:72) + ... frames from this test file ... + ... internal jest frames ..." + `); + }); + + it("use 'exclude' without filename in programmatic options", function () { + expectError(() => { + babel.parseSync("foo;", { + configFile: false, + exclude: /node_modules/, + }); + }).toMatchInlineSnapshot(` + "Error: Configuration contains string/RegExp pattern, but no filename was passed to Babel + at Module.parseSync (/packages/babel-core/lib/parse.js:58:72) + ... frames from this test file ... + ... internal jest frames ..." + `); + }); + + it("use 'exclude' without filename in preset", function () { + expectError(() => { + babel.parseSync("foo;", { + root: fixture("use-exclude-in-preset"), + }); + }).toMatchInlineSnapshot(` + "Error: [BABEL] unknown: Preset /* your preset */ requires a filename to be set when babel is called directly, + \`\`\` + babel.transformSync(code, { filename: 'file.ts', presets: [/* your preset */] }); + \`\`\` + See https://babeljs.io/docs/en/options#filename for more information. + at Module.parseSync (/packages/babel-core/lib/parse.js:58:72) + ... frames from this test file ... + ... internal jest frames ..." + `); + }); + + it("invalid option", function () { + expectError(() => { + babel.parseSync("foo;", { + root: fixture("invalid-option"), + }); + }).toMatchInlineSnapshot(` + "Error: .sourceType must be \\"module\\", \\"script\\", \\"unambiguous\\", or undefined + at /packages/babel-core/test/fixtures/errors/invalid-option/babel.config.json + at Module.parseSync (/packages/babel-core/lib/parse.js:58:72) + ... frames from this test file ... + ... internal jest frames ..." + `); + }); + + it("invalid option in programmatic options", function () { + expectError(() => + babel.parseSync("foo;", { + root: fixture("valid"), + sourceType: "foo", + }), + ).toMatchInlineSnapshot(` + "Error: .sourceType must be \\"module\\", \\"script\\", \\"unambiguous\\", or undefined + at Module.parseSync (/packages/babel-core/lib/parse.js:58:72) + ... frames from this test file ... + ... internal jest frames ..." + `); + }); +}); diff --git a/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js b/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js new file mode 100644 index 000000000000..924b0e469607 --- /dev/null +++ b/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js @@ -0,0 +1,12 @@ +g(); + +module.exports = function myConfig() { +}; + +function f() { + throw new Error("Error inside config!"); +} + +function g() { + f(); +} diff --git a/packages/babel-core/test/fixtures/errors/error-config-file/babel.config.js b/packages/babel-core/test/fixtures/errors/error-config-file/babel.config.js new file mode 100644 index 000000000000..75f3b90b36d8 --- /dev/null +++ b/packages/babel-core/test/fixtures/errors/error-config-file/babel.config.js @@ -0,0 +1,4 @@ +module.exports = function myConfig() { +}; + +throw new Error("Error inside config!"); diff --git a/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js b/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js new file mode 100644 index 000000000000..2e566443c71f --- /dev/null +++ b/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js @@ -0,0 +1,11 @@ +module.exports = function myConfig() { + g(); +}; + +function f() { + throw new Error("Error inside config!"); +} + +function g() { + f(); +} diff --git a/packages/babel-core/test/fixtures/errors/error-config-function/babel.config.js b/packages/babel-core/test/fixtures/errors/error-config-function/babel.config.js new file mode 100644 index 000000000000..927a5fa8e51c --- /dev/null +++ b/packages/babel-core/test/fixtures/errors/error-config-function/babel.config.js @@ -0,0 +1,3 @@ +module.exports = function myConfig() { + throw new Error("Error inside config!"); +} diff --git a/packages/babel-core/test/fixtures/errors/invalid-json/babel.config.json b/packages/babel-core/test/fixtures/errors/invalid-json/babel.config.json new file mode 100644 index 000000000000..62df75952cde --- /dev/null +++ b/packages/babel-core/test/fixtures/errors/invalid-json/babel.config.json @@ -0,0 +1,3 @@ +{ + foo +} diff --git a/packages/babel-core/test/fixtures/errors/invalid-option/babel.config.json b/packages/babel-core/test/fixtures/errors/invalid-option/babel.config.json new file mode 100644 index 000000000000..5b6f9463c1b8 --- /dev/null +++ b/packages/babel-core/test/fixtures/errors/invalid-option/babel.config.json @@ -0,0 +1,3 @@ +{ + "sourceType": "foo" +} diff --git a/packages/babel-core/test/fixtures/errors/invalid-pkg-json/package.json b/packages/babel-core/test/fixtures/errors/invalid-pkg-json/package.json new file mode 100644 index 000000000000..62df75952cde --- /dev/null +++ b/packages/babel-core/test/fixtures/errors/invalid-pkg-json/package.json @@ -0,0 +1,3 @@ +{ + foo +} diff --git a/packages/babel-core/test/fixtures/errors/use-exclude-in-preset/babel.config.js b/packages/babel-core/test/fixtures/errors/use-exclude-in-preset/babel.config.js new file mode 100644 index 000000000000..e6fd96349016 --- /dev/null +++ b/packages/babel-core/test/fixtures/errors/use-exclude-in-preset/babel.config.js @@ -0,0 +1,4 @@ +module.exports = function myConfig(api) { + api.cache.never(); + return { presets: ["./my-preset.js"] }; +}; diff --git a/packages/babel-core/test/fixtures/errors/use-exclude-in-preset/my-preset.js b/packages/babel-core/test/fixtures/errors/use-exclude-in-preset/my-preset.js new file mode 100644 index 000000000000..b7a7a3ecc252 --- /dev/null +++ b/packages/babel-core/test/fixtures/errors/use-exclude-in-preset/my-preset.js @@ -0,0 +1,3 @@ +module.exports = function myPreset() { + return { exclude: /node_modules/ }; +}; diff --git a/packages/babel-core/test/fixtures/errors/use-exclude/babel.config.js b/packages/babel-core/test/fixtures/errors/use-exclude/babel.config.js new file mode 100644 index 000000000000..37e992d37d27 --- /dev/null +++ b/packages/babel-core/test/fixtures/errors/use-exclude/babel.config.js @@ -0,0 +1,4 @@ +module.exports = function myConfig(api) { + api.cache.never(); + return { exclude: /node_modules/ } +}; diff --git a/packages/babel-core/test/fixtures/errors/valid/babel.config.json b/packages/babel-core/test/fixtures/errors/valid/babel.config.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/babel-core/test/fixtures/errors/valid/babel.config.json @@ -0,0 +1 @@ +{}