Skip to content

Commit

Permalink
Add error extraction script
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson committed Sep 24, 2023
1 parent 6cffdcb commit be2b255
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 0 deletions.
159 changes: 159 additions & 0 deletions packages/toolkit/scripts/mangleErrors.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
const fs = require('fs')
const path = require('path')
const helperModuleImports = require('@babel/helper-module-imports')

/**
* Converts an AST type into a javascript string so that it can be added to the error message lookup.
*
* Adapted from React (https://github.com/facebook/react/blob/master/scripts/shared/evalToString.js) with some
* adjustments
*/
const evalToString = (ast) => {
switch (ast.type) {
case 'StringLiteral':
case 'Literal': // ESLint
return ast.value
case 'BinaryExpression': // `+`
if (ast.operator !== '+') {
throw new Error('Unsupported binary operator ' + ast.operator)
}
return evalToString(ast.left) + evalToString(ast.right)
case 'TemplateLiteral':
return ast.quasis.reduce(
(concatenatedValue, templateElement) =>
concatenatedValue + templateElement.value.raw,
''
)
case 'Identifier':
return ast.name
default:
console.log('Bad AST in mangleErrors -> evalToString(): ', ast)
throw new Error(`Unsupported AST in evalToString: ${ast.type}, ${ast}`)
}
}

/**
* Takes a `throw new error` statement and transforms it depending on the minify argument. Either option results in a
* smaller bundle size in production for consumers.
*
* If minify is enabled, we'll replace the error message with just an index that maps to an arrow object lookup.
*
* If minify is disabled, we'll add in a conditional statement to check the process.env.NODE_ENV which will output a
* an error number index in production or the actual error message in development. This allows consumers using webpack
* or another build tool to have these messages in development but have just the error index in production.
*
* E.g.
* Before:
* throw new Error("This is my error message.");
* throw new Error("This is a second error message.");
*
* After (with minify):
* throw new Error(0);
* throw new Error(1);
*
* After: (without minify):
* throw new Error(node.process.NODE_ENV === 'production' ? 0 : "This is my error message.");
* throw new Error(node.process.NODE_ENV === 'production' ? 1 : "This is a second error message.");
*/
module.exports = (babel) => {
const t = babel.types
// When the plugin starts up, we'll load in the existing file. This allows us to continually add to it so that the
// indexes do not change between builds.
let errorsFiles = ''
// Save this to the root
const errorsPath = path.join(__dirname, '../../../errors.json')
if (fs.existsSync(errorsPath)) {
errorsFiles = fs.readFileSync(errorsPath).toString()
}
let errors = Object.values(JSON.parse(errorsFiles || '{}'))
// This variable allows us to skip writing back to the file if the errors array hasn't changed
let changeInArray = false

return {
pre: () => {
changeInArray = false
},
visitor: {
ThrowStatement(path, file) {
const args = path.node.argument.arguments
const minify = file.opts.minify

if (args && args[0]) {
// Skip running this logic when certain types come up:
// Identifier comes up when a variable is thrown (E.g. throw new error(message))
// NumericLiteral, CallExpression, and ConditionalExpression is code we have already processed
if (
path.node.argument.arguments[0].type === 'Identifier' ||
path.node.argument.arguments[0].type === 'NumericLiteral' ||
path.node.argument.arguments[0].type === 'ConditionalExpression' ||
path.node.argument.arguments[0].type === 'CallExpression' ||
path.node.argument.arguments[0].type === 'ObjectExpression' ||
path.node.argument.arguments[0].type === 'MemberExpression' ||
path.node.argument.arguments[0]?.callee?.name === 'HandledError'
) {
return
}

const errorMsgLiteral = evalToString(path.node.argument.arguments[0])

if (errorMsgLiteral.includes('Super expression')) {
// ignore Babel runtime error message
return
}

// Attempt to get the existing index of the error. If it is not found, add it to the array as a new error.
let errorIndex = errors.indexOf(errorMsgLiteral)
if (errorIndex === -1) {
errors.push(errorMsgLiteral)
errorIndex = errors.length - 1
changeInArray = true
}

// Import the error message function
const formatProdErrorMessageIdentifier = helperModuleImports.addNamed(
path,
'formatProdErrorMessage',
'@reduxjs/toolkit',
{ nameHint: 'formatProdErrorMessage' }
)

// Creates a function call to output the message to the error code page on the website
const prodMessage = t.callExpression(
formatProdErrorMessageIdentifier,
[t.numericLiteral(errorIndex)]
)

if (minify) {
path.replaceWith(
t.throwStatement(
t.newExpression(t.identifier('Error'), [prodMessage])
)
)
} else {
path.replaceWith(
t.throwStatement(
t.newExpression(t.identifier('Error'), [
t.conditionalExpression(
t.binaryExpression(
'===',
t.identifier('process.env.NODE_ENV'),
t.stringLiteral('production')
),
prodMessage,
path.node.argument.arguments[0]
),
])
)
)
}
}
},
},
post: () => {
// If there is a new error in the array, convert it to an indexed object and write it back to the file.
if (changeInArray) {
fs.writeFileSync(errorsPath, JSON.stringify({ ...errors }, null, 2))
}
},
}
}
13 changes: 13 additions & 0 deletions packages/toolkit/src/formatProdErrorMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Adapted from React: https://github.com/facebook/react/blob/master/packages/shared/formatProdErrorMessage.js
*
* Do not require this module directly! Use normal throw error calls. These messages will be replaced with error codes
* during build.
* @param {number} code
*/
export function formatProdErrorMessage(code: number) {
return (
`Minified Redux Toolkit error #${code}; visit https://redux-toolkit.js.org/Errors?code=${code} for the full message or ` +
'use the non-minified dev environment for full errors. '
)
}

0 comments on commit be2b255

Please sign in to comment.