@mui/internal-code-infra 0.0.4-canary.40 → 0.0.4-canary.42
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.
- package/build/brokenLinksChecker/index.d.mts +1 -0
- package/build/eslint/mui/rules/no-presentation-role.d.mts +5 -0
- package/package.json +4 -4
- package/src/brokenLinksChecker/crawlWorker.mjs +24 -12
- package/src/brokenLinksChecker/index.mjs +3 -1
- package/src/brokenLinksChecker/index.test.ts +11 -31
- package/src/eslint/mui/config.mjs +1 -0
- package/src/eslint/mui/index.mjs +2 -0
- package/src/eslint/mui/rules/no-presentation-role.mjs +60 -0
- package/src/eslint/mui/rules/no-presentation-role.test.mjs +33 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-code-infra",
|
|
3
|
-
"version": "0.0.4-canary.
|
|
3
|
+
"version": "0.0.4-canary.42",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "Infra scripts and configs to be used across MUI repos.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -139,9 +139,9 @@
|
|
|
139
139
|
"unified-lint-rule": "^3.0.1",
|
|
140
140
|
"unist-util-visit": "^5.1.0",
|
|
141
141
|
"yargs": "^18.0.0",
|
|
142
|
-
"@mui/internal-babel-plugin-
|
|
142
|
+
"@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.27",
|
|
143
143
|
"@mui/internal-babel-plugin-display-name": "1.0.4-canary.19",
|
|
144
|
-
"@mui/internal-babel-plugin-
|
|
144
|
+
"@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.36"
|
|
145
145
|
},
|
|
146
146
|
"peerDependencies": {
|
|
147
147
|
"@next/eslint-plugin-next": "*",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"publishConfig": {
|
|
192
192
|
"access": "public"
|
|
193
193
|
},
|
|
194
|
-
"gitSha": "
|
|
194
|
+
"gitSha": "f2b7a9f6b2db57a03f2e16a5bb351f02a55c3e17",
|
|
195
195
|
"scripts": {
|
|
196
196
|
"build": "tsgo -p tsconfig.build.json",
|
|
197
197
|
"typescript": "tsgo -noEmit",
|
|
@@ -160,21 +160,23 @@ if (pageData.status < 200 || pageData.status >= 400) {
|
|
|
160
160
|
contentType: type,
|
|
161
161
|
}));
|
|
162
162
|
|
|
163
|
-
// HTML validation.
|
|
164
|
-
//
|
|
165
|
-
//
|
|
163
|
+
// HTML validation. Every entry whose path matches contributes to the
|
|
164
|
+
// page's config: each is registered as a synthetic preset and the page's
|
|
165
|
+
// root config `extends` them in order. html-validate's own resolution then
|
|
166
|
+
// merges them, so callers can layer path-specific overrides on top of a
|
|
167
|
+
// baseline entry without re-stating the baseline rules.
|
|
166
168
|
/** @type {{ pageUrl: string, results: import('html-validate').Result[] } | null} */
|
|
167
169
|
let htmlValidateResults = null;
|
|
168
170
|
if (type === 'text/html' && options.htmlValidate.length > 0) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
171
|
+
const matchedEntries = options.htmlValidate.filter((entry) =>
|
|
172
|
+
matchesAnyPattern(pageUrl, entry.path),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (matchedEntries.length > 0) {
|
|
176
|
+
const overridePresets = Object.fromEntries(
|
|
177
|
+
matchedEntries.map((entry, index) => [`mui:override-${index}`, entry.config]),
|
|
178
|
+
);
|
|
176
179
|
|
|
177
|
-
if (matchedEntry) {
|
|
178
180
|
const muiHtmlValidateResolver = staticResolver({
|
|
179
181
|
configs: {
|
|
180
182
|
'mui:recommended': {
|
|
@@ -184,13 +186,23 @@ if (pageData.status < 200 || pageData.status >= 400) {
|
|
|
184
186
|
'require-sri': 'off',
|
|
185
187
|
},
|
|
186
188
|
},
|
|
189
|
+
...overridePresets,
|
|
187
190
|
},
|
|
188
191
|
});
|
|
189
192
|
|
|
190
193
|
const htmlValidator = new HtmlValidate(
|
|
191
|
-
new StaticConfigLoader([muiHtmlValidateResolver],
|
|
194
|
+
new StaticConfigLoader([muiHtmlValidateResolver], {
|
|
195
|
+
extends: Object.keys(overridePresets),
|
|
196
|
+
}),
|
|
192
197
|
);
|
|
193
198
|
|
|
199
|
+
if (options.verbose) {
|
|
200
|
+
const resolved = await htmlValidator.getConfigFor(pageUrl);
|
|
201
|
+
console.warn(
|
|
202
|
+
`[html-validate config] ${pageUrl}\n${JSON.stringify(resolved.getConfigData(), null, 2)}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
194
206
|
const report = await htmlValidator.validateString(rawContent, pageUrl);
|
|
195
207
|
htmlValidateResults = { pageUrl, results: report.results };
|
|
196
208
|
}
|
|
@@ -351,7 +351,8 @@ function shouldIgnoreLink(link, ignores) {
|
|
|
351
351
|
* @property {number} [concurrency] - Number of concurrent page fetches (defaults to 4)
|
|
352
352
|
* @property {string[]} [seedUrls] - Starting URLs for the crawl (defaults to ['/'])
|
|
353
353
|
* @property {IgnoreRule[]} [ignores] - Rules to ignore broken links. Each rule can have path, href, contentType, and/or has properties. All specified properties must match (AND logic). Within a property, multiple values use OR logic.
|
|
354
|
-
* @property {HtmlValidateOption} [htmlValidate] - Enable HTML validation on crawled pages. `false` (default): disabled. `true`: validate with recommended rules. Object: use as html-validate config — `extends` defaults to `['mui:recommended']` when omitted, so most callers only need to set `rules`. Array: per-path config overrides —
|
|
354
|
+
* @property {HtmlValidateOption} [htmlValidate] - Enable HTML validation on crawled pages. `false` (default): disabled. `true`: validate with recommended rules. Object: use as html-validate config — `extends` defaults to `['mui:recommended']` when omitted, so most callers only need to set `rules`. Array: per-path config overrides — every entry whose `path` matches the page URL contributes to the merged config (later entries win on conflicting rule keys); an entry without `path` matches every page (use as a baseline and layer more specific overrides on top). If no entry matches, the page is not validated.
|
|
355
|
+
* @property {boolean} [verbose] - Log extra diagnostics during crawling (e.g. resolved html-validate config per page). Defaults to `false`.
|
|
355
356
|
*/
|
|
356
357
|
|
|
357
358
|
/**
|
|
@@ -449,6 +450,7 @@ function resolveOptions(rawOptions) {
|
|
|
449
450
|
seedUrls: rawOptions.seedUrls ?? ['/'],
|
|
450
451
|
ignores: normalizedIgnores,
|
|
451
452
|
htmlValidate: resolveHtmlValidateConfig(rawOptions.htmlValidate),
|
|
453
|
+
verbose: rawOptions.verbose ?? false,
|
|
452
454
|
};
|
|
453
455
|
}
|
|
454
456
|
|
|
@@ -62,19 +62,14 @@ describe('Broken Links Checker', () => {
|
|
|
62
62
|
// Test href-only rule (matches from any page) - note: matches the actual href value
|
|
63
63
|
{ href: 'broken-relative.html' },
|
|
64
64
|
],
|
|
65
|
-
// Exercise the array form
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// matches `.html`.
|
|
65
|
+
// Exercise the array form with union semantics: every matching entry
|
|
66
|
+
// contributes to the page's config. The baseline entry (no `path`)
|
|
67
|
+
// turns off `no-raw-characters` everywhere; the path-specific entry
|
|
68
|
+
// turns off `no-dup-id` only on /invalid-html.html. Both rules are
|
|
69
|
+
// silenced on that page because the configs are merged, not replaced.
|
|
71
70
|
htmlValidate: [
|
|
72
71
|
{ config: { rules: { 'no-raw-characters': 'off' } } },
|
|
73
|
-
{
|
|
74
|
-
path: '/invalid-html.html',
|
|
75
|
-
config: { rules: { 'no-dup-id': 'off', 'no-raw-characters': 'off' } },
|
|
76
|
-
},
|
|
77
|
-
{ path: /\.html$/, config: { rules: { 'no-raw-characters': 'off' } } },
|
|
72
|
+
{ path: '/invalid-html.html', config: { rules: { 'no-dup-id': 'off' } } },
|
|
78
73
|
],
|
|
79
74
|
});
|
|
80
75
|
|
|
@@ -281,31 +276,16 @@ describe('Broken Links Checker', () => {
|
|
|
281
276
|
expect(result.pages.get('/example.md')?.contentType).toBe('text/markdown');
|
|
282
277
|
expect(result.pages.get('/')?.contentType).toBe('text/html');
|
|
283
278
|
|
|
284
|
-
// Test htmlValidate: invalid-html.html has
|
|
279
|
+
// Test htmlValidate union semantics: invalid-html.html has both a duplicate
|
|
280
|
+
// ID (no-dup-id) and a raw `&` (no-raw-characters). The path-specific
|
|
281
|
+
// entry silences no-dup-id; the baseline entry silences no-raw-characters.
|
|
282
|
+
// Under union semantics both apply, so the page reports zero issues.
|
|
285
283
|
const htmlValidateIssues = result.issues.filter(
|
|
286
284
|
(issue): issue is HtmlValidateIssue => issue.type === 'html-validate',
|
|
287
285
|
);
|
|
288
286
|
const invalidHtmlIssues = htmlValidateIssues.filter(
|
|
289
287
|
(issue) => issue.pageUrl === '/invalid-html.html',
|
|
290
288
|
);
|
|
291
|
-
expect(invalidHtmlIssues
|
|
292
|
-
expect(invalidHtmlIssues).toEqual(
|
|
293
|
-
expect.arrayContaining([
|
|
294
|
-
expect.objectContaining({
|
|
295
|
-
type: 'html-validate',
|
|
296
|
-
pageUrl: '/invalid-html.html',
|
|
297
|
-
ruleId: 'no-dup-id',
|
|
298
|
-
}),
|
|
299
|
-
]),
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
// Test htmlValidate override: no-raw-characters is off, so raw & should NOT be reported
|
|
303
|
-
expect(invalidHtmlIssues).not.toEqual(
|
|
304
|
-
expect.arrayContaining([
|
|
305
|
-
expect.objectContaining({
|
|
306
|
-
ruleId: 'no-raw-characters',
|
|
307
|
-
}),
|
|
308
|
-
]),
|
|
309
|
-
);
|
|
289
|
+
expect(invalidHtmlIssues).toEqual([]);
|
|
310
290
|
}, 30000);
|
|
311
291
|
});
|
|
@@ -421,6 +421,7 @@ export function createCoreConfig(options = {}) {
|
|
|
421
421
|
'mui/consistent-production-guard': 'error',
|
|
422
422
|
'mui/add-undef-to-optional': 'off',
|
|
423
423
|
'mui/flatten-parentheses': 'warn',
|
|
424
|
+
'mui/no-presentation-role': 'off',
|
|
424
425
|
|
|
425
426
|
'react-hooks/exhaustive-deps': [
|
|
426
427
|
'error',
|
package/src/eslint/mui/index.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import rulesOfUseThemeVariants from './rules/rules-of-use-theme-variants.mjs';
|
|
|
12
12
|
import straightQuotes from './rules/straight-quotes.mjs';
|
|
13
13
|
import addUndefToOptional from './rules/add-undef-to-optional.mjs';
|
|
14
14
|
import flattenParentheses from './rules/flatten-parentheses.mjs';
|
|
15
|
+
import noPresentationRole from './rules/no-presentation-role.mjs';
|
|
15
16
|
|
|
16
17
|
/** @type {import('eslint').ESLint.Plugin} */
|
|
17
18
|
const muiPlugin = {
|
|
@@ -35,6 +36,7 @@ const muiPlugin = {
|
|
|
35
36
|
// Some discrepancies between TypeScript and ESLint types - casting to any
|
|
36
37
|
'add-undef-to-optional': /** @type {any} */ (addUndefToOptional),
|
|
37
38
|
'flatten-parentheses': /** @type {any} */ (flattenParentheses),
|
|
39
|
+
'no-presentation-role': noPresentationRole,
|
|
38
40
|
},
|
|
39
41
|
};
|
|
40
42
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
3
|
+
*/
|
|
4
|
+
const rule = {
|
|
5
|
+
meta: {
|
|
6
|
+
docs: {
|
|
7
|
+
description:
|
|
8
|
+
'Disallow role="presentation" in favor of role="none". Both are equivalent, but role="none" is clearer and shorter.',
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
noPresentation:
|
|
12
|
+
'Use role="none" instead of role="presentation". They are equivalent, but role="none" is preferred.',
|
|
13
|
+
},
|
|
14
|
+
fixable: 'code',
|
|
15
|
+
type: 'suggestion',
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
/** @param {import('estree-jsx').JSXAttribute} node */
|
|
21
|
+
JSXAttribute(node) {
|
|
22
|
+
if (node.name.type !== 'JSXIdentifier' || node.name.name !== 'role') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { value } = node;
|
|
27
|
+
|
|
28
|
+
// role="presentation"
|
|
29
|
+
if (value !== null && value.type === 'Literal' && value.value === 'presentation') {
|
|
30
|
+
context.report({
|
|
31
|
+
node,
|
|
32
|
+
messageId: 'noPresentation',
|
|
33
|
+
fix(fixer) {
|
|
34
|
+
return fixer.replaceText(value, '"none"');
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// role={'presentation'}
|
|
41
|
+
if (
|
|
42
|
+
value !== null &&
|
|
43
|
+
value.type === 'JSXExpressionContainer' &&
|
|
44
|
+
value.expression.type === 'Literal' &&
|
|
45
|
+
value.expression.value === 'presentation'
|
|
46
|
+
) {
|
|
47
|
+
context.report({
|
|
48
|
+
node,
|
|
49
|
+
messageId: 'noPresentation',
|
|
50
|
+
fix(fixer) {
|
|
51
|
+
return fixer.replaceText(value, '"none"');
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default rule;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import eslint from 'eslint';
|
|
2
|
+
import parser from '@typescript-eslint/parser';
|
|
3
|
+
import rule from './no-presentation-role.mjs';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new eslint.RuleTester({
|
|
6
|
+
languageOptions: {
|
|
7
|
+
parser,
|
|
8
|
+
parserOptions: {
|
|
9
|
+
ecmaFeatures: { jsx: true },
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
ruleTester.run('no-presentation-role', rule, {
|
|
15
|
+
valid: ['<div role="none" />', '<div role="button" />', '<div />', '<div role={presentation} />'],
|
|
16
|
+
invalid: [
|
|
17
|
+
{
|
|
18
|
+
code: '<div role="presentation" />',
|
|
19
|
+
errors: [{ messageId: 'noPresentation' }],
|
|
20
|
+
output: '<div role="none" />',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
code: "<div role={'presentation'} />",
|
|
24
|
+
errors: [{ messageId: 'noPresentation' }],
|
|
25
|
+
output: '<div role="none" />',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: '<table role="presentation"><tr><td /></tr></table>',
|
|
29
|
+
errors: [{ messageId: 'noPresentation' }],
|
|
30
|
+
output: '<table role="none"><tr><td /></tr></table>',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|