Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize bundles for RTK 2.0 #3740

Merged
merged 6 commits into from
Sep 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions errors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"0": "The object notation for `createReducer` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer",
"1": "A case reducer on a non-draftable value must not return undefined",
"2": ": ",
"3": "prepareAction did not return an object",
"4": "\"reducer\" is a required argument, and must be a function or an object of functions that can be passed to combineReducers",
"5": "when using a middleware builder function, an array of middleware must be returned",
"6": "each middleware provided to configureStore must be a function",
"7": "\"enhancers\" field must be a callback",
"8": "\"enhancers\" callback must return an array",
"9": "each enhancer provided to configureStore must be a function",
"10": "`name` is a required option for createSlice",
"11": "The object notation for `createSlice.extraReducers` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createSlice",
"12": "selectState returned undefined for an uninjected slice reducer",
"13": "Please use the `create.preparedReducer` notation for prepared action creators with the `create` notation.",
"14": "The slice reducer for key \"\" returned undefined when called for selector(). If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.",
"15": "original must be used on state Proxy",
"16": "Creating or removing a listener requires one of the known fields for matching an action",
"17": "Unsubscribe not initialized",
"18": ": getOriginalState can only be called synchronously",
"19": "`builder.addCase` should only be called before calling `builder.addMatcher`",
"20": "`builder.addCase` should only be called before calling `builder.addDefaultCase`",
"21": "addCase cannot be called with two reducers for the same action type",
"22": "`builder.addMatcher` should only be called before calling `builder.addDefaultCase`",
"23": "`builder.addDefaultCase` can only be called once",
"24": " is not a function",
"25": "When using `fakeBaseQuery`, all queries & mutations must use the `queryFn` definition syntax.",
"26": "Warning: Middleware for RTK-Query API at reducerPath \"\" has not been added to the store.\nYou must add the middleware for RTK-Query to function correctly!",
"27": "Warning: Middleware for RTK-Query API at reducerPath \"\" has not been added to the store.\n You must add the middleware for RTK-Query to function correctly!",
"28": "Cannot refetch a query that has not been started yet."
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@babel/helper-compilation-targets": "7.19.3",
"@babel/traverse": "7.19.3",
"@babel/types": "7.19.3",
"esbuild": "0.17.17",
"esbuild": "0.19.3",
"jest-snapshot": "29.3.1",
"msw": "patch:msw@npm:0.40.2#.yarn/patches/msw-npm-0.40.2-2107d48752",
"jscodeshift": "0.13.1",
Expand Down
1 change: 1 addition & 0 deletions packages/toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"axios": "^0.19.2",
"console-testing-library": "0.6.1",
"convert-source-map": "^1.7.0",
"esbuild-extra": "^0.3.1",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-react-app": "^7.0.1",
Expand Down
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. '
)
}
6 changes: 6 additions & 0 deletions packages/toolkit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// This must remain here so that the `mangleErrors.cjs` build script
// does not have to import this into each source file it rewrites.
import { formatProdErrorMessage } from './formatProdErrorMessage'

export * from 'redux'
export {
produce as createNextState,
Expand Down Expand Up @@ -207,3 +211,5 @@ export { combineSlices } from './combineSlices'
export type { WithSlice } from './combineSlices'

export type { ExtractDispatchExtensions as TSHelpersExtractDispatchExtensions } from './tsHelpers'

export { formatProdErrorMessage } from './formatProdErrorMessage'
3 changes: 2 additions & 1 deletion packages/toolkit/src/listenerMiddleware/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { addAbortSignalListener, catchRejection, noop } from './utils'
*/
export const validateActive = (signal: AbortSignal): void => {
if (signal.aborted) {
throw new TaskAbortError((signal as AbortSignalWithReason<string>).reason)
const { reason } = signal as AbortSignalWithReason<string>
throw new TaskAbortError(reason)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isAsyncThunkAction, isFulfilled } from '@reduxjs/toolkit'
import { isAsyncThunkAction, isFulfilled } from '../rtkImports'
import type { UnknownAction } from 'redux'
import type { ThunkDispatch } from 'redux-thunk'
import type { BaseQueryFn, BaseQueryMeta } from '../../baseQueryTypes'
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/query/core/buildMiddleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
ThunkDispatch,
UnknownAction,
} from '@reduxjs/toolkit'
import { isAction, createAction } from '@reduxjs/toolkit'
import { isAction, createAction } from '../rtkImports'

import type {
EndpointDefinitions,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isAnyOf, isFulfilled, isRejectedWithValue } from '@reduxjs/toolkit'
import { isAnyOf, isFulfilled, isRejectedWithValue } from '../rtkImports'

import type { FullTagDescription } from '../../endpointDefinitions'
import { calculateProvidedBy } from '../../endpointDefinitions'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isPending, isRejected, isFulfilled } from '@reduxjs/toolkit'
import { isPending, isRejected, isFulfilled } from '../rtkImports'
import type {
BaseQueryError,
BaseQueryFn,
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/query/core/buildSelectors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createNextState, createSelector } from '@reduxjs/toolkit'
import { createNextState, createSelector } from './rtkImports'
import type {
MutationSubState,
QuerySubState,
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/query/core/buildSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
isRejectedWithValue,
createNextState,
prepareAutoBatched,
} from '@reduxjs/toolkit'
} from './rtkImports'
import type {
QuerySubstateIdentifier,
QuerySubState,
Expand Down
5 changes: 3 additions & 2 deletions packages/toolkit/src/query/core/buildThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ import {
isPending,
isRejected,
isRejectedWithValue,
} from '@reduxjs/toolkit'
createAsyncThunk,
SHOULD_AUTOBATCH,
} from './rtkImports'
import type { Patch } from 'immer'
import { isDraftable, produceWithPatches } from 'immer'
import type { ThunkAction, ThunkDispatch, AsyncThunk } from '@reduxjs/toolkit'
import { createAsyncThunk, SHOULD_AUTOBATCH } from '@reduxjs/toolkit'

import { HandledError } from '../HandledError'

Expand Down
24 changes: 24 additions & 0 deletions packages/toolkit/src/query/core/rtkImports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// This file exists to consolidate all of the imports from the `@reduxjs/toolkit` package.
// ESBuild does not de-duplicate imports, so this file is used to ensure that each method
// imported is only listed once, and there's only one mention of the `@reduxjs/toolkit` package.

export {
createAction,
createSlice,
createSelector,
createAsyncThunk,
combineReducers,
createNextState,
isAnyOf,
isAllOf,
isAction,
isPending,
isRejected,
isFulfilled,
isRejectedWithValue,
isAsyncThunkAction,
prepareAutoBatched,
SHOULD_AUTOBATCH,
isPlainObject,
nanoid,
} from '@reduxjs/toolkit'
2 changes: 1 addition & 1 deletion packages/toolkit/src/query/core/setupListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {
ThunkDispatch,
ActionCreatorWithoutPayload, // Workaround for API-Extractor
} from '@reduxjs/toolkit'
import { createAction } from '@reduxjs/toolkit'
import { createAction } from './rtkImports'

export const onFocus = /* @__PURE__ */ createAction('__rtkq/focused')
export const onFocusLost = /* @__PURE__ */ createAction('__rtkq/unfocused')
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/query/createApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
EndpointDefinitions,
} from './endpointDefinitions'
import { DefinitionType, isQueryDefinition } from './endpointDefinitions'
import { nanoid } from '@reduxjs/toolkit'
import { nanoid } from './core/rtkImports'
import type { UnknownAction } from '@reduxjs/toolkit'
import type { NoInfer } from './tsHelpers'
import { defaultMemoize } from 'reselect'
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/query/defaultSerializeQueryArgs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { QueryCacheKey } from './core/apiState'
import type { EndpointDefinition } from './endpointDefinitions'
import { isPlainObject } from '@reduxjs/toolkit'
import { isPlainObject } from './core/rtkImports'

const cache: WeakMap<any, string> | undefined = WeakMap
? new WeakMap()
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/query/fetchBaseQuery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { joinUrls } from './utils'
import { isPlainObject } from '@reduxjs/toolkit'
import { isPlainObject } from './core/rtkImports'
import type { BaseQueryApi, BaseQueryFn } from './baseQueryTypes'
import type { MaybePromise, Override } from './tsHelpers'

Expand Down
4 changes: 4 additions & 0 deletions packages/toolkit/src/query/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// This must remain here so that the `mangleErrors.cjs` build script
// does not have to import this into each source file it rewrites.
import { formatProdErrorMessage } from '@reduxjs/toolkit'

export type {
QuerySubState,
SubscriptionOptions,
Expand Down
Loading
Loading