Skip to content

Commit 1bafc3c

Browse files
authored
[lexical] Chore: Change $getTextNodeOffset invariant to warn in prod (error in __DEV__) (#7397)
1 parent 8fc8487 commit 1bafc3c

File tree

9 files changed

+329
-152
lines changed

9 files changed

+329
-152
lines changed

packages/lexical-devtools/tsconfig.json

+4
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,17 @@
161161
"@lexical/yjs": ["../lexical-yjs/src/index.ts"],
162162
"shared/canUseDOM": ["../shared/src/canUseDOM.ts"],
163163
"shared/caretFromPoint": ["../shared/src/caretFromPoint.ts"],
164+
"shared/devInvariant": ["../shared/src/devInvariant.ts"],
164165
"shared/environment": ["../shared/src/environment.ts"],
165166
"shared/formatDevErrorMessage": [
166167
"../shared/src/formatDevErrorMessage.ts"
167168
],
168169
"shared/formatProdErrorMessage": [
169170
"../shared/src/formatProdErrorMessage.ts"
170171
],
172+
"shared/formatProdWarningMessage": [
173+
"../shared/src/formatProdWarningMessage.ts"
174+
],
171175
"shared/invariant": ["../shared/src/invariant.ts"],
172176
"shared/normalizeClassNames": ["../shared/src/normalizeClassNames.ts"],
173177
"shared/react-test-utils": ["../shared/src/react-test-utils.ts"],

packages/lexical/src/caret/LexicalCaret.ts

+14-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
import type {LexicalNode, NodeKey} from '../LexicalNode';
99

10+
import devInvariant from 'shared/devInvariant';
1011
import invariant from 'shared/invariant';
1112

1213
import {$getRoot, $isRootOrShadowRoot} from '../LexicalUtils';
@@ -890,7 +891,7 @@ export function $getTextPointCaret(
890891

891892
/**
892893
* Get a normalized offset into a TextNode given a numeric offset or a
893-
* direction for which end of the string to use. Throws if the offset
894+
* direction for which end of the string to use. Throws in dev if the offset
894895
* is not in the bounds of the text content size.
895896
*
896897
* @param origin a TextNode
@@ -902,14 +903,19 @@ export function $getTextNodeOffset(
902903
offset: number | CaretDirection,
903904
): number {
904905
const size = origin.getTextContentSize();
905-
const numericOffset =
906+
let numericOffset =
906907
offset === 'next' ? size : offset === 'previous' ? 0 : offset;
907-
invariant(
908-
numericOffset >= 0 && numericOffset <= size,
909-
'$getTextNodeOffset: invalid offset %s for size %s',
910-
String(offset),
911-
String(size),
912-
);
908+
if (numericOffset < 0 || numericOffset > size) {
909+
devInvariant(
910+
false,
911+
'$getTextNodeOffset: invalid offset %s for size %s at key %s',
912+
String(offset),
913+
String(size),
914+
origin.getKey(),
915+
);
916+
// Clamp invalid offsets in prod
917+
numericOffset = numericOffset < 0 ? 0 : size;
918+
}
913919
return numericOffset;
914920
}
915921

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
export default function devInvariant(
10+
cond?: boolean,
11+
message?: string,
12+
...args: string[]
13+
): void {
14+
if (cond) {
15+
return;
16+
}
17+
18+
throw new Error(
19+
args.reduce((msg, arg) => msg.replace('%s', String(arg)), message || ''),
20+
);
21+
}

packages/shared/src/devInvariant.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
/**
10+
* If `!cond` throw in `__DEV__` like an invariant and warn in prod
11+
*/
12+
export default function devInvariant(
13+
cond?: boolean,
14+
message?: string,
15+
...args: string[]
16+
): void {
17+
if (cond) {
18+
return;
19+
}
20+
21+
throw new Error(
22+
'Internal Lexical error: devInvariant() is meant to be replaced at compile ' +
23+
'time. There is no runtime version. Error: ' +
24+
message,
25+
);
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
export default function prodWarningMessage(
10+
code: string,
11+
...args: string[]
12+
): void {
13+
const url = new URL('https://lexical.dev/docs/error');
14+
const params = new URLSearchParams();
15+
params.append('code', code);
16+
for (const arg of args) {
17+
params.append('v', arg);
18+
}
19+
url.search = params.toString();
20+
21+
console.warn(
22+
`Minified Lexical warning #${code}; visit ${url.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`,
23+
);
24+
}

scripts/error-codes/__tests__/unit/transform-error-messages.test.js

+119-41
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ function fmt(strings: TemplateStringsArray, ...keys: unknown[]) {
5353
.replace(/.use strict.;\n/g, '')
5454
.replace(/var _[^;]+;\n/g, '')
5555
.replace(/function _interopRequireDefault\([^)]*\) {[^;]+?;[\s\n]*}\n/g, '')
56-
.replace(/_format(Dev|Prod)ErrorMessage\d+/g, 'format$1ErrorMessage')
56+
.replace(/_format(Dev|Prod)(Error|Warning)Message\d+/g, 'format$1$2Message')
5757
.replace(
58-
/\(0,\s*format(Dev|Prod)ErrorMessage\.default\)/g,
59-
'format$1ErrorMessage',
58+
/\(0,\s*format(Dev|Prod)(Error|Warning)Message\.default\)/g,
59+
'format$1$2Message',
6060
)
6161
.trim();
6262
return prettier.format(before, {
@@ -98,77 +98,155 @@ async function expectTransform(opts) {
9898
}
9999

100100
describe('transform-error-messages', () => {
101-
describe('{extractCodes: true, noMinify: false}', () => {
102-
const opts = {extractCodes: true, noMinify: false};
103-
it('inserts known and extracts unknown message codes', async () => {
104-
await expectTransform({
105-
codeBefore: `
101+
describe('invariant', () => {
102+
describe('{extractCodes: true, noMinify: false}', () => {
103+
const opts = {extractCodes: true, noMinify: false};
104+
it('inserts known and extracts unknown message codes', async () => {
105+
await expectTransform({
106+
codeBefore: `
106107
invariant(condition, ${JSON.stringify(NEW_MSG)});
107108
invariant(condition, ${JSON.stringify(KNOWN_MSG)}, adj, noun);
108109
`,
109-
codeExpect: `
110+
codeExpect: `
110111
if (!condition) {
111112
formatProdErrorMessage(1);
112113
}
113114
if (!condition) {
114115
formatProdErrorMessage(0, adj, noun);
115116
}`,
116-
messageMapBefore: KNOWN_MSG_MAP,
117-
messageMapExpect: NEW_MSG_MAP,
118-
opts,
117+
messageMapBefore: KNOWN_MSG_MAP,
118+
messageMapExpect: NEW_MSG_MAP,
119+
opts,
120+
});
119121
});
120122
});
121-
});
122-
describe('{extractCodes: true, noMinify: true}', () => {
123-
const opts = {extractCodes: true, noMinify: true};
124-
it('inserts known and extracts unknown message codes', async () => {
125-
await expectTransform({
126-
codeBefore: `
123+
describe('{extractCodes: true, noMinify: true}', () => {
124+
const opts = {extractCodes: true, noMinify: true};
125+
it('inserts known and extracts unknown message codes', async () => {
126+
await expectTransform({
127+
codeBefore: `
127128
invariant(condition, ${JSON.stringify(NEW_MSG)});
128129
invariant(condition, ${JSON.stringify(KNOWN_MSG)}, adj, noun);
129130
`,
130-
codeExpect: `
131+
codeExpect: `
131132
if (!condition) {
132133
formatDevErrorMessage(\`A new invariant\`);
133134
}
134135
if (!condition) {
135136
formatDevErrorMessage(\`A \${adj} message that contains \${noun}\`);
136137
}`,
137-
messageMapBefore: KNOWN_MSG_MAP,
138-
messageMapExpect: NEW_MSG_MAP,
139-
opts,
138+
messageMapBefore: KNOWN_MSG_MAP,
139+
messageMapExpect: NEW_MSG_MAP,
140+
opts,
141+
});
140142
});
141143
});
142-
});
143-
describe('{extractCodes: false, noMinify: false}', () => {
144-
const opts = {extractCodes: false, noMinify: false};
145-
it('inserts known message', async () => {
146-
await expectTransform({
147-
codeBefore: `invariant(condition, ${JSON.stringify(
148-
KNOWN_MSG,
149-
)}, adj, noun)`,
150-
codeExpect: `
144+
describe('{extractCodes: false, noMinify: false}', () => {
145+
const opts = {extractCodes: false, noMinify: false};
146+
it('inserts known message', async () => {
147+
await expectTransform({
148+
codeBefore: `invariant(condition, ${JSON.stringify(
149+
KNOWN_MSG,
150+
)}, adj, noun)`,
151+
codeExpect: `
151152
if (!condition) {
152153
formatProdErrorMessage(0, adj, noun);
153154
}
154155
`,
155-
messageMapBefore: KNOWN_MSG_MAP,
156-
messageMapExpect: KNOWN_MSG_MAP,
157-
opts,
156+
messageMapBefore: KNOWN_MSG_MAP,
157+
messageMapExpect: KNOWN_MSG_MAP,
158+
opts,
159+
});
160+
});
161+
it('inserts warning comment for unknown messages', async () => {
162+
await expectTransform({
163+
codeBefore: `invariant(condition, ${JSON.stringify(NEW_MSG)})`,
164+
codeExpect: `
165+
/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/
166+
if (!condition) {
167+
formatDevErrorMessage(\`A new invariant\`);
168+
}
169+
`,
170+
messageMapBefore: KNOWN_MSG_MAP,
171+
messageMapExpect: KNOWN_MSG_MAP,
172+
opts,
173+
});
158174
});
159175
});
160-
it('inserts warning comment for unknown messages', async () => {
161-
await expectTransform({
162-
codeBefore: `invariant(condition, ${JSON.stringify(NEW_MSG)})`,
163-
codeExpect: `
176+
});
177+
describe('devInvariant', () => {
178+
describe('{extractCodes: true, noMinify: false}', () => {
179+
const opts = {extractCodes: true, noMinify: false};
180+
it('inserts known and extracts unknown message codes', async () => {
181+
await expectTransform({
182+
codeBefore: `
183+
devInvariant(condition, ${JSON.stringify(NEW_MSG)});
184+
devInvariant(condition, ${JSON.stringify(KNOWN_MSG)}, adj, noun);
185+
`,
186+
codeExpect: `
187+
if (!condition) {
188+
formatProdWarningMessage(1);
189+
}
190+
if (!condition) {
191+
formatProdWarningMessage(0, adj, noun);
192+
}`,
193+
messageMapBefore: KNOWN_MSG_MAP,
194+
messageMapExpect: NEW_MSG_MAP,
195+
opts,
196+
});
197+
});
198+
});
199+
describe('{extractCodes: true, noMinify: true}', () => {
200+
const opts = {extractCodes: true, noMinify: true};
201+
it('inserts known and extracts unknown message codes', async () => {
202+
await expectTransform({
203+
codeBefore: `
204+
devInvariant(condition, ${JSON.stringify(NEW_MSG)});
205+
devInvariant(condition, ${JSON.stringify(KNOWN_MSG)}, adj, noun);
206+
`,
207+
codeExpect: `
208+
if (!condition) {
209+
formatDevErrorMessage(\`A new invariant\`);
210+
}
211+
if (!condition) {
212+
formatDevErrorMessage(\`A \${adj} message that contains \${noun}\`);
213+
}`,
214+
messageMapBefore: KNOWN_MSG_MAP,
215+
messageMapExpect: NEW_MSG_MAP,
216+
opts,
217+
});
218+
});
219+
});
220+
describe('{extractCodes: false, noMinify: false}', () => {
221+
const opts = {extractCodes: false, noMinify: false};
222+
it('inserts known message', async () => {
223+
await expectTransform({
224+
codeBefore: `devInvariant(condition, ${JSON.stringify(
225+
KNOWN_MSG,
226+
)}, adj, noun)`,
227+
codeExpect: `
228+
if (!condition) {
229+
formatProdWarningMessage(0, adj, noun);
230+
}
231+
`,
232+
messageMapBefore: KNOWN_MSG_MAP,
233+
messageMapExpect: KNOWN_MSG_MAP,
234+
opts,
235+
});
236+
});
237+
it('inserts warning comment for unknown messages', async () => {
238+
await expectTransform({
239+
codeBefore: `devInvariant(condition, ${JSON.stringify(NEW_MSG)})`,
240+
codeExpect: `
164241
/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/
165242
if (!condition) {
166243
formatDevErrorMessage(\`A new invariant\`);
167244
}
168245
`,
169-
messageMapBefore: KNOWN_MSG_MAP,
170-
messageMapExpect: KNOWN_MSG_MAP,
171-
opts,
246+
messageMapBefore: KNOWN_MSG_MAP,
247+
messageMapExpect: KNOWN_MSG_MAP,
248+
opts,
249+
});
172250
});
173251
});
174252
});

0 commit comments

Comments
 (0)