Skip to content

Commit

Permalink
Merge pull request #4586 from aryaemami59/fix-mangleErrors
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson authored Aug 30, 2024
2 parents 259892d + 9cc383e commit 6353756
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
const fs = require('fs')
const path = require('path')
const helperModuleImports = require('@babel/helper-module-imports')
import type { Node, PluginObj, PluginPass } from '@babel/core'
import * as helperModuleImports from '@babel/helper-module-imports'
import * as fs from 'node:fs'
import * as path from 'node:path'

type Babel = typeof import('@babel/core')

/**
* Represents the options for the {@linkcode mangleErrorsPlugin}.
*
* @internal
*/
export interface MangleErrorsPluginOptions {
/**
* Whether to minify the error messages or not.
* If `true`, the error messages will be replaced with an index
* that maps object lookup.
*/
minify: boolean
}

/**
* Converts an AST type into a javascript string so that it can be added to the error message lookup.
* 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
* Adapted from React
* {@linkcode https://github.com/facebook/react/blob/master/scripts/shared/evalToString.js | evalToString}
* with some adjustments.
*/
const evalToString = (ast) => {
const evalToString = (
ast: Node | { type: 'Literal'; value: string },
): string => {
switch (ast.type) {
case 'StringLiteral':
case 'Literal': // ESLint
Expand All @@ -33,29 +54,54 @@ const evalToString = (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.
* Transforms a `throw new Error` statement based on the
* {@linkcode MangleErrorsPluginOptions.minify | minify} argument,
* resulting in a smaller bundle size for consumers in production.
*
* If minify is enabled, we'll replace the error message with just an index that maps to an arrow object lookup.
* If {@linkcode MangleErrorsPluginOptions.minify | minify} is enabled,
* the error message will be replaced with an index that maps to
* an 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.
* If {@linkcode MangleErrorsPluginOptions.minify | minify} is disabled,
* a conditional statement will be added to check `process.env.NODE_ENV`,
* which will output 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 only 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.");
* @example
* <caption>__Before:__</caption>
*
* After (with minify):
* throw new Error(0);
* throw new Error(1);
* ```ts
* throw new Error('each middleware provided to configureStore must be a function');
* throw new Error(
* '`reducer` is a required argument, and must be a function or an object of functions that can be passed to combineReducers',
* )
* ```
*
* 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.");
* @example
* <caption>__After (with minify):__</caption>
*
* ```ts
* throw new Error(formatProdErrorMessage(0));
* throw new Error(formatProdErrorMessage(1));
* ```
*
* @example
* <caption>__After (without minify):__</caption>
*
* ```ts
* throw new Error(
* process.env.NODE_ENV === 'production'
* ? formatProdErrorMessage(4)
* : 'each middleware provided to configureStore must be a function',
* )
* ```
*/
module.exports = (babel) => {
export const mangleErrorsPlugin = (
babel: Babel,
options: MangleErrorsPluginOptions,
): PluginObj<PluginPass & MangleErrorsPluginOptions> => {
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.
Expand All @@ -65,18 +111,25 @@ module.exports = (babel) => {
if (fs.existsSync(errorsPath)) {
errorsFiles = fs.readFileSync(errorsPath).toString()
}
let errors = Object.values(JSON.parse(errorsFiles || '{}'))
const 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 {
name: 'mangle-errors-plugin',
pre: () => {
changeInArray = false
},
visitor: {
ThrowStatement(path, file) {
ThrowStatement(path) {
if (
!('arguments' in path.node.argument) ||
!t.isNewExpression(path.node.argument)
) {
return
}
const args = path.node.argument.arguments
const minify = file.opts.minify
const { minify } = options

if (args && args[0]) {
// Skip running this logic when certain types come up:
Expand All @@ -89,11 +142,14 @@ module.exports = (babel) => {
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'
!t.isExpression(path.node.argument.arguments[0]) ||
!t.isIdentifier(path.node.argument.callee)
) {
return
}

const errorName = path.node.argument.callee.name

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

if (errorMsgLiteral.includes('Super expression')) {
Expand Down Expand Up @@ -126,13 +182,13 @@ module.exports = (babel) => {
if (minify) {
path.replaceWith(
t.throwStatement(
t.newExpression(t.identifier('Error'), [prodMessage]),
t.newExpression(t.identifier(errorName), [prodMessage]),
),
)
} else {
path.replaceWith(
t.throwStatement(
t.newExpression(t.identifier('Error'), [
t.newExpression(t.identifier(errorName), [
t.conditionalExpression(
t.binaryExpression(
'===',
Expand All @@ -157,3 +213,5 @@ module.exports = (babel) => {
},
}
}

export default mangleErrorsPlugin
22 changes: 17 additions & 5 deletions packages/toolkit/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import path from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Options as TsupOptions } from 'tsup'
import { defineConfig } from 'tsup'
import type { MangleErrorsPluginOptions } from './scripts/mangleErrors.mjs'
import { mangleErrorsPlugin } from './scripts/mangleErrors.mjs'

// No __dirname under Node ESM
const __filename = fileURLToPath(import.meta.url)
Expand Down Expand Up @@ -129,18 +131,28 @@ if (process.env.NODE_ENV === 'production') {

// Extract error strings, replace them with error codes, and write messages to a file
const mangleErrorsTransform: Plugin = {
name: 'mangle-errors-plugin',
name: mangleErrorsPlugin.name,
setup(build) {
const { onTransform } = getBuildExtensions(build, 'mangle-errors-plugin')
const { onTransform } = getBuildExtensions(build, mangleErrorsPlugin.name)

onTransform({ loaders: ['ts', 'tsx'] }, async (args) => {
try {
const res = babel.transformSync(args.code, {
const res = await babel.transformAsync(args.code, {
parserOpts: {
plugins: ['typescript', 'jsx'],
},
plugins: [['./scripts/mangleErrors.cjs', { minify: false }]],
})!
plugins: [
[
mangleErrorsPlugin,
{ minify: false } satisfies MangleErrorsPluginOptions,
],
],
})

if (res == null) {
throw new Error('Babel transformAsync returned null')
}

return {
code: res.code!,
map: res.map!,
Expand Down

0 comments on commit 6353756

Please sign in to comment.