Skip to content

Commit be2b255

Browse files
committed
Add error extraction script
1 parent 6cffdcb commit be2b255

File tree

2 files changed

+172
-0
lines changed

2 files changed

+172
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
const fs = require('fs')
2+
const path = require('path')
3+
const helperModuleImports = require('@babel/helper-module-imports')
4+
5+
/**
6+
* Converts an AST type into a javascript string so that it can be added to the error message lookup.
7+
*
8+
* Adapted from React (https://github.com/facebook/react/blob/master/scripts/shared/evalToString.js) with some
9+
* adjustments
10+
*/
11+
const evalToString = (ast) => {
12+
switch (ast.type) {
13+
case 'StringLiteral':
14+
case 'Literal': // ESLint
15+
return ast.value
16+
case 'BinaryExpression': // `+`
17+
if (ast.operator !== '+') {
18+
throw new Error('Unsupported binary operator ' + ast.operator)
19+
}
20+
return evalToString(ast.left) + evalToString(ast.right)
21+
case 'TemplateLiteral':
22+
return ast.quasis.reduce(
23+
(concatenatedValue, templateElement) =>
24+
concatenatedValue + templateElement.value.raw,
25+
''
26+
)
27+
case 'Identifier':
28+
return ast.name
29+
default:
30+
console.log('Bad AST in mangleErrors -> evalToString(): ', ast)
31+
throw new Error(`Unsupported AST in evalToString: ${ast.type}, ${ast}`)
32+
}
33+
}
34+
35+
/**
36+
* Takes a `throw new error` statement and transforms it depending on the minify argument. Either option results in a
37+
* smaller bundle size in production for consumers.
38+
*
39+
* If minify is enabled, we'll replace the error message with just an index that maps to an arrow object lookup.
40+
*
41+
* If minify is disabled, we'll add in a conditional statement to check the process.env.NODE_ENV which will output a
42+
* an error number index in production or the actual error message in development. This allows consumers using webpack
43+
* or another build tool to have these messages in development but have just the error index in production.
44+
*
45+
* E.g.
46+
* Before:
47+
* throw new Error("This is my error message.");
48+
* throw new Error("This is a second error message.");
49+
*
50+
* After (with minify):
51+
* throw new Error(0);
52+
* throw new Error(1);
53+
*
54+
* After: (without minify):
55+
* throw new Error(node.process.NODE_ENV === 'production' ? 0 : "This is my error message.");
56+
* throw new Error(node.process.NODE_ENV === 'production' ? 1 : "This is a second error message.");
57+
*/
58+
module.exports = (babel) => {
59+
const t = babel.types
60+
// When the plugin starts up, we'll load in the existing file. This allows us to continually add to it so that the
61+
// indexes do not change between builds.
62+
let errorsFiles = ''
63+
// Save this to the root
64+
const errorsPath = path.join(__dirname, '../../../errors.json')
65+
if (fs.existsSync(errorsPath)) {
66+
errorsFiles = fs.readFileSync(errorsPath).toString()
67+
}
68+
let errors = Object.values(JSON.parse(errorsFiles || '{}'))
69+
// This variable allows us to skip writing back to the file if the errors array hasn't changed
70+
let changeInArray = false
71+
72+
return {
73+
pre: () => {
74+
changeInArray = false
75+
},
76+
visitor: {
77+
ThrowStatement(path, file) {
78+
const args = path.node.argument.arguments
79+
const minify = file.opts.minify
80+
81+
if (args && args[0]) {
82+
// Skip running this logic when certain types come up:
83+
// Identifier comes up when a variable is thrown (E.g. throw new error(message))
84+
// NumericLiteral, CallExpression, and ConditionalExpression is code we have already processed
85+
if (
86+
path.node.argument.arguments[0].type === 'Identifier' ||
87+
path.node.argument.arguments[0].type === 'NumericLiteral' ||
88+
path.node.argument.arguments[0].type === 'ConditionalExpression' ||
89+
path.node.argument.arguments[0].type === 'CallExpression' ||
90+
path.node.argument.arguments[0].type === 'ObjectExpression' ||
91+
path.node.argument.arguments[0].type === 'MemberExpression' ||
92+
path.node.argument.arguments[0]?.callee?.name === 'HandledError'
93+
) {
94+
return
95+
}
96+
97+
const errorMsgLiteral = evalToString(path.node.argument.arguments[0])
98+
99+
if (errorMsgLiteral.includes('Super expression')) {
100+
// ignore Babel runtime error message
101+
return
102+
}
103+
104+
// Attempt to get the existing index of the error. If it is not found, add it to the array as a new error.
105+
let errorIndex = errors.indexOf(errorMsgLiteral)
106+
if (errorIndex === -1) {
107+
errors.push(errorMsgLiteral)
108+
errorIndex = errors.length - 1
109+
changeInArray = true
110+
}
111+
112+
// Import the error message function
113+
const formatProdErrorMessageIdentifier = helperModuleImports.addNamed(
114+
path,
115+
'formatProdErrorMessage',
116+
'@reduxjs/toolkit',
117+
{ nameHint: 'formatProdErrorMessage' }
118+
)
119+
120+
// Creates a function call to output the message to the error code page on the website
121+
const prodMessage = t.callExpression(
122+
formatProdErrorMessageIdentifier,
123+
[t.numericLiteral(errorIndex)]
124+
)
125+
126+
if (minify) {
127+
path.replaceWith(
128+
t.throwStatement(
129+
t.newExpression(t.identifier('Error'), [prodMessage])
130+
)
131+
)
132+
} else {
133+
path.replaceWith(
134+
t.throwStatement(
135+
t.newExpression(t.identifier('Error'), [
136+
t.conditionalExpression(
137+
t.binaryExpression(
138+
'===',
139+
t.identifier('process.env.NODE_ENV'),
140+
t.stringLiteral('production')
141+
),
142+
prodMessage,
143+
path.node.argument.arguments[0]
144+
),
145+
])
146+
)
147+
)
148+
}
149+
}
150+
},
151+
},
152+
post: () => {
153+
// If there is a new error in the array, convert it to an indexed object and write it back to the file.
154+
if (changeInArray) {
155+
fs.writeFileSync(errorsPath, JSON.stringify({ ...errors }, null, 2))
156+
}
157+
},
158+
}
159+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Adapted from React: https://github.com/facebook/react/blob/master/packages/shared/formatProdErrorMessage.js
3+
*
4+
* Do not require this module directly! Use normal throw error calls. These messages will be replaced with error codes
5+
* during build.
6+
* @param {number} code
7+
*/
8+
export function formatProdErrorMessage(code: number) {
9+
return (
10+
`Minified Redux Toolkit error #${code}; visit https://redux-toolkit.js.org/Errors?code=${code} for the full message or ` +
11+
'use the non-minified dev environment for full errors. '
12+
)
13+
}

0 commit comments

Comments
 (0)