@mui/internal-code-infra 0.0.4-canary.3 → 0.0.4-canary.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +19 -8
  2. package/build/babel-config.d.mts +11 -3
  3. package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
  4. package/build/brokenLinksChecker/index.d.mts +35 -2
  5. package/build/changelog/types.d.ts +1 -1
  6. package/build/cli/cmdArgosPush.d.mts +2 -2
  7. package/build/cli/cmdBuild.d.mts +2 -2
  8. package/build/cli/cmdCopyFiles.d.mts +2 -2
  9. package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
  10. package/build/cli/cmdGenerateChangelog.d.mts +2 -2
  11. package/build/cli/cmdGithubAuth.d.mts +2 -2
  12. package/build/cli/cmdListWorkspaces.d.mts +4 -2
  13. package/build/cli/cmdNetlifyIgnore.d.mts +2 -2
  14. package/build/cli/cmdPublish.d.mts +4 -2
  15. package/build/cli/cmdPublishCanary.d.mts +3 -2
  16. package/build/cli/cmdPublishNewPackage.d.mts +4 -2
  17. package/build/cli/cmdSetVersionOverrides.d.mts +2 -2
  18. package/build/cli/cmdVale.d.mts +46 -0
  19. package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
  20. package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
  21. package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
  22. package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +2 -2
  23. package/build/markdownlint/duplicate-h1.d.mts +1 -1
  24. package/build/markdownlint/git-diff.d.mts +1 -1
  25. package/build/markdownlint/index.d.mts +1 -1
  26. package/build/markdownlint/straight-quotes.d.mts +1 -1
  27. package/build/markdownlint/table-alignment.d.mts +1 -1
  28. package/build/markdownlint/terminal-language.d.mts +1 -1
  29. package/build/utils/build.d.mts +3 -3
  30. package/build/utils/github.d.mts +1 -1
  31. package/build/utils/pnpm.d.mts +68 -2
  32. package/build/utils/testUtils.d.mts +7 -0
  33. package/package.json +38 -31
  34. package/src/babel-config.mjs +9 -3
  35. package/src/brokenLinksChecker/__fixtures__/static-site/index.html +1 -0
  36. package/src/brokenLinksChecker/__fixtures__/static-site/invalid-html.html +15 -0
  37. package/src/brokenLinksChecker/crawlWorker.mjs +173 -0
  38. package/src/brokenLinksChecker/index.mjs +177 -164
  39. package/src/brokenLinksChecker/index.test.ts +55 -13
  40. package/src/build-env.d.ts +13 -0
  41. package/src/changelog/fetchChangelogs.mjs +6 -2
  42. package/src/changelog/types.ts +1 -1
  43. package/src/cli/cmdListWorkspaces.mjs +9 -2
  44. package/src/cli/cmdNetlifyIgnore.mjs +4 -88
  45. package/src/cli/cmdPublish.mjs +51 -14
  46. package/src/cli/cmdPublishCanary.mjs +139 -107
  47. package/src/cli/cmdPublishNewPackage.mjs +27 -6
  48. package/src/cli/cmdVale.mjs +513 -0
  49. package/src/cli/cmdVale.test.mjs +644 -0
  50. package/src/cli/index.mjs +2 -0
  51. package/src/eslint/baseConfig.mjs +2 -1
  52. package/src/eslint/docsConfig.mjs +2 -1
  53. package/src/eslint/jsonConfig.mjs +2 -1
  54. package/src/eslint/mui/config.mjs +11 -1
  55. package/src/eslint/testConfig.mjs +2 -1
  56. package/src/estree-typescript.d.ts +1 -1
  57. package/src/untyped-plugins.d.ts +11 -11
  58. package/src/utils/build.test.mjs +546 -575
  59. package/src/utils/pnpm.mjs +192 -3
  60. package/src/utils/pnpm.test.mjs +580 -0
  61. package/src/utils/testUtils.mjs +18 -0
  62. package/src/utils/typescript.test.mjs +249 -272
  63. package/vale/.vale.ini +1 -0
  64. package/vale/styles/MUI/CorrectReferenceAllCases.yml +43 -0
  65. package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
  66. package/vale/styles/MUI/GoogleLatin.yml +11 -0
  67. package/vale/styles/MUI/MuiBrandName.yml +22 -0
  68. package/vale/styles/MUI/NoBritish.yml +112 -0
  69. package/vale/styles/MUI/NoCompanyName.yml +17 -0
@@ -0,0 +1,644 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { describe, it, expect } from 'vitest';
4
+ import { makeTempDir } from '../utils/testUtils.mjs';
5
+ import { getReplacementText, applyFixes } from './cmdVale.mjs';
6
+
7
+ describe('getReplacementText', () => {
8
+ describe('with explicit Action.Name="replace"', () => {
9
+ it('returns the first param when Action has replace params', () => {
10
+ const alert = {
11
+ Action: { Name: 'replace', Params: ['for example'] },
12
+ Message: "Use 'for example' instead of 'eg'",
13
+ };
14
+ expect(getReplacementText(alert)).toBe('for example');
15
+ });
16
+
17
+ it('returns the first param even with multiple params', () => {
18
+ const alert = {
19
+ Action: { Name: 'replace', Params: ['first', 'second'] },
20
+ Message: "Use 'first' instead of 'bad'",
21
+ };
22
+ expect(getReplacementText(alert)).toBe('first');
23
+ });
24
+
25
+ it('falls back to message parsing when Action.Name is replace but Params is null', () => {
26
+ const alert = {
27
+ Action: { Name: 'replace', Params: null },
28
+ Message: "Use 'color' instead of 'colour'",
29
+ };
30
+ expect(getReplacementText(alert)).toBe('color');
31
+ });
32
+
33
+ it('falls back to message parsing when Action.Name is replace but Params is empty', () => {
34
+ const alert = {
35
+ Action: { Name: 'replace', Params: [] },
36
+ Message: "Use 'color' instead of 'colour'",
37
+ };
38
+ expect(getReplacementText(alert)).toBe('color');
39
+ });
40
+ });
41
+
42
+ describe('with no explicit Action (message parsing)', () => {
43
+ it('parses simple "Use X instead of Y" messages', () => {
44
+ const alert = {
45
+ Action: { Name: '', Params: null },
46
+ Message: "Use 'npm' instead of 'NPM'",
47
+ };
48
+ expect(getReplacementText(alert)).toBe('npm');
49
+ });
50
+
51
+ it('parses messages with extra words before first quote', () => {
52
+ const alert = {
53
+ Action: { Name: '', Params: null },
54
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
55
+ };
56
+ expect(getReplacementText(alert)).toBe('color');
57
+ });
58
+
59
+ it('parses messages with extra words between instead-of and second quote', () => {
60
+ const alert = {
61
+ Action: { Name: '', Params: null },
62
+ Message: "Use the US spelling 'gray' instead of the British 'grey'",
63
+ };
64
+ expect(getReplacementText(alert)).toBe('gray');
65
+ });
66
+
67
+ it('parses non-breaking space messages', () => {
68
+ const alert = {
69
+ Action: { Name: '', Params: null },
70
+ Message:
71
+ "Use a non-breaking space (option+space on Mac, Alt+0160 on Windows or AltGr+Space on Linux, instead of space) for brand name ('Material\u00a0UI' instead of 'Material UI')",
72
+ };
73
+ expect(getReplacementText(alert)).toBe('Material\u00a0UI');
74
+ });
75
+
76
+ it('parses "Use X instead of Y" with e.g. and periods', () => {
77
+ const alert = {
78
+ Action: { Name: '', Params: null },
79
+ Message: "Use 'e.g.' instead of 'eg'",
80
+ };
81
+ expect(getReplacementText(alert)).toBe('e.g.');
82
+ });
83
+
84
+ it('parses TypeScript-style replacements', () => {
85
+ const alert = {
86
+ Action: { Name: '', Params: null },
87
+ Message: "Use 'TypeScript ' instead of 'typescript '",
88
+ };
89
+ expect(getReplacementText(alert)).toBe('TypeScript ');
90
+ });
91
+
92
+ it('returns null for messages without the expected pattern', () => {
93
+ const alert = {
94
+ Action: { Name: '', Params: null },
95
+ Message:
96
+ "We avoid referencing the company name 'MUI Dashboard'. Instead you can reference a product or the team.",
97
+ };
98
+ expect(getReplacementText(alert)).toBeNull();
99
+ });
100
+
101
+ it('returns null for completely unstructured messages', () => {
102
+ const alert = {
103
+ Action: { Name: '', Params: null },
104
+ Message: 'This sentence is too long.',
105
+ };
106
+ expect(getReplacementText(alert)).toBeNull();
107
+ });
108
+ });
109
+ });
110
+
111
+ describe('applyFixes', () => {
112
+ /**
113
+ * Helper to create a temp file and return its path.
114
+ * @param {string} name
115
+ * @param {string} content
116
+ * @returns {Promise<string>}
117
+ */
118
+ async function createFile(name, content) {
119
+ const tmpDir = await makeTempDir();
120
+ const filePath = path.join(tmpDir, name);
121
+ await fs.writeFile(filePath, content, 'utf-8');
122
+ return filePath;
123
+ }
124
+
125
+ /**
126
+ * @param {string} filePath
127
+ * @returns {Promise<string>}
128
+ */
129
+ async function readFile(filePath) {
130
+ return fs.readFile(filePath, 'utf-8');
131
+ }
132
+
133
+ it('applies a single fix on a single line', async () => {
134
+ const filePath = await createFile('test.md', 'The colour is nice.\n');
135
+ const results = {
136
+ [filePath]: [
137
+ {
138
+ Action: { Name: '', Params: null },
139
+ Span: /** @type {[number, number]} */ ([5, 10]),
140
+ Check: 'MUI.NoBritish',
141
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
142
+ Severity: 'error',
143
+ Match: 'colour',
144
+ Line: 1,
145
+ },
146
+ ],
147
+ };
148
+
149
+ const { fixed, skipped } = await applyFixes(results, 'all');
150
+ expect(fixed).toBe(1);
151
+ expect(skipped).toBe(0);
152
+ expect(await readFile(filePath)).toBe('The color is nice.\n');
153
+ });
154
+
155
+ it('applies multiple fixes on the same line (right-to-left)', async () => {
156
+ const filePath = await createFile('test.md', 'The colour is grey today.\n');
157
+ const results = {
158
+ [filePath]: [
159
+ {
160
+ Action: { Name: '', Params: null },
161
+ Span: /** @type {[number, number]} */ ([5, 10]),
162
+ Check: 'MUI.NoBritish',
163
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
164
+ Severity: 'error',
165
+ Match: 'colour',
166
+ Line: 1,
167
+ },
168
+ {
169
+ Action: { Name: '', Params: null },
170
+ Span: /** @type {[number, number]} */ ([15, 18]),
171
+ Check: 'MUI.NoBritish',
172
+ Message: "Use the US spelling 'gray' instead of the British 'grey'",
173
+ Severity: 'error',
174
+ Match: 'grey',
175
+ Line: 1,
176
+ },
177
+ ],
178
+ };
179
+
180
+ const { fixed, skipped } = await applyFixes(results, 'all');
181
+ expect(fixed).toBe(2);
182
+ expect(skipped).toBe(0);
183
+ expect(await readFile(filePath)).toBe('The color is gray today.\n');
184
+ });
185
+
186
+ it('applies fixes across multiple lines', async () => {
187
+ const filePath = await createFile('test.md', 'The colour is nice.\nThe grey sky.\n');
188
+ const results = {
189
+ [filePath]: [
190
+ {
191
+ Action: { Name: '', Params: null },
192
+ Span: /** @type {[number, number]} */ ([5, 10]),
193
+ Check: 'MUI.NoBritish',
194
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
195
+ Severity: 'error',
196
+ Match: 'colour',
197
+ Line: 1,
198
+ },
199
+ {
200
+ Action: { Name: '', Params: null },
201
+ Span: /** @type {[number, number]} */ ([5, 8]),
202
+ Check: 'MUI.NoBritish',
203
+ Message: "Use the US spelling 'gray' instead of the British 'grey'",
204
+ Severity: 'error',
205
+ Match: 'grey',
206
+ Line: 2,
207
+ },
208
+ ],
209
+ };
210
+
211
+ const { fixed, skipped } = await applyFixes(results, 'all');
212
+ expect(fixed).toBe(2);
213
+ expect(skipped).toBe(0);
214
+ expect(await readFile(filePath)).toBe('The color is nice.\nThe gray sky.\n');
215
+ });
216
+
217
+ it('applies fixes with explicit Action replace params', async () => {
218
+ const filePath = await createFile('test.md', 'Use eg in a sentence.\n');
219
+ const results = {
220
+ [filePath]: [
221
+ {
222
+ Action: { Name: 'replace', Params: ['for example'] },
223
+ Span: /** @type {[number, number]} */ ([5, 6]),
224
+ Check: 'MUI.GoogleLatin',
225
+ Message: "Use 'for example' instead of 'eg'",
226
+ Severity: 'error',
227
+ Match: 'eg',
228
+ Line: 1,
229
+ },
230
+ ],
231
+ };
232
+
233
+ const { fixed, skipped } = await applyFixes(results, 'all');
234
+ expect(fixed).toBe(1);
235
+ expect(skipped).toBe(0);
236
+ expect(await readFile(filePath)).toBe('Use for example in a sentence.\n');
237
+ });
238
+
239
+ it('deduplicates alerts at the same location', async () => {
240
+ const filePath = await createFile('test.md', 'Use eg here.\n');
241
+ const results = {
242
+ [filePath]: [
243
+ {
244
+ Action: { Name: 'replace', Params: ['for example'] },
245
+ Span: /** @type {[number, number]} */ ([5, 6]),
246
+ Check: 'MUI.GoogleLatin',
247
+ Message: "Use 'for example' instead of 'eg'",
248
+ Severity: 'error',
249
+ Match: 'eg',
250
+ Line: 1,
251
+ },
252
+ {
253
+ Action: { Name: '', Params: null },
254
+ Span: /** @type {[number, number]} */ ([5, 6]),
255
+ Check: 'MUI.CorrectRererenceCased',
256
+ Message: "Use 'e.g.' instead of 'eg'",
257
+ Severity: 'error',
258
+ Match: 'eg',
259
+ Line: 1,
260
+ },
261
+ ],
262
+ };
263
+
264
+ const { fixed } = await applyFixes(results, 'all');
265
+ // Only one fix applied despite two alerts at the same position
266
+ expect(fixed).toBe(1);
267
+ expect(await readFile(filePath)).toBe('Use for example here.\n');
268
+ });
269
+
270
+ describe('fixLevel filtering', () => {
271
+ it('fixes only errors when fixLevel is "error"', async () => {
272
+ const filePath = await createFile('test.md', 'The MUI Dashboard has colour.\n');
273
+ const results = {
274
+ [filePath]: [
275
+ {
276
+ Action: { Name: '', Params: null },
277
+ Span: /** @type {[number, number]} */ ([5, 17]),
278
+ Check: 'MUI.NoCompanyName',
279
+ Message:
280
+ "We avoid referencing the company name 'MUI Dashboard'. Instead you can reference a product or the team.",
281
+ Severity: 'warning',
282
+ Match: 'MUI Dashboard',
283
+ Line: 1,
284
+ },
285
+ {
286
+ Action: { Name: '', Params: null },
287
+ Span: /** @type {[number, number]} */ ([23, 28]),
288
+ Check: 'MUI.NoBritish',
289
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
290
+ Severity: 'error',
291
+ Match: 'colour',
292
+ Line: 1,
293
+ },
294
+ ],
295
+ };
296
+
297
+ const { fixed, skipped } = await applyFixes(results, 'error');
298
+ expect(fixed).toBe(1);
299
+ expect(skipped).toBe(1);
300
+ expect(await readFile(filePath)).toBe('The MUI Dashboard has color.\n');
301
+ });
302
+
303
+ it('fixes both errors and warnings when fixLevel is "all"', async () => {
304
+ const filePath = await createFile('test.md', 'NPM is colour.\n');
305
+ const results = {
306
+ [filePath]: [
307
+ {
308
+ Action: { Name: '', Params: null },
309
+ Span: /** @type {[number, number]} */ ([1, 3]),
310
+ Check: 'MUI.CorrectReferenceAllCases',
311
+ Message: "Use 'npm' instead of 'NPM'",
312
+ Severity: 'warning',
313
+ Match: 'NPM',
314
+ Line: 1,
315
+ },
316
+ {
317
+ Action: { Name: '', Params: null },
318
+ Span: /** @type {[number, number]} */ ([8, 13]),
319
+ Check: 'MUI.NoBritish',
320
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
321
+ Severity: 'error',
322
+ Match: 'colour',
323
+ Line: 1,
324
+ },
325
+ ],
326
+ };
327
+
328
+ const { fixed, skipped } = await applyFixes(results, 'all');
329
+ expect(fixed).toBe(2);
330
+ expect(skipped).toBe(0);
331
+ expect(await readFile(filePath)).toBe('npm is color.\n');
332
+ });
333
+ });
334
+
335
+ it('skips alerts with no determinable replacement', async () => {
336
+ const filePath = await createFile('test.md', 'The MUI Dashboard is great.\n');
337
+ const results = {
338
+ [filePath]: [
339
+ {
340
+ Action: { Name: '', Params: null },
341
+ Span: /** @type {[number, number]} */ ([5, 17]),
342
+ Check: 'MUI.NoCompanyName',
343
+ Message:
344
+ "We avoid referencing the company name 'MUI Dashboard'. Instead you can reference a product or the team.",
345
+ Severity: 'warning',
346
+ Match: 'MUI Dashboard',
347
+ Line: 1,
348
+ },
349
+ ],
350
+ };
351
+
352
+ const { fixed, skipped } = await applyFixes(results, 'all');
353
+ expect(fixed).toBe(0);
354
+ expect(skipped).toBe(1);
355
+ // File should be unchanged
356
+ expect(await readFile(filePath)).toBe('The MUI Dashboard is great.\n');
357
+ });
358
+
359
+ it('handles multiple files', async () => {
360
+ const file1 = await createFile('a.md', 'The colour.\n');
361
+ const file2 = await createFile('b.md', 'The grey sky.\n');
362
+ const results = {
363
+ [file1]: [
364
+ {
365
+ Action: { Name: '', Params: null },
366
+ Span: /** @type {[number, number]} */ ([5, 10]),
367
+ Check: 'MUI.NoBritish',
368
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
369
+ Severity: 'error',
370
+ Match: 'colour',
371
+ Line: 1,
372
+ },
373
+ ],
374
+ [file2]: [
375
+ {
376
+ Action: { Name: '', Params: null },
377
+ Span: /** @type {[number, number]} */ ([5, 8]),
378
+ Check: 'MUI.NoBritish',
379
+ Message: "Use the US spelling 'gray' instead of the British 'grey'",
380
+ Severity: 'error',
381
+ Match: 'grey',
382
+ Line: 1,
383
+ },
384
+ ],
385
+ };
386
+
387
+ const { fixed, skipped } = await applyFixes(results, 'all');
388
+ expect(fixed).toBe(2);
389
+ expect(skipped).toBe(0);
390
+ expect(await readFile(file1)).toBe('The color.\n');
391
+ expect(await readFile(file2)).toBe('The gray sky.\n');
392
+ });
393
+
394
+ it('applies multiple fixes on the same line and across different lines', async () => {
395
+ const filePath = await createFile(
396
+ 'test.md',
397
+ 'The colour is grey.\nI like NPM and eg stuff.\nThis line is fine.\nMore colour here.\n',
398
+ );
399
+ const results = {
400
+ [filePath]: [
401
+ {
402
+ Action: { Name: '', Params: null },
403
+ Span: /** @type {[number, number]} */ ([5, 10]),
404
+ Check: 'MUI.NoBritish',
405
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
406
+ Severity: 'error',
407
+ Match: 'colour',
408
+ Line: 1,
409
+ },
410
+ {
411
+ Action: { Name: '', Params: null },
412
+ Span: /** @type {[number, number]} */ ([15, 18]),
413
+ Check: 'MUI.NoBritish',
414
+ Message: "Use the US spelling 'gray' instead of the British 'grey'",
415
+ Severity: 'error',
416
+ Match: 'grey',
417
+ Line: 1,
418
+ },
419
+ {
420
+ Action: { Name: '', Params: null },
421
+ Span: /** @type {[number, number]} */ ([8, 10]),
422
+ Check: 'MUI.CorrectReferenceAllCases',
423
+ Message: "Use 'npm' instead of 'NPM'",
424
+ Severity: 'error',
425
+ Match: 'NPM',
426
+ Line: 2,
427
+ },
428
+ {
429
+ Action: { Name: 'replace', Params: ['for example'] },
430
+ Span: /** @type {[number, number]} */ ([16, 17]),
431
+ Check: 'MUI.GoogleLatin',
432
+ Message: "Use 'for example' instead of 'eg'",
433
+ Severity: 'error',
434
+ Match: 'eg',
435
+ Line: 2,
436
+ },
437
+ {
438
+ Action: { Name: '', Params: null },
439
+ Span: /** @type {[number, number]} */ ([6, 11]),
440
+ Check: 'MUI.NoBritish',
441
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
442
+ Severity: 'error',
443
+ Match: 'colour',
444
+ Line: 4,
445
+ },
446
+ ],
447
+ };
448
+
449
+ const { fixed, skipped } = await applyFixes(results, 'all');
450
+ expect(fixed).toBe(5);
451
+ expect(skipped).toBe(0);
452
+ expect(await readFile(filePath)).toBe(
453
+ 'The color is gray.\nI like npm and for example stuff.\nThis line is fine.\nMore color here.\n',
454
+ );
455
+ });
456
+
457
+ it('handles empty results', async () => {
458
+ const { fixed, skipped } = await applyFixes({}, 'all');
459
+ expect(fixed).toBe(0);
460
+ expect(skipped).toBe(0);
461
+ });
462
+
463
+ it('handles a file with no fixable alerts (all unfixable)', async () => {
464
+ const filePath = await createFile('test.md', 'Something wrong.\n');
465
+ const results = {
466
+ [filePath]: [
467
+ {
468
+ Action: { Name: '', Params: null },
469
+ Span: /** @type {[number, number]} */ ([1, 9]),
470
+ Check: 'SomeRule',
471
+ Message: 'This sentence is problematic.',
472
+ Severity: 'error',
473
+ Match: 'Something',
474
+ Line: 1,
475
+ },
476
+ ],
477
+ };
478
+
479
+ const { fixed, skipped } = await applyFixes(results, 'all');
480
+ expect(fixed).toBe(0);
481
+ expect(skipped).toBe(1);
482
+ expect(await readFile(filePath)).toBe('Something wrong.\n');
483
+ });
484
+
485
+ it('handles replacement that changes string length (shorter to longer)', async () => {
486
+ const filePath = await createFile('test.md', 'Use eg here and eg there.\n');
487
+ const results = {
488
+ [filePath]: [
489
+ {
490
+ Action: { Name: 'replace', Params: ['for example'] },
491
+ Span: /** @type {[number, number]} */ ([5, 6]),
492
+ Check: 'MUI.GoogleLatin',
493
+ Message: "Use 'for example' instead of 'eg'",
494
+ Severity: 'error',
495
+ Match: 'eg',
496
+ Line: 1,
497
+ },
498
+ {
499
+ Action: { Name: 'replace', Params: ['for example'] },
500
+ Span: /** @type {[number, number]} */ ([17, 18]),
501
+ Check: 'MUI.GoogleLatin',
502
+ Message: "Use 'for example' instead of 'eg'",
503
+ Severity: 'error',
504
+ Match: 'eg',
505
+ Line: 1,
506
+ },
507
+ ],
508
+ };
509
+
510
+ const { fixed } = await applyFixes(results, 'all');
511
+ expect(fixed).toBe(2);
512
+ expect(await readFile(filePath)).toBe('Use for example here and for example there.\n');
513
+ });
514
+
515
+ it('handles replacement that changes string length (longer to shorter)', async () => {
516
+ const filePath = await createFile('test.md', 'The favourite colour is great.\n');
517
+ const results = {
518
+ [filePath]: [
519
+ {
520
+ Action: { Name: '', Params: null },
521
+ Span: /** @type {[number, number]} */ ([5, 13]),
522
+ Check: 'MUI.NoBritish',
523
+ Message: "Use the US spelling 'favorite' instead of the British 'favourite'",
524
+ Severity: 'error',
525
+ Match: 'favourite',
526
+ Line: 1,
527
+ },
528
+ {
529
+ Action: { Name: '', Params: null },
530
+ Span: /** @type {[number, number]} */ ([15, 20]),
531
+ Check: 'MUI.NoBritish',
532
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
533
+ Severity: 'error',
534
+ Match: 'colour',
535
+ Line: 1,
536
+ },
537
+ ],
538
+ };
539
+
540
+ const { fixed } = await applyFixes(results, 'all');
541
+ expect(fixed).toBe(2);
542
+ expect(await readFile(filePath)).toBe('The favorite color is great.\n');
543
+ });
544
+
545
+ it('preserves lines that have no alerts', async () => {
546
+ const filePath = await createFile(
547
+ 'test.md',
548
+ 'Line one is fine.\nLine two has colour.\nLine three is fine.\n',
549
+ );
550
+ const results = {
551
+ [filePath]: [
552
+ {
553
+ Action: { Name: '', Params: null },
554
+ Span: /** @type {[number, number]} */ ([14, 19]),
555
+ Check: 'MUI.NoBritish',
556
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
557
+ Severity: 'error',
558
+ Match: 'colour',
559
+ Line: 2,
560
+ },
561
+ ],
562
+ };
563
+
564
+ const { fixed } = await applyFixes(results, 'all');
565
+ expect(fixed).toBe(1);
566
+ expect(await readFile(filePath)).toBe(
567
+ 'Line one is fine.\nLine two has color.\nLine three is fine.\n',
568
+ );
569
+ });
570
+
571
+ it('handles fix at the start of a line', async () => {
572
+ const filePath = await createFile('test.md', 'NPM is great.\n');
573
+ const results = {
574
+ [filePath]: [
575
+ {
576
+ Action: { Name: '', Params: null },
577
+ Span: /** @type {[number, number]} */ ([1, 3]),
578
+ Check: 'MUI.CorrectReferenceAllCases',
579
+ Message: "Use 'npm' instead of 'NPM'",
580
+ Severity: 'error',
581
+ Match: 'NPM',
582
+ Line: 1,
583
+ },
584
+ ],
585
+ };
586
+
587
+ const { fixed } = await applyFixes(results, 'all');
588
+ expect(fixed).toBe(1);
589
+ expect(await readFile(filePath)).toBe('npm is great.\n');
590
+ });
591
+
592
+ it('handles fix at the end of a line', async () => {
593
+ const filePath = await createFile('test.md', 'I like colour\n');
594
+ const results = {
595
+ [filePath]: [
596
+ {
597
+ Action: { Name: '', Params: null },
598
+ Span: /** @type {[number, number]} */ ([8, 13]),
599
+ Check: 'MUI.NoBritish',
600
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
601
+ Severity: 'error',
602
+ Match: 'colour',
603
+ Line: 1,
604
+ },
605
+ ],
606
+ };
607
+
608
+ const { fixed } = await applyFixes(results, 'all');
609
+ expect(fixed).toBe(1);
610
+ expect(await readFile(filePath)).toBe('I like color\n');
611
+ });
612
+
613
+ it('handles mixed fixable and unfixable alerts in the same file', async () => {
614
+ const filePath = await createFile('test.md', 'MUI Dashboard colour.\n');
615
+ const results = {
616
+ [filePath]: [
617
+ {
618
+ Action: { Name: '', Params: null },
619
+ Span: /** @type {[number, number]} */ ([1, 13]),
620
+ Check: 'MUI.NoCompanyName',
621
+ Message:
622
+ "We avoid referencing the company name 'MUI Dashboard'. Instead you can reference a product or the team.",
623
+ Severity: 'warning',
624
+ Match: 'MUI Dashboard',
625
+ Line: 1,
626
+ },
627
+ {
628
+ Action: { Name: '', Params: null },
629
+ Span: /** @type {[number, number]} */ ([15, 20]),
630
+ Check: 'MUI.NoBritish',
631
+ Message: "Use the US spelling 'color' instead of the British 'colour'",
632
+ Severity: 'error',
633
+ Match: 'colour',
634
+ Line: 1,
635
+ },
636
+ ],
637
+ };
638
+
639
+ const { fixed, skipped } = await applyFixes(results, 'all');
640
+ expect(fixed).toBe(1);
641
+ expect(skipped).toBe(1);
642
+ expect(await readFile(filePath)).toBe('MUI Dashboard color.\n');
643
+ });
644
+ });
package/src/cli/index.mjs CHANGED
@@ -15,6 +15,7 @@ import cmdPublish from './cmdPublish.mjs';
15
15
  import cmdPublishCanary from './cmdPublishCanary.mjs';
16
16
  import cmdPublishNewPackage from './cmdPublishNewPackage.mjs';
17
17
  import cmdSetVersionOverrides from './cmdSetVersionOverrides.mjs';
18
+ import cmdVale from './cmdVale.mjs';
18
19
  import cmdValidateBuiltTypes from './cmdValidateBuiltTypes.mjs';
19
20
 
20
21
  const pkgJson = createRequire(import.meta.url)('../../package.json');
@@ -47,6 +48,7 @@ await yargs(hideBin(process.argv))
47
48
  .command(cmdPublishCanary)
48
49
  .command(cmdPublishNewPackage)
49
50
  .command(cmdSetVersionOverrides)
51
+ .command(cmdVale)
50
52
  .command(cmdValidateBuiltTypes)
51
53
  .fail((msg, err, yargsInstance) => {
52
54
  if (msg) {
@@ -1,6 +1,7 @@
1
1
  import { includeIgnoreFile, fixupConfigRules } from '@eslint/compat';
2
2
  import eslintJs from '@eslint/js';
3
- import { defineConfig } from 'eslint/config';
3
+ // TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
4
+ import { defineConfig } from '@eslint/config-helpers';
4
5
  import prettier from 'eslint-config-prettier/flat';
5
6
  import compatPlugin from 'eslint-plugin-compat';
6
7
  import importPlugin from 'eslint-plugin-import';
@@ -1,5 +1,6 @@
1
1
  import nextjs from '@next/eslint-plugin-next';
2
- import { defineConfig } from 'eslint/config';
2
+ // TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
3
+ import { defineConfig } from '@eslint/config-helpers';
3
4
 
4
5
  /**
5
6
  * @returns {import('eslint').Linter.Config[]}
@@ -1,4 +1,5 @@
1
- import { defineConfig } from 'eslint/config';
1
+ // TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
2
+ import { defineConfig } from '@eslint/config-helpers';
2
3
  import json from '@eslint/json';
3
4
 
4
5
  /**