diff --git a/packages/babel-core/src/transformation/normalize-file.js b/packages/babel-core/src/transformation/normalize-file.js index 2a0fbc0250..8124db2885 100644 --- a/packages/babel-core/src/transformation/normalize-file.js +++ b/packages/babel-core/src/transformation/normalize-file.js @@ -6,6 +6,7 @@ import convertSourceMap, { typeof Converter } from "convert-source-map"; import { parse } from "babylon"; import { codeFrameColumns } from "@babel/code-frame"; import File from "./file/file"; +import generateMissingPluginMessage from "./util/missing-plugin-helper"; const shebangRegex = /^#!.*/; @@ -88,21 +89,27 @@ function parser(pluginPasses, options, code) { } throw new Error("More than one plugin attempted to override parsing."); } catch (err) { - const loc = err.loc; + const { loc, missingPlugin } = err; if (loc) { err.loc = null; - err.message = - `${options.filename || "unknown"}: ${err.message}\n` + - codeFrameColumns( - code, - { - start: { - line: loc.line, - column: loc.column + 1, - }, + const codeFrame = codeFrameColumns( + code, + { + start: { + line: loc.line, + column: loc.column + 1, }, - options, - ); + }, + options, + ); + if (missingPlugin) { + err.message = + `${options.filename || "unknown"}: ` + + generateMissingPluginMessage(missingPlugin[0], loc, codeFrame); + } else { + err.message = + `${options.filename || "unknown"}: ${err.message}\n\n` + codeFrame; + } } throw err; } diff --git a/packages/babel-core/src/transformation/util/missing-plugin-helper.js b/packages/babel-core/src/transformation/util/missing-plugin-helper.js new file mode 100644 index 0000000000..0cc73a4a46 --- /dev/null +++ b/packages/babel-core/src/transformation/util/missing-plugin-helper.js @@ -0,0 +1,236 @@ +// @flow + +const pluginNameMap = { + asyncGenerators: { + syntax: { + name: "@babel/plugin-syntax-async-generators", + url: "https://git.io/vb4SY", + }, + transform: { + name: "@babel/plugin-proposal-async-generator-functions", + url: "https://git.io/vb4yp", + }, + }, + classProperties: { + syntax: { + name: "@babel/plugin-syntax-class-properties", + url: "https://git.io/vb4yQ", + }, + transform: { + name: "@babel/plugin-proposal-class-properties", + url: "https://git.io/vb4SL", + }, + }, + decorators: { + syntax: { + name: "@babel/plugin-syntax-decorators", + url: "https://git.io/vb4y9", + }, + transform: { + name: "@babel/plugin-proposal-decorators", + url: "https://git.io/vb4ST", + }, + }, + doExpressions: { + syntax: { + name: "@babel/plugin-syntax-do-expressions", + url: "https://git.io/vb4yh", + }, + transform: { + name: "@babel/plugin-proposal-do-expressions", + url: "https://git.io/vb4S3", + }, + }, + dynamicImport: { + syntax: { + name: "@babel/plugin-syntax-dynamic-import", + url: "https://git.io/vb4Sv", + }, + }, + exportDefaultFrom: { + syntax: { + name: "@babel/plugin-syntax-export-default-from", + url: "https://git.io/vb4SO", + }, + transform: { + name: "@babel/plugin-proposal-export-default-from", + url: "https://git.io/vb4yH", + }, + }, + exportNamespaceFrom: { + syntax: { + name: "@babel/plugin-syntax-export-namespace-from", + url: "https://git.io/vb4Sf", + }, + transform: { + name: "@babel/plugin-proposal-export-namespace-from", + url: "https://git.io/vb4SG", + }, + }, + flow: { + syntax: { + name: "@babel/plugin-syntax-flow", + url: "https://git.io/vb4yb", + }, + transform: { + name: "@babel/plugin-transform-flow-strip-types", + url: "https://git.io/vb49g", + }, + }, + functionBind: { + syntax: { + name: "@babel/plugin-syntax-function-bind", + url: "https://git.io/vb4y7", + }, + transform: { + name: "@babel/plugin-proposal-function-bind", + url: "https://git.io/vb4St", + }, + }, + functionSent: { + syntax: { + name: "@babel/plugin-syntax-function-sent", + url: "https://git.io/vb4yN", + }, + transform: { + name: "@babel/plugin-proposal-function-sent", + url: "https://git.io/vb4SZ", + }, + }, + importMeta: { + syntax: { + name: "@babel/plugin-syntax-import-meta", + url: "https://git.io/vbKK6", + }, + }, + jsx: { + syntax: { + name: "@babel/plugin-syntax-jsx", + url: "https://git.io/vb4yA", + }, + transform: { + name: "@babel/plugin-transform-react-jsx", + url: "https://git.io/vb4yd", + }, + }, + nullishCoalescingOperator: { + syntax: { + name: "@babel/plugin-syntax-nullish-coalescing-operator", + url: "https://git.io/vb4yx", + }, + transform: { + name: "@babel/plugin-proposal-nullish-coalescing-operator", + url: "https://git.io/vb4Se", + }, + }, + numericSeparator: { + syntax: { + name: "@babel/plugin-syntax-numeric-separator", + url: "https://git.io/vb4Sq", + }, + transform: { + name: "@babel/plugin-proposal-numeric-separator", + url: "https://git.io/vb4yS", + }, + }, + objectRestSpread: { + syntax: { + name: "@babel/plugin-syntax-object-rest-spread", + url: "https://git.io/vb4y5", + }, + transform: { + name: "@babel/plugin-proposal-object-rest-spread", + url: "https://git.io/vb4Ss", + }, + }, + optionalCatchBinding: { + syntax: { + name: "@babel/plugin-syntax-optional-catch-binding", + url: "https://git.io/vb4Sn", + }, + transform: { + name: "@babel/plugin-proposal-optional-catch-binding", + url: "https://git.io/vb4SI", + }, + }, + optionalChaining: { + syntax: { + name: "@babel/plugin-syntax-optional-chaining", + url: "https://git.io/vb4Sc", + }, + transform: { + name: "@babel/plugin-proposal-optional-chaining", + url: "https://git.io/vb4Sk", + }, + }, + pipelineOperator: { + syntax: { + name: "@babel/plugin-syntax-pipeline-operator", + url: "https://git.io/vb4yj", + }, + transform: { + name: "@babel/plugin-proposal-pipeline-operator", + url: "https://git.io/vb4SU", + }, + }, + throwExpressions: { + syntax: { + name: "@babel/plugin-syntax-throw-expressions", + url: "https://git.io/vb4SJ", + }, + transform: { + name: "@babel/plugin-proposal-throw-expressions", + url: "https://git.io/vb4yF", + }, + }, + typescript: { + syntax: { + name: "@babel/plugin-syntax-typescript", + url: "https://git.io/vb4SC", + }, + transform: { + name: "@babel/plugin-transform-typescript", + url: "https://git.io/vb4Sm", + }, + }, +}; + +const getNameURLCombination = ({ name, url }) => `${name} (${url})`; + +/* +Returns a string of the format: +Support for the experimental syntax [babylon plugin name] isn't currently enabled ([loc]): + +[code frame] + +Add [npm package name] ([url]) to the 'plugins' section of your Babel config +to enable [parsing|transformation]. +*/ +export default function generateMissingPluginMessage( + missingPluginName: string, + loc, + codeFrame, +): string { + let helpMessage = + `Support for the experimental syntax '${missingPluginName}' isn't currently enabled ` + + `(${loc.line}:${loc.column + 1}):\n\n` + + codeFrame; + const pluginInfo = pluginNameMap[missingPluginName]; + if (pluginInfo) { + const { syntax: syntaxPlugin, transform: transformPlugin } = pluginInfo; + if (syntaxPlugin) { + if (transformPlugin) { + const transformPluginInfo = getNameURLCombination(transformPlugin); + helpMessage += + `\n\nAdd ${transformPluginInfo} to the 'plugins' section of your Babel config ` + + `to enable transformation.`; + } else { + const syntaxPluginInfo = getNameURLCombination(syntaxPlugin); + helpMessage += + `\n\nAdd ${syntaxPluginInfo} to the 'plugins' section of your Babel config ` + + `to enable parsing.`; + } + } + } + return helpMessage; +} diff --git a/packages/babel-core/test/api.js b/packages/babel-core/test/api.js index c37c2bcf28..a38ae5913c 100644 --- a/packages/babel-core/test/api.js +++ b/packages/babel-core/test/api.js @@ -599,4 +599,52 @@ describe("api", function() { assert.ok(script.indexOf("typeof") >= 0); }); }); + + describe("handle parsing errors", function() { + const options = { + babelrc: false, + }; + + it("only syntax plugin available", function(done) { + babel.transformFile( + __dirname + "/fixtures/api/parsing-errors/only-syntax/file.js", + options, + function(err) { + assert.ok( + RegExp( + "Support for the experimental syntax 'dynamicImport' isn't currently enabled \\(1:9\\):", + ).exec(err.message), + ); + assert.ok( + RegExp( + "Add @babel/plugin-syntax-dynamic-import \\(https://git.io/vb4Sv\\) to the " + + "'plugins' section of your Babel config to enable parsing.", + ).exec(err.message), + ); + done(); + }, + ); + }); + + it("both syntax and transform plugin available", function(done) { + babel.transformFile( + __dirname + "/fixtures/api/parsing-errors/syntax-and-transform/file.js", + options, + function(err) { + assert.ok( + RegExp( + "Support for the experimental syntax 'asyncGenerators' isn't currently enabled \\(1:15\\):", + ).exec(err.message), + ); + assert.ok( + RegExp( + "Add @babel/plugin-proposal-async-generator-functions \\(https://git.io/vb4yp\\) to the " + + "'plugins' section of your Babel config to enable transformation.", + ).exec(err.message), + ); + done(); + }, + ); + }); + }); }); diff --git a/packages/babel-core/test/fixtures/api/parsing-errors/only-syntax/file.js b/packages/babel-core/test/fixtures/api/parsing-errors/only-syntax/file.js new file mode 100644 index 0000000000..55194f0636 --- /dev/null +++ b/packages/babel-core/test/fixtures/api/parsing-errors/only-syntax/file.js @@ -0,0 +1 @@ +var $ = import("jquery"); \ No newline at end of file diff --git a/packages/babel-core/test/fixtures/api/parsing-errors/syntax-and-transform/file.js b/packages/babel-core/test/fixtures/api/parsing-errors/syntax-and-transform/file.js new file mode 100644 index 0000000000..a0eb4637c6 --- /dev/null +++ b/packages/babel-core/test/fixtures/api/parsing-errors/syntax-and-transform/file.js @@ -0,0 +1,4 @@ +async function* agf() { + await 1; + yield 2; +}