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 01/13] 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 @@ +{} From 74cd5f5406e5f340ea36cdfa3e72f325d9bd4205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Mon, 22 Aug 2022 17:28:36 +0200 Subject: [PATCH 02/13] Fixes --- .../src/errors/rewrite-stack-trace.ts | 16 ++++++++- packages/babel-core/test/errors-stacks.js | 35 ++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/babel-core/src/errors/rewrite-stack-trace.ts b/packages/babel-core/src/errors/rewrite-stack-trace.ts index f909c4bcc76f..9c8a30ae30b0 100644 --- a/packages/babel-core/src/errors/rewrite-stack-trace.ts +++ b/packages/babel-core/src/errors/rewrite-stack-trace.ts @@ -45,6 +45,15 @@ const ErrorToString = Function.call.bind(Error.prototype.toString); const SUPPORTED = !!Error.captureStackTrace; +// We add some extra frames to Error.stackTraceLimit, so that we can respect +// the original Error.stackTraceLimit even after removing all our internal +// frames. +// STACK_TRACE_LIMIT_DELTA should be bigger than the expected number of internal +// frames, but not too big because capturing the stack trace is slow (this is +// why Error.stackTraceLimit does not default to Infinity!). +// Increase it if needed. +const STACK_TRACE_LIMIT_DELTA = 100; + const START_HIDNG = "startHiding - secret - don't use this - v1"; const STOP_HIDNG = "stopHiding - secret - don't use this - v1"; @@ -118,6 +127,8 @@ function setupPrepareStackTrace() { const { prepareStackTrace = defaultPrepareStackTrace } = Error; + Error.stackTraceLimit += STACK_TRACE_LIMIT_DELTA; + Error.prepareStackTrace = function stackTraceRewriter(err, trace) { let newTrace = []; @@ -143,7 +154,10 @@ function setupPrepareStackTrace() { } } - return prepareStackTrace(err, newTrace.slice(0, Error.stackTraceLimit)); + return prepareStackTrace( + err, + newTrace.slice(0, Error.stackTraceLimit - STACK_TRACE_LIMIT_DELTA), + ); }; } diff --git a/packages/babel-core/test/errors-stacks.js b/packages/babel-core/test/errors-stacks.js index d01fc0f7ce52..6ca93ac6f100 100644 --- a/packages/babel-core/test/errors-stacks.js +++ b/packages/babel-core/test/errors-stacks.js @@ -73,8 +73,7 @@ describe("@babel/core errors", function () { 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 ..." + ... frames from this test file ..." `); }); @@ -94,12 +93,38 @@ describe("@babel/core errors", function () { 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 ..." + at Module.parseSync (/packages/babel-core/lib/parse.js:58:72)" `); }); + it("error inside config file with more frames and increased Error.stackTraceLimit", function () { + const INC = 10; + Error.stackTraceLimit += INC; + try { + 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 ..." + `); + } finally { + Error.stackTraceLimit -= INC; + } + }); + it("invalid JSON config file", function () { expectError(() => { babel.parseSync("foo;", { From 67af9a53969a6eb3194b3a1a6df79e182154acfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Mon, 22 Aug 2022 17:43:35 +0200 Subject: [PATCH 03/13] All entry points --- packages/babel-core/src/parse.ts | 2 +- packages/babel-core/src/transform-ast.ts | 24 +++++++++++--- packages/babel-core/src/transform-file.ts | 38 ++++++++++++++++------- packages/babel-core/src/transform.ts | 17 +++++++--- 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/packages/babel-core/src/parse.ts b/packages/babel-core/src/parse.ts index c62fcf5d40d2..1622ecaab608 100644 --- a/packages/babel-core/src/parse.ts +++ b/packages/babel-core/src/parse.ts @@ -56,7 +56,7 @@ 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 parseSync(code, opts); + return beginHiddenCallStack(parseRunner.sync)(code, opts); } } diff --git a/packages/babel-core/src/transform-ast.ts b/packages/babel-core/src/transform-ast.ts index 3d9cfdda6783..dbe9d7eea9d8 100644 --- a/packages/babel-core/src/transform-ast.ts +++ b/packages/babel-core/src/transform-ast.ts @@ -5,6 +5,8 @@ import type { InputOptions, ResolvedConfig } from "./config"; import { run } from "./transformation"; import type * as t from "@babel/types"; +import { beginHiddenCallStack } from "./errors/rewrite-stack-trace"; + import type { FileResult, FileResultCallback } from "./transformation"; type AstRoot = t.File | t.Program; @@ -57,12 +59,26 @@ export const transformFromAst: TransformFromAst = function transformFromAst( // console.warn( // "Starting from Babel 8.0.0, the 'transformFromAst' function will expect a callback. If you need to call it synchronously, please use 'transformFromAstSync'.", // ); - return transformFromAstRunner.sync(ast, code, opts); + return beginHiddenCallStack(transformFromAstRunner.sync)(ast, code, opts); } } - transformFromAstRunner.errback(ast, code, opts, callback); + beginHiddenCallStack(transformFromAstRunner.errback)( + ast, + code, + opts, + callback, + ); }; -export const transformFromAstSync = transformFromAstRunner.sync; -export const transformFromAstAsync = transformFromAstRunner.async; +export function transformFromAstSync( + ...args: Parameters +) { + return beginHiddenCallStack(transformFromAstRunner.sync)(...args); +} + +export function transformFromAstAsync( + ...args: Parameters +) { + return beginHiddenCallStack(transformFromAstRunner.async)(...args); +} diff --git a/packages/babel-core/src/transform-file.ts b/packages/babel-core/src/transform-file.ts index 5701cf2be9e6..c493425db86b 100644 --- a/packages/babel-core/src/transform-file.ts +++ b/packages/babel-core/src/transform-file.ts @@ -14,15 +14,6 @@ type transformFileType = typeof import("./transform-file"); // transform-file-browser. ({} as any as transformFileBrowserType as transformFileType); -type TransformFile = { - (filename: string, callback: FileResultCallback): void; - ( - filename: string, - opts: InputOptions | undefined | null, - callback: FileResultCallback, - ): void; -}; - const transformFileRunner = gensync(function* ( filename: string, opts?: InputOptions, @@ -36,6 +27,29 @@ const transformFileRunner = gensync(function* ( return yield* run(config, code); }); -export const transformFile = transformFileRunner.errback as TransformFile; -export const transformFileSync = transformFileRunner.sync; -export const transformFileAsync = transformFileRunner.async; +// @ts-expect-error TS doesn't detect that this signature is compatible +export function transformFile( + filename: string, + callback: FileResultCallback, +): void; +export function transformFile( + filename: string, + opts: InputOptions | undefined | null, + callback: FileResultCallback, +): void; +export function transformFile( + ...args: Parameters +) { + return transformFileRunner.errback(...args); +} + +export function transformFileSync( + ...args: Parameters +) { + return transformFileRunner.sync(...args); +} +export function transformFileAsync( + ...args: Parameters +) { + return transformFileRunner.async(...args); +} diff --git a/packages/babel-core/src/transform.ts b/packages/babel-core/src/transform.ts index 3569de9e7575..9a3f7c815d09 100644 --- a/packages/babel-core/src/transform.ts +++ b/packages/babel-core/src/transform.ts @@ -5,6 +5,7 @@ import type { InputOptions, ResolvedConfig } from "./config"; import { run } from "./transformation"; import type { FileResult, FileResultCallback } from "./transformation"; +import { beginHiddenCallStack } from "./errors/rewrite-stack-trace"; export type { FileResult } from "./transformation"; @@ -52,12 +53,20 @@ export const transform: Transform = function transform( // console.warn( // "Starting from Babel 8.0.0, the 'transform' function will expect a callback. If you need to call it synchronously, please use 'transformSync'.", // ); - return transformRunner.sync(code, opts); + return beginHiddenCallStack(transformRunner.sync)(code, opts); } } - transformRunner.errback(code, opts, callback); + beginHiddenCallStack(transformRunner.errback)(code, opts, callback); }; -export const transformSync = transformRunner.sync; -export const transformAsync = transformRunner.async; +export function transformSync( + ...args: Parameters +) { + return beginHiddenCallStack(transformRunner.sync)(...args); +} +export function transformAsync( + ...args: Parameters +) { + return beginHiddenCallStack(transformRunner.async)(...args); +} From 53b7938de7553f6aee63a43cf0c3cb7b6ac95dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Mon, 22 Aug 2022 17:47:31 +0200 Subject: [PATCH 04/13] Add a test for internal errors --- packages/babel-core/test/errors-stacks.js | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/babel-core/test/errors-stacks.js b/packages/babel-core/test/errors-stacks.js index 6ca93ac6f100..d639e0fe58fc 100644 --- a/packages/babel-core/test/errors-stacks.js +++ b/packages/babel-core/test/errors-stacks.js @@ -211,4 +211,32 @@ describe("@babel/core errors", function () { ... internal jest frames ..." `); }); + + it("internal errors have the full stack trace", function () { + expectError(() => { + const { map } = Array.prototype; + try { + Array.prototype.map = () => { + throw new Error("Internal error! This is a fake bug :)"); + }; + babel.parseSync("foo;", { + root: fixture("valid"), + }); + } finally { + Array.prototype.map = map; + } + }).toMatchInlineSnapshot(` + "Error: Internal error! This is a fake bug :) + ... frames from this test file ... + at loadOneConfig (/packages/babel-core/lib/config/files/configuration.js:148:47) + at loadOneConfig.next () + at buildRootChain (/packages/babel-core/lib/config/config-chain.js:86:51) + at buildRootChain.next () + at loadPrivatePartialConfig (/packages/babel-core/lib/config/partial.js:103:62) + at loadPrivatePartialConfig.next () + at loadFullConfig (/packages/babel-core/lib/config/full.js:57:46) + at loadFullConfig.next () + at parse (/packages/babel-core/lib/parse.js:29:45)" + `); + }); }); From fefa3474a1b24e2861609350208291d4244212e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Wed, 24 Aug 2022 15:35:20 +0200 Subject: [PATCH 05/13] Remove line numbers in snapshot --- packages/babel-core/test/errors-stacks.js | 93 ++++++++++++----------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/packages/babel-core/test/errors-stacks.js b/packages/babel-core/test/errors-stacks.js index d639e0fe58fc..9face3ebb355 100644 --- a/packages/babel-core/test/errors-stacks.js +++ b/packages/babel-core/test/errors-stacks.js @@ -19,6 +19,9 @@ function expectError(run) { "\n ... internal jest frames ...", ); stack = replaceAll(stack, process.cwd(), ""); + // Replace line/column numbers, since they are affected by how + // the code is compiled. + stack = stack.replace(/:\d+:\d+\)$/gm, ":_:_)"); return expect(stack); } throw new Error("It should have thrown an error."); @@ -35,8 +38,8 @@ describe("@babel/core errors", 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) + at myConfig (/packages/babel-core/test/fixtures/errors/error-config-function/babel.config.js:_:_) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) ... frames from this test file ... ... internal jest frames ..." `); @@ -49,10 +52,10 @@ describe("@babel/core errors", function () { }); }).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) + at f (/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js:_:_) + at g (/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js:_:_) + at myConfig (/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js:_:_) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) ... frames from this test file ... ... internal jest frames ..." `); @@ -65,14 +68,14 @@ describe("@babel/core errors", function () { }); }).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) + at Object. (/packages/babel-core/test/fixtures/errors/error-config-file/babel.config.js:_:_) + at Module._compile (node:internal/modules/cjs/loader:_:_) + at Module._extensions..js (node:internal/modules/cjs/loader:_:_) + at Module.load (node:internal/modules/cjs/loader:_:_) + at Module._load (node:internal/modules/cjs/loader:_:_) + at Module.require (node:internal/modules/cjs/loader:_:_) + at require (node:internal/modules/cjs/helpers:_:_) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) ... frames from this test file ..." `); }); @@ -84,16 +87,16 @@ describe("@babel/core errors", function () { }); }).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)" + at f (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) + at g (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) + at Object. (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) + at Module._compile (node:internal/modules/cjs/loader:_:_) + at Module._extensions..js (node:internal/modules/cjs/loader:_:_) + at Module.load (node:internal/modules/cjs/loader:_:_) + at Module._load (node:internal/modules/cjs/loader:_:_) + at Module.require (node:internal/modules/cjs/loader:_:_) + at require (node:internal/modules/cjs/helpers:_:_) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_)" `); }); @@ -107,16 +110,16 @@ describe("@babel/core errors", function () { }); }).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) + at f (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) + at g (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) + at Object. (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) + at Module._compile (node:internal/modules/cjs/loader:_:_) + at Module._extensions..js (node:internal/modules/cjs/loader:_:_) + at Module.load (node:internal/modules/cjs/loader:_:_) + at Module._load (node:internal/modules/cjs/loader:_:_) + at Module.require (node:internal/modules/cjs/loader:_:_) + at require (node:internal/modules/cjs/helpers:_:_) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) ... frames from this test file ... ... internal jest frames ..." `); @@ -133,7 +136,7 @@ describe("@babel/core errors", function () { }).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) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) ... frames from this test file ... ... internal jest frames ..." `); @@ -147,7 +150,7 @@ describe("@babel/core errors", function () { }).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) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) ... frames from this test file ... ... internal jest frames ..." `); @@ -161,7 +164,7 @@ describe("@babel/core errors", function () { }); }).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) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) ... frames from this test file ... ... internal jest frames ..." `); @@ -178,7 +181,7 @@ describe("@babel/core errors", function () { 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) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) ... frames from this test file ... ... internal jest frames ..." `); @@ -192,7 +195,7 @@ describe("@babel/core errors", function () { }).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) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) ... frames from this test file ... ... internal jest frames ..." `); @@ -206,7 +209,7 @@ describe("@babel/core errors", function () { }), ).toMatchInlineSnapshot(` "Error: .sourceType must be \\"module\\", \\"script\\", \\"unambiguous\\", or undefined - at Module.parseSync (/packages/babel-core/lib/parse.js:58:72) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) ... frames from this test file ... ... internal jest frames ..." `); @@ -228,15 +231,15 @@ describe("@babel/core errors", function () { }).toMatchInlineSnapshot(` "Error: Internal error! This is a fake bug :) ... frames from this test file ... - at loadOneConfig (/packages/babel-core/lib/config/files/configuration.js:148:47) + at loadOneConfig (/packages/babel-core/lib/config/files/configuration.js:_:_) at loadOneConfig.next () - at buildRootChain (/packages/babel-core/lib/config/config-chain.js:86:51) + at buildRootChain (/packages/babel-core/lib/config/config-chain.js:_:_) at buildRootChain.next () - at loadPrivatePartialConfig (/packages/babel-core/lib/config/partial.js:103:62) + at loadPrivatePartialConfig (/packages/babel-core/lib/config/partial.js:_:_) at loadPrivatePartialConfig.next () - at loadFullConfig (/packages/babel-core/lib/config/full.js:57:46) + at loadFullConfig (/packages/babel-core/lib/config/full.js:_:_) at loadFullConfig.next () - at parse (/packages/babel-core/lib/parse.js:29:45)" + at parse (/packages/babel-core/lib/parse.js:_:_)" `); }); }); From 655a4d90cd7c37b058c4ba65472e3f39cfcf543f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Wed, 24 Aug 2022 15:51:35 +0200 Subject: [PATCH 06/13] Hide more jest frames --- packages/babel-core/test/errors-stacks.js | 143 ++++++++++++---------- 1 file changed, 75 insertions(+), 68 deletions(-) diff --git a/packages/babel-core/test/errors-stacks.js b/packages/babel-core/test/errors-stacks.js index 9face3ebb355..24623af206e1 100644 --- a/packages/babel-core/test/errors-stacks.js +++ b/packages/babel-core/test/errors-stacks.js @@ -9,19 +9,25 @@ function expectError(run) { 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 absolute URLs + stack = replaceAll(stack, process.cwd(), ""); + stack = replaceAll(stack, "file://", ""); + // Remove jest-related stack frames. + // The `at async Promise.all` frame comes from inside jest-light-runner and is only + // visible when using --run-in-band, comes from inside jest but doesn't have an + // associated file path. + stack = stack.replace( + /(?:\n\s*at [^\n]+?node_modules\/(?:@?jest|piscina)[^\n]+|\n\s*at async Promise.all[^\n]+)+/g, + "\n at ... internal jest frames ...", ); - // Remove jest-related stack frames + // Remove node internal frames, since they change by version stack = stack.replace( - /(?:\n\s*at[^\n]+?node_modules\/(?:jest|piscina)[^\n]+)+/g, - "\n ... internal jest frames ...", + /(?:\n\s*at ((?:async )?[\w.]+)? ?\((?:node:)?internal\/[^\n]+)+/g, + "\n at $1 (... internal node frames ...)", ); - stack = replaceAll(stack, process.cwd(), ""); // Replace line/column numbers, since they are affected by how // the code is compiled. - stack = stack.replace(/:\d+:\d+\)$/gm, ":_:_)"); + stack = stack.replace(/\d*:\d+:\d+(\)?)$/gm, ":_:_$1"); return expect(stack); } throw new Error("It should have thrown an error."); @@ -31,6 +37,13 @@ const fixture = name => new URL(`./fixtures/errors/${name}`, import.meta.url).pathname; describe("@babel/core errors", function () { + beforeAll(() => { + Error.stackTraceLimit += 100; + }); + afterAll(() => { + Error.stackTraceLimit -= 100; + }); + it("error inside config function", function () { expectError(() => { babel.parseSync("foo;", { @@ -40,8 +53,10 @@ describe("@babel/core errors", function () { "Error: Error inside config! at myConfig (/packages/babel-core/test/fixtures/errors/error-config-function/babel.config.js:_:_) at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - ... frames from this test file ... - ... internal jest frames ..." + at /packages/babel-core/test/errors-stacks.js?:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at ... internal jest frames ..." `); }); @@ -56,8 +71,10 @@ describe("@babel/core errors", function () { at g (/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js:_:_) at myConfig (/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js:_:_) at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - ... frames from this test file ... - ... internal jest frames ..." + at /packages/babel-core/test/errors-stacks.js?:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at ... internal jest frames ..." `); }); @@ -69,14 +86,12 @@ describe("@babel/core errors", function () { }).toMatchInlineSnapshot(` "Error: Error inside config! at Object. (/packages/babel-core/test/fixtures/errors/error-config-file/babel.config.js:_:_) - at Module._compile (node:internal/modules/cjs/loader:_:_) - at Module._extensions..js (node:internal/modules/cjs/loader:_:_) - at Module.load (node:internal/modules/cjs/loader:_:_) - at Module._load (node:internal/modules/cjs/loader:_:_) - at Module.require (node:internal/modules/cjs/loader:_:_) - at require (node:internal/modules/cjs/helpers:_:_) + at require (... internal node frames ...) at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - ... frames from this test file ..." + at /packages/babel-core/test/errors-stacks.js?:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at ... internal jest frames ..." `); }); @@ -90,44 +105,15 @@ describe("@babel/core errors", function () { at f (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) at g (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) at Object. (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) - at Module._compile (node:internal/modules/cjs/loader:_:_) - at Module._extensions..js (node:internal/modules/cjs/loader:_:_) - at Module.load (node:internal/modules/cjs/loader:_:_) - at Module._load (node:internal/modules/cjs/loader:_:_) - at Module.require (node:internal/modules/cjs/loader:_:_) - at require (node:internal/modules/cjs/helpers:_:_) - at Module.parseSync (/packages/babel-core/lib/parse.js:_:_)" + at require (... internal node frames ...) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at ... internal jest frames ..." `); }); - it("error inside config file with more frames and increased Error.stackTraceLimit", function () { - const INC = 10; - Error.stackTraceLimit += INC; - try { - 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:_:_) - at g (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) - at Object. (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) - at Module._compile (node:internal/modules/cjs/loader:_:_) - at Module._extensions..js (node:internal/modules/cjs/loader:_:_) - at Module.load (node:internal/modules/cjs/loader:_:_) - at Module._load (node:internal/modules/cjs/loader:_:_) - at Module.require (node:internal/modules/cjs/loader:_:_) - at require (node:internal/modules/cjs/helpers:_:_) - at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - ... frames from this test file ... - ... internal jest frames ..." - `); - } finally { - Error.stackTraceLimit -= INC; - } - }); - it("invalid JSON config file", function () { expectError(() => { babel.parseSync("foo;", { @@ -137,8 +123,10 @@ describe("@babel/core errors", function () { "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:_:_) - ... frames from this test file ... - ... internal jest frames ..." + at /packages/babel-core/test/errors-stacks.js?:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at ... internal jest frames ..." `); }); @@ -151,8 +139,10 @@ describe("@babel/core errors", function () { "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:_:_) - ... frames from this test file ... - ... internal jest frames ..." + at /packages/babel-core/test/errors-stacks.js?:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at ... internal jest frames ..." `); }); @@ -165,8 +155,10 @@ describe("@babel/core errors", function () { }).toMatchInlineSnapshot(` "Error: Configuration contains string/RegExp pattern, but no filename was passed to Babel at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - ... frames from this test file ... - ... internal jest frames ..." + at /packages/babel-core/test/errors-stacks.js?:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at ... internal jest frames ..." `); }); @@ -182,8 +174,10 @@ describe("@babel/core errors", function () { \`\`\` See https://babeljs.io/docs/en/options#filename for more information. at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - ... frames from this test file ... - ... internal jest frames ..." + at /packages/babel-core/test/errors-stacks.js?:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at ... internal jest frames ..." `); }); @@ -196,8 +190,10 @@ describe("@babel/core errors", function () { "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:_:_) - ... frames from this test file ... - ... internal jest frames ..." + at /packages/babel-core/test/errors-stacks.js?:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at ... internal jest frames ..." `); }); @@ -210,8 +206,10 @@ describe("@babel/core errors", function () { ).toMatchInlineSnapshot(` "Error: .sourceType must be \\"module\\", \\"script\\", \\"unambiguous\\", or undefined at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - ... frames from this test file ... - ... internal jest frames ..." + at /packages/babel-core/test/errors-stacks.js?:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at ... internal jest frames ..." `); }); @@ -230,7 +228,7 @@ describe("@babel/core errors", function () { } }).toMatchInlineSnapshot(` "Error: Internal error! This is a fake bug :) - ... frames from this test file ... + at Array.map (/packages/babel-core/test/errors-stacks.js?:_:_) at loadOneConfig (/packages/babel-core/lib/config/files/configuration.js:_:_) at loadOneConfig.next () at buildRootChain (/packages/babel-core/lib/config/config-chain.js:_:_) @@ -239,7 +237,16 @@ describe("@babel/core errors", function () { at loadPrivatePartialConfig.next () at loadFullConfig (/packages/babel-core/lib/config/full.js:_:_) at loadFullConfig.next () - at parse (/packages/babel-core/lib/parse.js:_:_)" + at parse (/packages/babel-core/lib/parse.js:_:_) + at parse.next () + at evaluateSync (/node_modules/gensync/index.js:_:_) + at sync (/node_modules/gensync/index.js:_:_) + at stopHiding - secret - don't use this - v1 (/packages/babel-core/lib/errors/rewrite-stack-trace.js:_:_) + at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) + at /packages/babel-core/test/errors-stacks.js?:_:_ + at ... internal jest frames ..." `); }); }); From b08d5156564f2acf13be02d4ee39e06cce8cb5a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Wed, 24 Aug 2022 22:12:36 +0200 Subject: [PATCH 07/13] Fix Node.js 10 tests --- packages/babel-core/test/errors-stacks.js | 98 ++++++++++++++--------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/packages/babel-core/test/errors-stacks.js b/packages/babel-core/test/errors-stacks.js index 24623af206e1..56d746228c2c 100644 --- a/packages/babel-core/test/errors-stacks.js +++ b/packages/babel-core/test/errors-stacks.js @@ -26,8 +26,30 @@ function expectError(run) { "\n at $1 (... internal node frames ...)", ); // Replace line/column numbers, since they are affected by how - // the code is compiled. - stack = stack.replace(/\d*:\d+:\d+(\)?)$/gm, ":_:_$1"); + // the code is compiled. The first optional ?\d+ is added by Jest. + stack = stack.replace(/(?:\?\d+)?:\d+:\d+(\)?)$/gm, ":_:_$1"); + + // This is only needed because Node.js < 16 (and old Jest) stack traces + // are quite different from newer stack traces. + // TODO(Babel 8): Delete this code + { + stack = replaceAll(stack, "Object.parseSync", "Module.parseSync"); + stack = stack.replace( + /(?:run|Object\.) \(([^)]+)\)/g, + "$1", + ); + stack = replaceAll( + stack, + "at ... internal jest frames ...\n at Module.parseSync", + "at require (... internal node frames ...)\n at Module.parseSync", + ); + stack = replaceAll( + stack, + "\n at ... internal jest frames ...\n at new Promise ()", + "", + ); + } + return expect(stack); } throw new Error("It should have thrown an error."); @@ -53,9 +75,9 @@ describe("@babel/core errors", function () { "Error: Error inside config! at myConfig (/packages/babel-core/test/fixtures/errors/error-config-function/babel.config.js:_:_) at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ - at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ + at /packages/babel-core/test/errors-stacks.js:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js:_:_) + at /packages/babel-core/test/errors-stacks.js:_:_ at ... internal jest frames ..." `); }); @@ -71,9 +93,9 @@ describe("@babel/core errors", function () { at g (/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js:_:_) at myConfig (/packages/babel-core/test/fixtures/errors/error-config-function-more-frames/babel.config.js:_:_) at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ - at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ + at /packages/babel-core/test/errors-stacks.js:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js:_:_) + at /packages/babel-core/test/errors-stacks.js:_:_ at ... internal jest frames ..." `); }); @@ -85,12 +107,12 @@ describe("@babel/core errors", function () { }); }).toMatchInlineSnapshot(` "Error: Error inside config! - at Object. (/packages/babel-core/test/fixtures/errors/error-config-file/babel.config.js:_:_) + at /packages/babel-core/test/fixtures/errors/error-config-file/babel.config.js:_:_ at require (... internal node frames ...) at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ - at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ + at /packages/babel-core/test/errors-stacks.js:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js:_:_) + at /packages/babel-core/test/errors-stacks.js:_:_ at ... internal jest frames ..." `); }); @@ -104,12 +126,12 @@ describe("@babel/core errors", function () { "Error: Error inside config! at f (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) at g (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) - at Object. (/packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_) + at /packages/babel-core/test/fixtures/errors/error-config-file-more-frames/babel.config.js:_:_ at require (... internal node frames ...) at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ - at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ + at /packages/babel-core/test/errors-stacks.js:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js:_:_) + at /packages/babel-core/test/errors-stacks.js:_:_ at ... internal jest frames ..." `); }); @@ -123,9 +145,9 @@ describe("@babel/core errors", function () { "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:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ - at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ + at /packages/babel-core/test/errors-stacks.js:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js:_:_) + at /packages/babel-core/test/errors-stacks.js:_:_ at ... internal jest frames ..." `); }); @@ -139,9 +161,9 @@ describe("@babel/core errors", function () { "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:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ - at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ + at /packages/babel-core/test/errors-stacks.js:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js:_:_) + at /packages/babel-core/test/errors-stacks.js:_:_ at ... internal jest frames ..." `); }); @@ -155,9 +177,9 @@ describe("@babel/core errors", function () { }).toMatchInlineSnapshot(` "Error: Configuration contains string/RegExp pattern, but no filename was passed to Babel at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ - at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ + at /packages/babel-core/test/errors-stacks.js:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js:_:_) + at /packages/babel-core/test/errors-stacks.js:_:_ at ... internal jest frames ..." `); }); @@ -174,9 +196,9 @@ describe("@babel/core errors", function () { \`\`\` See https://babeljs.io/docs/en/options#filename for more information. at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ - at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ + at /packages/babel-core/test/errors-stacks.js:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js:_:_) + at /packages/babel-core/test/errors-stacks.js:_:_ at ... internal jest frames ..." `); }); @@ -190,9 +212,9 @@ describe("@babel/core errors", function () { "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:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ - at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ + at /packages/babel-core/test/errors-stacks.js:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js:_:_) + at /packages/babel-core/test/errors-stacks.js:_:_ at ... internal jest frames ..." `); }); @@ -206,9 +228,9 @@ describe("@babel/core errors", function () { ).toMatchInlineSnapshot(` "Error: .sourceType must be \\"module\\", \\"script\\", \\"unambiguous\\", or undefined at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ - at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ + at /packages/babel-core/test/errors-stacks.js:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js:_:_) + at /packages/babel-core/test/errors-stacks.js:_:_ at ... internal jest frames ..." `); }); @@ -228,7 +250,7 @@ describe("@babel/core errors", function () { } }).toMatchInlineSnapshot(` "Error: Internal error! This is a fake bug :) - at Array.map (/packages/babel-core/test/errors-stacks.js?:_:_) + at Array.map (/packages/babel-core/test/errors-stacks.js:_:_) at loadOneConfig (/packages/babel-core/lib/config/files/configuration.js:_:_) at loadOneConfig.next () at buildRootChain (/packages/babel-core/lib/config/config-chain.js:_:_) @@ -243,9 +265,9 @@ describe("@babel/core errors", function () { at sync (/node_modules/gensync/index.js:_:_) at stopHiding - secret - don't use this - v1 (/packages/babel-core/lib/errors/rewrite-stack-trace.js:_:_) at Module.parseSync (/packages/babel-core/lib/parse.js:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ - at expectError (/packages/babel-core/test/errors-stacks.js?:_:_) - at /packages/babel-core/test/errors-stacks.js?:_:_ + at /packages/babel-core/test/errors-stacks.js:_:_ + at expectError (/packages/babel-core/test/errors-stacks.js:_:_) + at /packages/babel-core/test/errors-stacks.js:_:_ at ... internal jest frames ..." `); }); From fae9608f086df122770085fc3f7a5398e66badee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Wed, 24 Aug 2022 22:21:14 +0200 Subject: [PATCH 08/13] Fix Node.js 8 and 6 tests --- packages/babel-core/test/errors-stacks.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/babel-core/test/errors-stacks.js b/packages/babel-core/test/errors-stacks.js index 56d746228c2c..23de8b5393d0 100644 --- a/packages/babel-core/test/errors-stacks.js +++ b/packages/babel-core/test/errors-stacks.js @@ -1,5 +1,8 @@ import * as babel from "../lib/index.js"; +// TODO: Remove this in Babel 8, once we drop Node.js 8 +import { URL } from "url"; + const replaceAll = "".replaceAll ? Function.call.bind("".replaceAll) : (str, find, replace) => str.split(find).join(replace); @@ -33,6 +36,7 @@ function expectError(run) { // are quite different from newer stack traces. // TODO(Babel 8): Delete this code { + // Node.js <= 10 stack = replaceAll(stack, "Object.parseSync", "Module.parseSync"); stack = stack.replace( /(?:run|Object\.) \(([^)]+)\)/g, @@ -48,6 +52,13 @@ function expectError(run) { "\n at ... internal jest frames ...\n at new Promise ()", "", ); + // Node.js 8 + stack = stack.replace(/\n\s*at $/g, ""); + // Node.js 6 + stack = stack.replace( + /(at (\w+) \([^)]+\)\n\s*at) next \(native\)/g, + "$1 $2.next ()", + ); } return expect(stack); From a75ea8b96bd6fdc4f8091fdd0c1252ecf43a2c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Wed, 24 Aug 2022 23:04:33 +0200 Subject: [PATCH 09/13] Fix windows tests --- packages/babel-core/test/errors-stacks.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/babel-core/test/errors-stacks.js b/packages/babel-core/test/errors-stacks.js index 23de8b5393d0..d3887116fce5 100644 --- a/packages/babel-core/test/errors-stacks.js +++ b/packages/babel-core/test/errors-stacks.js @@ -12,9 +12,11 @@ function expectError(run) { run(); } catch (e) { let { stack } = e; + // Normalize windows paths + stack = stack.replace(/\\/g, "/"); // Remove absolute URLs - stack = replaceAll(stack, process.cwd(), ""); - stack = replaceAll(stack, "file://", ""); + stack = replaceAll(stack, process.cwd().replace(/\\/g, "/"), ""); + stack = stack.replace(/file:\/\/\/?/g, ""); // Remove jest-related stack frames. // The `at async Promise.all` frame comes from inside jest-light-runner and is only // visible when using --run-in-band, comes from inside jest but doesn't have an From 8bdadb363ded68cbf305073d5e3835ebf84be86a Mon Sep 17 00:00:00 2001 From: liuxingbaoyu <30521560+liuxingbaoyu@users.noreply.github.com> Date: Thu, 25 Aug 2022 05:52:19 +0800 Subject: [PATCH 10/13] fix windows test --- packages/babel-core/test/errors-stacks.js | 9 +++++++-- scripts/set-module-type.js | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/babel-core/test/errors-stacks.js b/packages/babel-core/test/errors-stacks.js index d3887116fce5..942246ca4d36 100644 --- a/packages/babel-core/test/errors-stacks.js +++ b/packages/babel-core/test/errors-stacks.js @@ -1,7 +1,8 @@ import * as babel from "../lib/index.js"; // TODO: Remove this in Babel 8, once we drop Node.js 8 -import { URL } from "url"; +import { fileURLToPath } from "url"; +import path from "path"; const replaceAll = "".replaceAll ? Function.call.bind("".replaceAll) @@ -69,7 +70,11 @@ function expectError(run) { } const fixture = name => - new URL(`./fixtures/errors/${name}`, import.meta.url).pathname; + path.join( + path.dirname(fileURLToPath(import.meta.url)), + "fixtures/errors", + name, + ); describe("@babel/core errors", function () { beforeAll(() => { diff --git a/scripts/set-module-type.js b/scripts/set-module-type.js index fa4dce6ff1dc..f976a3718a85 100755 --- a/scripts/set-module-type.js +++ b/scripts/set-module-type.js @@ -5,7 +5,7 @@ import { fileURLToPath } from "url"; import path from "path"; const root = rel => - path.join(fileURLToPath(path.dirname(import.meta.url)), "../", rel); + path.join(path.dirname(fileURLToPath(import.meta.url)), "../", rel); // prettier-ignore let moduleType; From ce9f2d251e7b5feef5862176c13d4b49db8bcc55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Thu, 25 Aug 2022 10:26:02 +0200 Subject: [PATCH 11/13] Update packages/babel-core/test/errors-stacks.js Co-authored-by: liuxingbaoyu <30521560+liuxingbaoyu@users.noreply.github.com> --- packages/babel-core/test/errors-stacks.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/babel-core/test/errors-stacks.js b/packages/babel-core/test/errors-stacks.js index 942246ca4d36..e044c3310ece 100644 --- a/packages/babel-core/test/errors-stacks.js +++ b/packages/babel-core/test/errors-stacks.js @@ -1,6 +1,5 @@ import * as babel from "../lib/index.js"; -// TODO: Remove this in Babel 8, once we drop Node.js 8 import { fileURLToPath } from "url"; import path from "path"; From 89542adc84451b6c59f4126f7d8a545286e71864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Thu, 25 Aug 2022 18:31:14 +0200 Subject: [PATCH 12/13] Move type casts to config validation --- .../babel-core/src/config/config-chain.ts | 4 ++-- .../config/validation/option-assertions.ts | 6 +++--- .../src/config/validation/options.ts | 8 +++++++- .../src/errors/rewrite-stack-trace.ts | 20 +++++++++++-------- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/babel-core/src/config/config-chain.ts b/packages/babel-core/src/config/config-chain.ts index 83853974f597..7867e039e433 100644 --- a/packages/babel-core/src/config/config-chain.ts +++ b/packages/babel-core/src/config/config-chain.ts @@ -54,7 +54,7 @@ export type PresetInstance = { }; export type ConfigContext = { - filename: string | void; + filename: string | undefined; cwd: string; root: string; envName: string; @@ -899,7 +899,7 @@ function matchesPatterns( function matchPattern( pattern: IgnoreItem, dirname: string, - pathToTest: unknown, + pathToTest: string | undefined, context: ConfigContext, configName?: string, ): boolean { diff --git a/packages/babel-core/src/config/validation/option-assertions.ts b/packages/babel-core/src/config/validation/option-assertions.ts index d233a02c3018..041e0c8cf0f5 100644 --- a/packages/babel-core/src/config/validation/option-assertions.ts +++ b/packages/babel-core/src/config/validation/option-assertions.ts @@ -256,7 +256,7 @@ function assertIgnoreItem(loc: GeneralPath, value: unknown): IgnoreItem { )} must be an array of string/Function/RegExp values, or undefined`, ); } - return value; + return value as IgnoreItem; } export function assertConfigApplicableTest( @@ -278,7 +278,7 @@ export function assertConfigApplicableTest( `${msg(loc)} must be a string/Function/RegExp, or an array of those`, ); } - return value; + return value as ConfigApplicableTest; } function checkValidTest(value: unknown): value is string | Function | RegExp { @@ -327,7 +327,7 @@ export function assertBabelrcSearch( `or an array of those, got ${JSON.stringify(value as any)}`, ); } - return value; + return value as BabelrcSearch; } export function assertPluginList( diff --git a/packages/babel-core/src/config/validation/options.ts b/packages/babel-core/src/config/validation/options.ts index cd62d884f725..0d96c0b15d20 100644 --- a/packages/babel-core/src/config/validation/options.ts +++ b/packages/babel-core/src/config/validation/options.ts @@ -201,7 +201,13 @@ export type CallerMetadata = { export type EnvSet = { [x: string]: T; }; -export type IgnoreItem = string | Function | RegExp; +export type IgnoreItem = + | string + | RegExp + | (( + path: string | undefined, + context: { dirname: string; caller: CallerMetadata; envName: string }, + ) => unknown); export type IgnoreList = ReadonlyArray; export type PluginOptions = object | void | false; diff --git a/packages/babel-core/src/errors/rewrite-stack-trace.ts b/packages/babel-core/src/errors/rewrite-stack-trace.ts index 9c8a30ae30b0..1b463077b627 100644 --- a/packages/babel-core/src/errors/rewrite-stack-trace.ts +++ b/packages/babel-core/src/errors/rewrite-stack-trace.ts @@ -94,26 +94,30 @@ export function expectedError(error: Error) { return error; } -export function beginHiddenCallStack(fn: Fn): Fn { +export function beginHiddenCallStack( + fn: (...args: A) => R, +) { if (!SUPPORTED) return fn; return Object.defineProperty( - function (this: any) { + function (...args: A) { setupPrepareStackTrace(); - return fn.apply(this, arguments); - } as any as Fn, + return fn(...args); + }, "name", { value: STOP_HIDNG }, ); } -export function endHiddenCallStack(fn: Fn): Fn { +export function endHiddenCallStack( + fn: (...args: A) => R, +) { if (!SUPPORTED) return fn; return Object.defineProperty( - function (this: any) { - return fn.apply(this, arguments); - } as any as Fn, + function (...args: A) { + return fn(...args); + }, "name", { value: START_HIDNG }, ); From 358f2c56c1baa24b3f506249f5f1e1c131387fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Sat, 27 Aug 2022 11:12:53 +0200 Subject: [PATCH 13/13] Review --- .../babel-core/src/errors/rewrite-stack-trace.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/babel-core/src/errors/rewrite-stack-trace.ts b/packages/babel-core/src/errors/rewrite-stack-trace.ts index 1b463077b627..954593f40da8 100644 --- a/packages/babel-core/src/errors/rewrite-stack-trace.ts +++ b/packages/babel-core/src/errors/rewrite-stack-trace.ts @@ -33,10 +33,11 @@ * - 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". + * injcectVirtualStackFrame(error, filename) function: those are injected as a + * replacement of the hidden frames. + * In the example above, if we called injcectVirtualStackFrame(err, "h") and + * injcectVirtualStackFrame(err, "i") on the expected error thrown by c(), its + * shown call stack would have been "h, i, e, f". * This can be useful, for example, to report config validation errors as if they * were directly thrown in the config file. */ @@ -137,7 +138,9 @@ function setupPrepareStackTrace() { let newTrace = []; const isExpected = expectedErrors.has(err); - let status = isExpected ? "hiding" : "unknown"; + let status: "showing" | "hiding" | "unknown" = isExpected + ? "hiding" + : "unknown"; for (let i = 0; i < trace.length; i++) { const name = trace[i].getFunctionName(); if (name === START_HIDNG) {