mirror of
https://github.com/facebook/lexical.git
synced 2025-05-17 06:59:17 +08:00
199 lines
6.6 KiB
JavaScript
199 lines
6.6 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*/
|
|
|
|
'use strict';
|
|
// @ts-check
|
|
|
|
const fs = require('fs-extra');
|
|
const ErrorMap = require('./ErrorMap');
|
|
const evalToString = require('./evalToString');
|
|
const helperModuleImports = require('@babel/helper-module-imports');
|
|
const prettier = require('prettier');
|
|
|
|
/** @type {Map<string, ErrorMap>} */
|
|
const errorMaps = new Map();
|
|
/**
|
|
* Get a module-global ErrorMap instance so that all instances of this
|
|
* plugin are working with the same data structure. Typically there is
|
|
* at most one entry in this map (`${__dirname}/codes.json`).
|
|
*
|
|
* @param {string} filepath
|
|
* @returns {ErrorMap}
|
|
*/
|
|
function getErrorMap(filepath) {
|
|
let errorMap = errorMaps.get(filepath);
|
|
if (!errorMap) {
|
|
const prettierConfig = {
|
|
...(prettier.resolveConfig.sync('./') || {}),
|
|
filepath,
|
|
};
|
|
errorMap = new ErrorMap(fs.readJsonSync(filepath), (newErrorMap) =>
|
|
fs.writeFileSync(
|
|
filepath,
|
|
prettier.format(JSON.stringify(newErrorMap), prettierConfig),
|
|
),
|
|
);
|
|
errorMaps.set(filepath, errorMap);
|
|
}
|
|
return errorMap;
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} TransformErrorMessagesOptions
|
|
* @property {string} errorCodesPath
|
|
* @property {boolean} extractCodes
|
|
* @property {boolean} noMinify
|
|
*/
|
|
|
|
const invariantExpressions = [
|
|
{
|
|
dev: 'formatDevErrorMessage',
|
|
name: 'invariant',
|
|
prod: 'formatProdErrorMessage',
|
|
},
|
|
{
|
|
dev: 'formatDevErrorMessage',
|
|
name: 'devInvariant',
|
|
prod: 'formatProdWarningMessage',
|
|
},
|
|
];
|
|
|
|
/**
|
|
* @param {import('@babel/core')} babel
|
|
* @param {Partial<TransformErrorMessagesOptions>} opts
|
|
* @returns {import('@babel/core').PluginObj}
|
|
*/
|
|
module.exports = function (babel, opts) {
|
|
const t = babel.types;
|
|
const errorMap = getErrorMap(
|
|
(opts && opts.errorCodesPath) || `${__dirname}/codes.json`,
|
|
);
|
|
return {
|
|
visitor: {
|
|
CallExpression(path, file) {
|
|
const node = path.node;
|
|
const {extractCodes, noMinify} =
|
|
/** @type Partial<TransformErrorMessagesOptions> */ (file.opts);
|
|
for (const {name, dev, prod} of invariantExpressions) {
|
|
if (path.get('callee').isIdentifier({name})) {
|
|
// Turns this code:
|
|
//
|
|
// invariant(condition, 'A %s message that contains %s', adj, noun);
|
|
//
|
|
// into something equivalent to this:
|
|
//
|
|
// if (!condition) {
|
|
// if (__DEV__ || ERR_CODE === undefined) {
|
|
// formatDevErrorMessage(`A ${adj} message that contains ${noun}`);
|
|
// } else {
|
|
// formatProdErrorMessage(ERR_CODE, adj, noun)
|
|
// };
|
|
// }
|
|
//
|
|
// where ERR_CODE is an error code: a unique identifier (a number
|
|
// string) that references a verbose error message. The mapping is
|
|
// stored in `scripts/error-codes/codes.json`.
|
|
const condition = node.arguments[0];
|
|
const errorMsgLiteral = evalToString(node.arguments[1]);
|
|
const errorMsgExpressions = Array.from(node.arguments.slice(2));
|
|
const errorMsgQuasis = errorMsgLiteral
|
|
.split('%s')
|
|
.map((raw) =>
|
|
t.templateElement({cooked: String.raw({raw}), raw}),
|
|
);
|
|
|
|
// Outputs:
|
|
// `A ${adj} message that contains ${noun}`;
|
|
const devMessage = t.templateLiteral(
|
|
errorMsgQuasis,
|
|
errorMsgExpressions,
|
|
);
|
|
|
|
const parentStatementPath = path.parentPath;
|
|
if (parentStatementPath.type !== 'ExpressionStatement') {
|
|
throw path.buildCodeFrameError(
|
|
`${name}() cannot be called from expression context. Move the call to its own statement.`,
|
|
);
|
|
}
|
|
|
|
// We extract the prodErrorId even if we are not using it
|
|
// so we can extractCodes in a non-production build.
|
|
let prodErrorId = errorMap.getOrAddToErrorMap(
|
|
errorMsgLiteral,
|
|
extractCodes,
|
|
);
|
|
|
|
/** @type {babel.types.CallExpression} */
|
|
let callExpression;
|
|
if (noMinify || prodErrorId === undefined) {
|
|
// Error minification is disabled for this build.
|
|
//
|
|
// Outputs:
|
|
// if (!condition) {
|
|
// formatDevErrorMessage(`A ${adj} message that contains ${noun}`);
|
|
// }
|
|
const formatDevErrorMessageIdentifier =
|
|
helperModuleImports.addDefault(path, `shared/${dev}`, {
|
|
nameHint: dev,
|
|
});
|
|
callExpression = t.callExpression(
|
|
formatDevErrorMessageIdentifier,
|
|
[devMessage],
|
|
);
|
|
} else {
|
|
// Error minification enabled for this build.
|
|
//
|
|
// Outputs:
|
|
// if (!condition) {
|
|
// formatProdErrorMessage(ERR_CODE, adj, noun)
|
|
// }
|
|
|
|
// Import ReactErrorProd
|
|
const formatProdErrorMessageIdentifier =
|
|
helperModuleImports.addDefault(path, `shared/${prod}`, {
|
|
nameHint: prod,
|
|
});
|
|
|
|
// Outputs:
|
|
// formatProdErrorMessage(ERR_CODE, adj, noun);
|
|
callExpression = t.callExpression(
|
|
formatProdErrorMessageIdentifier,
|
|
[t.numericLiteral(prodErrorId), ...errorMsgExpressions],
|
|
);
|
|
}
|
|
|
|
parentStatementPath.replaceWith(
|
|
t.ifStatement(
|
|
t.unaryExpression('!', condition),
|
|
t.blockStatement([t.expressionStatement(callExpression)]),
|
|
),
|
|
);
|
|
|
|
if (!noMinify && prodErrorId === undefined) {
|
|
// There is no error code for this message. Add an inline comment
|
|
// that flags this as an unminified error. This allows the build
|
|
// to proceed, while also allowing a post-build linter to detect it.
|
|
//
|
|
// Outputs:
|
|
// /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
|
|
// if (!condition) {
|
|
// formatDevErrorMessage(`A ${adj} message that contains ${noun}`);
|
|
// }
|
|
parentStatementPath.addComment(
|
|
'leading',
|
|
'FIXME (minify-errors-in-prod): Unminified error message in production build!',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
},
|
|
};
|
|
};
|