Skip to content

Commit 7d40b8a

Browse files
committed
Lint rule to detect unminified errors
Adds a lint rule that detects when an Error constructor is used without a corresponding production error code. We already have this for `invariant`, but not for regular errors, i.e. `throw new Error(msg)`. There's also nothing that enforces the use of `invariant` besides convention. There are some packages where we don't care to minify errors. These are packages that run in environments where bundle size is not a concern, like react-pg. I added an override in the ESLint config to ignore these.
1 parent 2bcea20 commit 7d40b8a

22 files changed

+232
-6
lines changed

.eslintrc.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,41 @@ module.exports = {
127127
},
128128

129129
overrides: [
130+
{
131+
// By default, anything error message that appears the packages directory
132+
// must have a corresponding error code. The exceptions are defined
133+
// in the next override entry.
134+
files: ['packages/**/*.js'],
135+
rules: {
136+
'react-internal/prod-error-codes': ERROR,
137+
},
138+
},
139+
{
140+
// These are files where it's OK to have unminified error messages. These
141+
// are environments where bundle size isn't a concern, like tests
142+
// or Node.
143+
files: [
144+
'packages/react-dom/src/test-utils/**/*.js',
145+
'packages/react-devtools-shared/**/*.js',
146+
'packages/react-noop-renderer/**/*.js',
147+
'packages/react-pg/**/*.js',
148+
'packages/react-fs/**/*.js',
149+
'packages/react-refresh/**/*.js',
150+
'packages/react-server-dom-webpack/**/*.js',
151+
'packages/react-test-renderer/**/*.js',
152+
'packages/react-debug-tools/**/*.js',
153+
'packages/react-devtools-extensions/**/*.js',
154+
'packages/react-devtools-scheduling-profiler/**/*.js',
155+
'packages/react-native-renderer/**/*.js',
156+
'packages/eslint-plugin-react-hooks/**/*.js',
157+
'packages/jest-react/**/*.js',
158+
'packages/**/__tests__/*.js',
159+
'packages/**/npm/*.js',
160+
],
161+
rules: {
162+
'react-internal/prod-error-codes': OFF,
163+
},
164+
},
130165
{
131166
// We apply these settings to files that we ship through npm.
132167
// They must be ES5.

packages/react-art/src/ReactARTHostConfig.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -433,25 +433,25 @@ export function clearContainer(container) {
433433
}
434434

435435
export function getInstanceFromNode(node) {
436-
throw new Error('Not yet implemented.');
436+
throw new Error('Not implemented.');
437437
}
438438

439439
export function isOpaqueHydratingObject(value: mixed): boolean {
440-
throw new Error('Not yet implemented');
440+
throw new Error('Not implemented.');
441441
}
442442

443443
export function makeOpaqueHydratingObject(
444444
attemptToReadValue: () => void,
445445
): OpaqueIDType {
446-
throw new Error('Not yet implemented.');
446+
throw new Error('Not implemented.');
447447
}
448448

449449
export function makeClientId(): OpaqueIDType {
450-
throw new Error('Not yet implemented');
450+
throw new Error('Not implemented.');
451451
}
452452

453453
export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
454-
throw new Error('Not yet implemented');
454+
throw new Error('Not implemented.');
455455
}
456456

457457
export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {

packages/react-cache/src/ReactCacheOld.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const ReactCurrentDispatcher =
4949
function readContext(Context) {
5050
const dispatcher = ReactCurrentDispatcher.current;
5151
if (dispatcher === null) {
52+
// This wasn't being minified but we're going to retire this package anyway.
53+
// eslint-disable-next-line react-internal/prod-error-codes
5254
throw new Error(
5355
'react-cache: read and preload may only be called from within a ' +
5456
"component's render. They are not supported in event handlers or " +

packages/react-client/src/ReactFlightClient.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ export function resolveError(
397397
message: string,
398398
stack: string,
399399
): void {
400+
// eslint-disable-next-line react-internal/prod-error-codes
400401
const error = new Error(message);
401402
error.stack = stack;
402403
const chunks = response._chunks;

packages/react-client/src/ReactFlightClientHostConfigNoStream.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,22 @@ export type StringDecoder = void;
1212
export const supportsBinaryStreams = false;
1313

1414
export function createStringDecoder(): void {
15+
// eslint-disable-next-line react-internal/prod-error-codes
1516
throw new Error('Should never be called');
1617
}
1718

1819
export function readPartialStringChunk(
1920
decoder: StringDecoder,
2021
buffer: Uint8Array,
2122
): string {
23+
// eslint-disable-next-line react-internal/prod-error-codes
2224
throw new Error('Should never be called');
2325
}
2426

2527
export function readFinalStringChunk(
2628
decoder: StringDecoder,
2729
buffer: Uint8Array,
2830
): string {
31+
// eslint-disable-next-line react-internal/prod-error-codes
2932
throw new Error('Should never be called');
3033
}

packages/react-fetch/src/ReactFetchBrowser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ function preloadRecord(url: string, options: mixed): Record {
129129
if (options.method || options.body || options.signal) {
130130
// TODO: wire up our own cancellation mechanism.
131131
// TODO: figure out what to do with POST.
132+
// eslint-disable-next-line react-internal/prod-error-codes
132133
throw Error('Unsupported option');
133134
}
134135
}

packages/react-fetch/src/ReactFetchNode.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ function preloadRecord(url: string, options: mixed): Record<FetchResponse> {
177177
if (options.method || options.body || options.signal) {
178178
// TODO: wire up our own cancellation mechanism.
179179
// TODO: figure out what to do with POST.
180+
// eslint-disable-next-line react-internal/prod-error-codes
180181
throw Error('Unsupported option');
181182
}
182183
}

packages/react-reconciler/src/ReactFiberBeginWork.new.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,7 @@ function updateClassComponent(
10501050
case true: {
10511051
workInProgress.flags |= DidCapture;
10521052
workInProgress.flags |= ShouldCapture;
1053+
// eslint-disable-next-line react-internal/prod-error-codes
10531054
const error = new Error('Simulated error coming from DevTools');
10541055
const lane = pickArbitraryLane(renderLanes);
10551056
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
@@ -3317,6 +3318,7 @@ function remountFiber(
33173318
if (__DEV__) {
33183319
const returnFiber = oldWorkInProgress.return;
33193320
if (returnFiber === null) {
3321+
// eslint-disable-next-line react-internal/prod-error-codes
33203322
throw new Error('Cannot swap the root fiber.');
33213323
}
33223324

@@ -3337,11 +3339,13 @@ function remountFiber(
33373339
} else {
33383340
let prevSibling = returnFiber.child;
33393341
if (prevSibling === null) {
3342+
// eslint-disable-next-line react-internal/prod-error-codes
33403343
throw new Error('Expected parent to have a child.');
33413344
}
33423345
while (prevSibling.sibling !== oldWorkInProgress) {
33433346
prevSibling = prevSibling.sibling;
33443347
if (prevSibling === null) {
3348+
// eslint-disable-next-line react-internal/prod-error-codes
33453349
throw new Error('Expected to find the previous sibling.');
33463350
}
33473351
}

packages/react-reconciler/src/ReactFiberBeginWork.old.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,7 @@ function updateClassComponent(
10501050
case true: {
10511051
workInProgress.flags |= DidCapture;
10521052
workInProgress.flags |= ShouldCapture;
1053+
// eslint-disable-next-line react-internal/prod-error-codes
10531054
const error = new Error('Simulated error coming from DevTools');
10541055
const lane = pickArbitraryLane(renderLanes);
10551056
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
@@ -3317,6 +3318,7 @@ function remountFiber(
33173318
if (__DEV__) {
33183319
const returnFiber = oldWorkInProgress.return;
33193320
if (returnFiber === null) {
3321+
// eslint-disable-next-line react-internal/prod-error-codes
33203322
throw new Error('Cannot swap the root fiber.');
33213323
}
33223324

@@ -3337,11 +3339,13 @@ function remountFiber(
33373339
} else {
33383340
let prevSibling = returnFiber.child;
33393341
if (prevSibling === null) {
3342+
// eslint-disable-next-line react-internal/prod-error-codes
33403343
throw new Error('Expected parent to have a child.');
33413344
}
33423345
while (prevSibling.sibling !== oldWorkInProgress) {
33433346
prevSibling = prevSibling.sibling;
33443347
if (prevSibling === null) {
3348+
// eslint-disable-next-line react-internal/prod-error-codes
33453349
throw new Error('Expected to find the previous sibling.');
33463350
}
33473351
}

packages/react-reconciler/src/ReactFiberHooks.new.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,7 @@ function pushEffect(tag, create, destroy, deps) {
15321532
let stackContainsErrorMessage: boolean | null = null;
15331533

15341534
function getCallerStackFrame(): string {
1535+
// eslint-disable-next-line react-internal/prod-error-codes
15351536
const stackFrames = new Error('Error message').stack.split('\n');
15361537

15371538
// Some browsers (e.g. Chrome) include the error message in the stack

0 commit comments

Comments
 (0)