@mui/internal-code-infra 0.0.4-canary.4 → 0.0.4-canary.41
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/README.md +19 -8
- package/build/babel-config.d.mts +11 -3
- package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
- package/build/brokenLinksChecker/index.d.mts +45 -2
- package/build/changelog/types.d.ts +1 -1
- package/build/cli/cmdArgosPush.d.mts +2 -2
- package/build/cli/cmdBuild.d.mts +2 -2
- package/build/cli/cmdCopyFiles.d.mts +2 -2
- package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
- package/build/cli/cmdGenerateChangelog.d.mts +2 -2
- package/build/cli/cmdGithubAuth.d.mts +2 -2
- package/build/cli/cmdListWorkspaces.d.mts +4 -2
- package/build/cli/cmdNetlifyIgnore.d.mts +2 -2
- package/build/cli/cmdPublish.d.mts +4 -2
- package/build/cli/cmdPublishCanary.d.mts +3 -2
- package/build/cli/cmdPublishNewPackage.d.mts +4 -2
- package/build/cli/cmdSetVersionOverrides.d.mts +2 -2
- package/build/cli/cmdVale.d.mts +46 -0
- package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
- package/build/eslint/baseConfig.d.mts +3 -1
- package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
- package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
- package/build/eslint/mui/rules/no-guarded-throw.d.mts +31 -0
- package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +2 -2
- package/build/eslint/mui/rules/nodeEnvUtils.d.mts +18 -0
- package/build/markdownlint/duplicate-h1.d.mts +1 -1
- package/build/markdownlint/git-diff.d.mts +1 -1
- package/build/markdownlint/index.d.mts +1 -1
- package/build/markdownlint/straight-quotes.d.mts +1 -1
- package/build/markdownlint/table-alignment.d.mts +1 -1
- package/build/markdownlint/terminal-language.d.mts +1 -1
- package/build/remark/config.d.mts +43 -0
- package/build/remark/createLintTester.d.mts +10 -0
- package/build/remark/firstBlockHeading.d.mts +4 -0
- package/build/remark/gitDiff.d.mts +2 -0
- package/build/remark/noSpaceInLinks.d.mts +2 -0
- package/build/remark/straightQuotes.d.mts +2 -0
- package/build/remark/tableAlignment.d.mts +2 -0
- package/build/remark/terminalLanguage.d.mts +2 -0
- package/build/utils/build.d.mts +3 -3
- package/build/utils/github.d.mts +1 -1
- package/build/utils/pnpm.d.mts +68 -2
- package/build/utils/testUtils.d.mts +7 -0
- package/package.json +59 -32
- package/src/babel-config.mjs +9 -3
- package/src/brokenLinksChecker/__fixtures__/static-site/index.html +1 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/invalid-html.html +15 -0
- package/src/brokenLinksChecker/crawlWorker.mjs +212 -0
- package/src/brokenLinksChecker/index.mjs +215 -164
- package/src/brokenLinksChecker/index.test.ts +43 -13
- package/src/changelog/categorizeCommits.test.ts +5 -5
- package/src/changelog/fetchChangelogs.mjs +6 -2
- package/src/changelog/parseCommitLabels.test.ts +5 -5
- package/src/changelog/renderChangelog.mjs +1 -1
- package/src/changelog/types.ts +1 -1
- package/src/cli/cmdListWorkspaces.mjs +9 -2
- package/src/cli/cmdNetlifyIgnore.mjs +4 -88
- package/src/cli/cmdPublish.mjs +51 -14
- package/src/cli/cmdPublishCanary.mjs +139 -107
- package/src/cli/cmdPublishNewPackage.mjs +27 -6
- package/src/cli/cmdVale.mjs +513 -0
- package/src/cli/cmdVale.test.mjs +644 -0
- package/src/cli/index.mjs +2 -0
- package/src/eslint/baseConfig.mjs +45 -20
- package/src/eslint/docsConfig.mjs +2 -1
- package/src/eslint/jsonConfig.mjs +2 -1
- package/src/eslint/mui/config.mjs +20 -1
- package/src/eslint/mui/index.mjs +2 -0
- package/src/eslint/mui/rules/no-guarded-throw.mjs +115 -0
- package/src/eslint/mui/rules/no-guarded-throw.test.mjs +206 -0
- package/src/eslint/mui/rules/nodeEnvUtils.mjs +52 -0
- package/src/eslint/mui/rules/require-dev-wrapper.mjs +25 -40
- package/src/eslint/testConfig.mjs +2 -1
- package/src/estree-typescript.d.ts +1 -1
- package/src/remark/config.mjs +157 -0
- package/src/remark/createLintTester.mjs +19 -0
- package/src/remark/firstBlockHeading.mjs +87 -0
- package/src/remark/firstBlockHeading.test.mjs +107 -0
- package/src/remark/gitDiff.mjs +43 -0
- package/src/remark/gitDiff.test.mjs +45 -0
- package/src/remark/noSpaceInLinks.mjs +42 -0
- package/src/remark/noSpaceInLinks.test.mjs +22 -0
- package/src/remark/straightQuotes.mjs +31 -0
- package/src/remark/straightQuotes.test.mjs +25 -0
- package/src/remark/tableAlignment.mjs +23 -0
- package/src/remark/tableAlignment.test.mjs +28 -0
- package/src/remark/terminalLanguage.mjs +19 -0
- package/src/remark/terminalLanguage.test.mjs +17 -0
- package/src/untyped-plugins.d.ts +11 -11
- package/src/utils/build.test.mjs +546 -575
- package/src/utils/pnpm.mjs +192 -3
- package/src/utils/pnpm.test.mjs +580 -0
- package/src/utils/testUtils.mjs +18 -0
- package/src/utils/typescript.test.mjs +249 -272
- package/vale/.vale.ini +1 -0
- package/vale/styles/MUI/CorrectReferenceAllCases.yml +43 -0
- package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
- package/vale/styles/MUI/GoogleLatin.yml +11 -0
- package/vale/styles/MUI/MuiBrandName.yml +22 -0
- package/vale/styles/MUI/NoBritish.yml +112 -0
- package/vale/styles/MUI/NoCompanyName.yml +17 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
3
|
+
import { unified } from 'unified';
|
|
4
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
6
|
+
import remarkLint from 'remark-lint';
|
|
7
|
+
import remarkLintCodeBlockStyle from 'remark-lint-code-block-style';
|
|
8
|
+
import remarkLintFencedCodeFlag from 'remark-lint-fenced-code-flag';
|
|
9
|
+
import remarkLintHeadingIncrement from 'remark-lint-heading-increment';
|
|
10
|
+
import remarkLintHeadingStyle from 'remark-lint-heading-style';
|
|
11
|
+
import remarkLintNoDuplicateHeadings from 'remark-lint-no-duplicate-headings';
|
|
12
|
+
import remarkLintNoEmptyUrl from 'remark-lint-no-empty-url';
|
|
13
|
+
import remarkLintNoHeadingPunctuation from 'remark-lint-no-heading-punctuation';
|
|
14
|
+
import remarkLintNoMultipleToplevelHeadings from 'remark-lint-no-multiple-toplevel-headings';
|
|
15
|
+
import remarkLintNoUndefinedReferences from 'remark-lint-no-undefined-references';
|
|
16
|
+
import remarkLintNoUnusedDefinitions from 'remark-lint-no-unused-definitions';
|
|
17
|
+
import remarkLintTablePipes from 'remark-lint-table-pipes';
|
|
18
|
+
import muiFirstBlockHeading from './firstBlockHeading.mjs';
|
|
19
|
+
import muiGitDiff from './gitDiff.mjs';
|
|
20
|
+
import muiNoSpaceInLinks from './noSpaceInLinks.mjs';
|
|
21
|
+
import muiStraightQuotes from './straightQuotes.mjs';
|
|
22
|
+
import muiTableAlignment from './tableAlignment.mjs';
|
|
23
|
+
import muiTerminalLanguage from './terminalLanguage.mjs';
|
|
24
|
+
|
|
25
|
+
const GITHUB_ALERT_LABELS = ['!NOTE', '!TIP', '!WARNING', '!IMPORTANT', '!CAUTION'];
|
|
26
|
+
|
|
27
|
+
const RULES = {
|
|
28
|
+
'no-duplicate-headings': [remarkLintNoDuplicateHeadings, ['error']],
|
|
29
|
+
'no-multiple-toplevel-headings': [remarkLintNoMultipleToplevelHeadings, ['error']],
|
|
30
|
+
'no-undefined-references': [
|
|
31
|
+
remarkLintNoUndefinedReferences,
|
|
32
|
+
['error', { allow: GITHUB_ALERT_LABELS, allowShortcutLink: true }],
|
|
33
|
+
],
|
|
34
|
+
'no-unused-definitions': [remarkLintNoUnusedDefinitions, ['error']],
|
|
35
|
+
'heading-style': [remarkLintHeadingStyle, ['error', 'atx']],
|
|
36
|
+
'heading-increment': [remarkLintHeadingIncrement, ['error']],
|
|
37
|
+
'no-heading-punctuation': [remarkLintNoHeadingPunctuation, ['error', '.,;:!']],
|
|
38
|
+
'code-block-style': [remarkLintCodeBlockStyle, ['error', 'fenced']],
|
|
39
|
+
'fenced-code-flag': [remarkLintFencedCodeFlag, ['error']],
|
|
40
|
+
'no-empty-url': [remarkLintNoEmptyUrl, ['error']],
|
|
41
|
+
'table-pipes': [remarkLintTablePipes, ['error']],
|
|
42
|
+
'mui-first-block-heading': [muiFirstBlockHeading, ['error']],
|
|
43
|
+
'mui-git-diff': [muiGitDiff, ['error']],
|
|
44
|
+
'mui-no-space-in-links': [muiNoSpaceInLinks, ['error']],
|
|
45
|
+
'mui-straight-quotes': [muiStraightQuotes, ['error']],
|
|
46
|
+
'mui-table-alignment': [muiTableAlignment, ['error']],
|
|
47
|
+
'mui-terminal-language': [muiTerminalLanguage, ['error']],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string | undefined} filePath
|
|
52
|
+
*/
|
|
53
|
+
function relativePath(filePath) {
|
|
54
|
+
if (!filePath) {
|
|
55
|
+
return filePath;
|
|
56
|
+
}
|
|
57
|
+
if (path.isAbsolute(filePath)) {
|
|
58
|
+
return path.relative(process.cwd(), filePath);
|
|
59
|
+
}
|
|
60
|
+
return filePath;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wraps a remark-lint plugin so its transformer dispatches at runtime based on
|
|
65
|
+
* `file.path`. Each variant (base + per-override) runs through its own mini
|
|
66
|
+
* unified pipeline so severity, baked in by `unified-lint-rule` at attach time,
|
|
67
|
+
* is preserved correctly.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} name
|
|
70
|
+
* @param {import('unified').Plugin<any[], any, any>} plugin
|
|
71
|
+
* @param {any} baseSettings
|
|
72
|
+
* @param {Array<{ files: string | string[], settings: false | any }>} overrideEntries
|
|
73
|
+
*/
|
|
74
|
+
function withOverrides(name, plugin, baseSettings, overrideEntries) {
|
|
75
|
+
const baseProcessor = unified().use(plugin, baseSettings);
|
|
76
|
+
const variants = overrideEntries.map(({ files, settings }) => ({
|
|
77
|
+
files: Array.isArray(files) ? files : [files],
|
|
78
|
+
processor: settings === false ? null : unified().use(plugin, settings),
|
|
79
|
+
}));
|
|
80
|
+
function wrapper() {
|
|
81
|
+
/** @type {import('unified').Transformer} */
|
|
82
|
+
return async function transformer(tree, file) {
|
|
83
|
+
const candidate = relativePath(file.path);
|
|
84
|
+
const matched = candidate
|
|
85
|
+
? variants.find((variant) => variant.files.some((pattern) => minimatch(candidate, pattern)))
|
|
86
|
+
: undefined;
|
|
87
|
+
if (matched) {
|
|
88
|
+
if (matched.processor) {
|
|
89
|
+
await matched.processor.run(tree, file);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await baseProcessor.run(tree, file);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
Object.defineProperty(wrapper, 'name', { value: `mui-remark-overrides(${name})` });
|
|
97
|
+
return wrapper;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns a remark preset wiring the MUI-authored remark-lint plugins together
|
|
102
|
+
* with a curated set of community plugins. Drop this into `.remarkrc.mjs`:
|
|
103
|
+
*
|
|
104
|
+
* ```js
|
|
105
|
+
* import { createRemarkConfig } from '@mui/internal-code-infra/remark';
|
|
106
|
+
* export default createRemarkConfig();
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* Pass `overrides` to scope rule changes to a glob. Each entry's `rules` map
|
|
110
|
+
* is keyed by the rule name (the key used in `RULES`); `false` disables the
|
|
111
|
+
* rule for matching files, a settings tuple replaces its severity/options:
|
|
112
|
+
*
|
|
113
|
+
* ```js
|
|
114
|
+
* createRemarkConfig({
|
|
115
|
+
* overrides: [
|
|
116
|
+
* { files: 'docs/special/**', rules: { 'mui-no-space-in-links': false } },
|
|
117
|
+
* { files: '**\/CHANGELOG.md', rules: { 'heading-style': ['warn', 'atx'] } },
|
|
118
|
+
* ],
|
|
119
|
+
* });
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @param {Object} [options]
|
|
123
|
+
* @param {Array<{ files: string | string[], rules: Record<string, false | unknown[]> }>} [options.overrides]
|
|
124
|
+
*/
|
|
125
|
+
export function createRemarkConfig({ overrides = [] } = {}) {
|
|
126
|
+
for (const override of overrides) {
|
|
127
|
+
const unknown = Object.keys(override.rules).filter((ruleName) => !(ruleName in RULES));
|
|
128
|
+
if (unknown.length > 0) {
|
|
129
|
+
throw new Error(`Unknown remark-lint rule name(s): ${unknown.join(', ')}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const entries = Object.entries(RULES).map(([name, entry]) => {
|
|
134
|
+
const [plugin, baseSettings] = /** @type {[import('unified').Plugin<any[], any, any>, any]} */ (
|
|
135
|
+
entry
|
|
136
|
+
);
|
|
137
|
+
const overrideEntries = overrides
|
|
138
|
+
.filter((override) => name in override.rules)
|
|
139
|
+
.map((override) => ({ files: override.files, settings: override.rules[name] }));
|
|
140
|
+
if (overrideEntries.length === 0) {
|
|
141
|
+
return [plugin, baseSettings];
|
|
142
|
+
}
|
|
143
|
+
return [withOverrides(name, plugin, baseSettings, overrideEntries)];
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
settings: {
|
|
148
|
+
bullet: '-',
|
|
149
|
+
emphasis: '_',
|
|
150
|
+
fence: '`',
|
|
151
|
+
listItemIndent: 'one',
|
|
152
|
+
resourceLink: true,
|
|
153
|
+
rule: '-',
|
|
154
|
+
},
|
|
155
|
+
plugins: [[remarkFrontmatter, ['yaml', 'toml']], remarkGfm, remarkLint, ...entries],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { remark } from 'remark';
|
|
2
|
+
import remarkGfm from 'remark-gfm';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {unknown} plugin
|
|
6
|
+
* @param {unknown} [options]
|
|
7
|
+
* @returns {(input: string) => Array<{ reason: string, line: number, column: number }>}
|
|
8
|
+
*/
|
|
9
|
+
export function createLintTester(plugin, options) {
|
|
10
|
+
const entry = /** @type {any} */ (options === undefined ? plugin : [plugin, options]);
|
|
11
|
+
return function lint(input) {
|
|
12
|
+
const file = remark().use(remarkGfm).use(entry).processSync(input);
|
|
13
|
+
return file.messages.map((message) => ({
|
|
14
|
+
reason: message.reason,
|
|
15
|
+
line: message.line ?? 0,
|
|
16
|
+
column: message.column ?? 0,
|
|
17
|
+
}));
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { lintRule } from 'unified-lint-rule';
|
|
2
|
+
|
|
3
|
+
const FRONTMATTER_TYPES = new Set(['yaml', 'toml']);
|
|
4
|
+
const INVISIBLE_TAGS = new Set(['style', 'script']);
|
|
5
|
+
const DEFAULT_FRONT_MATTER_TITLE = /^\s*"?title"?\s*[:=]/m;
|
|
6
|
+
|
|
7
|
+
/** @param {import('mdast').RootContent} node */
|
|
8
|
+
const isSkippable = (node) => {
|
|
9
|
+
const type = /** @type {string} */ (node.type);
|
|
10
|
+
if (FRONTMATTER_TYPES.has(type)) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (type === 'html') {
|
|
14
|
+
const value = /** @type {{ value: string }} */ (/** @type {unknown} */ (node)).value.trim();
|
|
15
|
+
if (value.startsWith('<!--')) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const match = value.match(/^<\s*([a-zA-Z][a-zA-Z0-9-]*)/);
|
|
19
|
+
if (match && INVISIBLE_TAGS.has(match[1].toLowerCase())) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (type === 'mdxJsxFlowElement') {
|
|
24
|
+
const name = /** @type {{ name: string | null }} */ (/** @type {unknown} */ (node)).name;
|
|
25
|
+
if (name && INVISIBLE_TAGS.has(name.toLowerCase())) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (type === 'mdxjsEsm') {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (type === 'mdxFlowExpression') {
|
|
33
|
+
const value = /** @type {{ value: string }} */ (/** @type {unknown} */ (node)).value.trim();
|
|
34
|
+
if (
|
|
35
|
+
value === '' ||
|
|
36
|
+
(value.startsWith('/*') && value.endsWith('*/')) ||
|
|
37
|
+
value.startsWith('//')
|
|
38
|
+
) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {import('mdast').Root} tree
|
|
47
|
+
* @param {RegExp | false} pattern
|
|
48
|
+
*/
|
|
49
|
+
const hasFrontMatterTitle = (tree, pattern) => {
|
|
50
|
+
if (!pattern) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const frontMatter = tree.children.find((child) => FRONTMATTER_TYPES.has(child.type));
|
|
54
|
+
if (!frontMatter) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return pattern.test(
|
|
58
|
+
/** @type {{ value: string }} */ (/** @type {unknown} */ (frontMatter)).value,
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const remarkLintMuiFirstBlockHeading = lintRule(
|
|
63
|
+
{
|
|
64
|
+
origin: 'remark-lint:mui-first-block-heading',
|
|
65
|
+
url: 'https://github.com/mui/mui-public',
|
|
66
|
+
},
|
|
67
|
+
/** @type {import('unified-lint-rule').Rule<import('mdast').Root, { frontMatterTitle?: RegExp | false } | undefined>} */
|
|
68
|
+
(tree, file, options) => {
|
|
69
|
+
const frontMatterTitle =
|
|
70
|
+
options?.frontMatterTitle === undefined
|
|
71
|
+
? DEFAULT_FRONT_MATTER_TITLE
|
|
72
|
+
: options.frontMatterTitle;
|
|
73
|
+
if (hasFrontMatterTitle(tree, frontMatterTitle)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const firstBlock = tree.children.find((child) => !isSkippable(child));
|
|
77
|
+
if (!firstBlock) {
|
|
78
|
+
file.message('Documents must begin with a top-level heading.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (firstBlock.type !== 'heading' || firstBlock.depth !== 1) {
|
|
82
|
+
file.message('Documents must begin with a top-level heading.', firstBlock);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
export default remarkLintMuiFirstBlockHeading;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
3
|
+
import remarkMdx from 'remark-mdx';
|
|
4
|
+
import { remark } from 'remark';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
6
|
+
import plugin from './firstBlockHeading.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} input
|
|
10
|
+
* @param {Parameters<typeof plugin>[0]} [options]
|
|
11
|
+
*/
|
|
12
|
+
function lint(input, options) {
|
|
13
|
+
const file = remark()
|
|
14
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
15
|
+
.use(remarkGfm)
|
|
16
|
+
.use(plugin, options)
|
|
17
|
+
.processSync(input);
|
|
18
|
+
return file.messages.map((message) => ({
|
|
19
|
+
reason: message.reason,
|
|
20
|
+
line: message.line ?? 0,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} input
|
|
26
|
+
* @param {Parameters<typeof plugin>[0]} [options]
|
|
27
|
+
*/
|
|
28
|
+
function lintMdx(input, options) {
|
|
29
|
+
const file = remark()
|
|
30
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
31
|
+
.use(remarkGfm)
|
|
32
|
+
.use(remarkMdx)
|
|
33
|
+
.use(plugin, options)
|
|
34
|
+
.processSync(input);
|
|
35
|
+
return file.messages.map((message) => ({
|
|
36
|
+
reason: message.reason,
|
|
37
|
+
line: message.line ?? 0,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('remark-lint-mui-first-block-heading', () => {
|
|
42
|
+
it('accepts a document starting with an h1', () => {
|
|
43
|
+
expect(lint(`# Title\n\nSome content.\n`)).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('accepts an h1 after YAML frontmatter', () => {
|
|
47
|
+
expect(lint(`---\nfoo: bar\n---\n\n# Title\n`)).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('flags a paragraph before the h1', () => {
|
|
51
|
+
expect(lint(`Lead paragraph.\n\n# Title\n`)).toHaveLength(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('flags an h2 as the first block', () => {
|
|
55
|
+
expect(lint(`## Subtitle\n\nContent.\n`)).toHaveLength(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('flags an empty document', () => {
|
|
59
|
+
expect(lint(``)).toHaveLength(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('flags a document starting with HTML', () => {
|
|
63
|
+
expect(lint(`<div>Hello</div>\n\n# Title\n`)).toHaveLength(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('accepts an h1 after a <style> block', () => {
|
|
67
|
+
expect(lint(`<style>.x { color: red; }</style>\n\n# Title\n`)).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('accepts an h1 after a <script> block', () => {
|
|
71
|
+
expect(lint(`<script>var x = 1;</script>\n\n# Title\n`)).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts an h1 after an HTML comment', () => {
|
|
75
|
+
expect(lint(`<!-- a comment -->\n\n# Title\n`)).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('accepts an h1 after frontmatter and a <style> block', () => {
|
|
79
|
+
expect(lint(`---\nfoo: bar\n---\n\n<style>.x{}</style>\n\n# Title\n`)).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('accepts a document with a title in YAML frontmatter', () => {
|
|
83
|
+
expect(lint(`---\ntitle: Hello\n---\n\nSome content.\n`)).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('still flags missing h1 when frontmatterTitle is disabled', () => {
|
|
87
|
+
expect(
|
|
88
|
+
lint(`---\ntitle: Hello\n---\n\nSome content.\n`, { frontMatterTitle: false }),
|
|
89
|
+
).toHaveLength(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('accepts an h1 after MDX imports', () => {
|
|
93
|
+
expect(lintMdx(`import Foo from './foo';\n\n# Title\n`)).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('accepts an h1 after an MDX block comment expression', () => {
|
|
97
|
+
expect(lintMdx(`{/* a comment */}\n\n# Title\n`)).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('accepts an h1 after MDX comment + imports', () => {
|
|
101
|
+
expect(lintMdx(`{/* lint disable */}\n\nimport Foo from './foo';\n\n# Title\n`)).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('flags an MDX expression that is not a comment', () => {
|
|
105
|
+
expect(lintMdx(`{value}\n\n# Title\n`)).toHaveLength(1);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { lintRule } from 'unified-lint-rule';
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
|
|
4
|
+
const singleCharPrefixes = new Set([' ', '-', '+']);
|
|
5
|
+
const linePrefixes = ['@@ ', 'diff --git ', 'index '];
|
|
6
|
+
|
|
7
|
+
const remarkLintMuiGitDiff = lintRule(
|
|
8
|
+
{
|
|
9
|
+
origin: 'remark-lint:mui-git-diff',
|
|
10
|
+
url: 'https://github.com/mui/mui-public',
|
|
11
|
+
},
|
|
12
|
+
/** @param {import('mdast').Root} tree */
|
|
13
|
+
(tree, file) => {
|
|
14
|
+
visit(tree, 'code', (node) => {
|
|
15
|
+
if (node.lang !== 'diff' || !node.position) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const contentStartLine = node.position.start.line + 1;
|
|
19
|
+
const lines = node.value.split('\n');
|
|
20
|
+
lines.forEach((line, index) => {
|
|
21
|
+
if (line === '') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (singleCharPrefixes.has(line[0])) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (linePrefixes.some((prefix) => line.startsWith(prefix))) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const lineNumber = contentStartLine + index;
|
|
31
|
+
file.message(
|
|
32
|
+
'Line in a `diff` code block must start with " ", "+", "-", "@@ ", "diff --git ", or "index ".',
|
|
33
|
+
{
|
|
34
|
+
start: { line: lineNumber, column: 1 },
|
|
35
|
+
end: { line: lineNumber, column: line.length + 1 },
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export default remarkLintMuiGitDiff;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createLintTester } from './createLintTester.mjs';
|
|
3
|
+
import plugin from './gitDiff.mjs';
|
|
4
|
+
|
|
5
|
+
const lint = createLintTester(plugin);
|
|
6
|
+
|
|
7
|
+
describe('remark-lint-mui-git-diff', () => {
|
|
8
|
+
it('accepts a well-formed unified diff', () => {
|
|
9
|
+
const input = `# Title
|
|
10
|
+
|
|
11
|
+
\`\`\`diff
|
|
12
|
+
diff --git a/foo.txt b/foo.txt
|
|
13
|
+
index 0000..1111 100644
|
|
14
|
+
--- a/foo.txt
|
|
15
|
+
+++ b/foo.txt
|
|
16
|
+
@@ -1,2 +1,2 @@
|
|
17
|
+
unchanged line
|
|
18
|
+
-removed line
|
|
19
|
+
+added line
|
|
20
|
+
\`\`\`
|
|
21
|
+
`;
|
|
22
|
+
expect(lint(input)).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('ignores non-diff code blocks', () => {
|
|
26
|
+
const input = `\`\`\`js
|
|
27
|
+
const unrelated = 'line';
|
|
28
|
+
\`\`\`
|
|
29
|
+
`;
|
|
30
|
+
expect(lint(input)).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('flags lines that do not match the unified diff prefixes', () => {
|
|
34
|
+
const input = `\`\`\`diff
|
|
35
|
+
this line has no prefix
|
|
36
|
+
valid line
|
|
37
|
+
bad line too
|
|
38
|
+
\`\`\`
|
|
39
|
+
`;
|
|
40
|
+
const messages = lint(input);
|
|
41
|
+
expect(messages).toHaveLength(2);
|
|
42
|
+
expect(messages[0].line).toBe(2);
|
|
43
|
+
expect(messages[1].line).toBe(4);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { lintRule } from 'unified-lint-rule';
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
|
|
4
|
+
const remarkLintMuiNoSpaceInLinks = lintRule(
|
|
5
|
+
{
|
|
6
|
+
origin: 'remark-lint:mui-no-space-in-links',
|
|
7
|
+
url: 'https://github.com/mui/mui-public',
|
|
8
|
+
},
|
|
9
|
+
/** @param {import('mdast').Root} tree */
|
|
10
|
+
(tree, file) => {
|
|
11
|
+
const source = String(file.value);
|
|
12
|
+
visit(tree, 'link', (node) => {
|
|
13
|
+
if (!node.position || node.children.length === 0) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const first = node.children[0];
|
|
17
|
+
const last = node.children[node.children.length - 1];
|
|
18
|
+
if (!first.position || !last.position) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const textStart = first.position.start.offset;
|
|
22
|
+
const textEnd = last.position.end.offset;
|
|
23
|
+
if (textStart === undefined || textEnd === undefined) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const openBracket = source.lastIndexOf('[', textStart);
|
|
27
|
+
const closeBracket = source.indexOf(']', textEnd);
|
|
28
|
+
if (openBracket === -1 || closeBracket === -1) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const innerText = source.slice(openBracket + 1, closeBracket);
|
|
32
|
+
if (innerText.length === 0) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (innerText !== innerText.trim()) {
|
|
36
|
+
file.message('Link text should not start or end with whitespace.', node);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export default remarkLintMuiNoSpaceInLinks;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createLintTester } from './createLintTester.mjs';
|
|
3
|
+
import plugin from './noSpaceInLinks.mjs';
|
|
4
|
+
|
|
5
|
+
const lint = createLintTester(plugin);
|
|
6
|
+
|
|
7
|
+
describe('remark-lint-mui-no-space-in-links', () => {
|
|
8
|
+
it('accepts links without surrounding whitespace', () => {
|
|
9
|
+
expect(lint(`[link text](https://example.com)\n`)).toEqual([]);
|
|
10
|
+
expect(lint(`Some [inline](https://example.com) text.\n`)).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('flags leading whitespace in link text', () => {
|
|
14
|
+
const messages = lint(`[ link text ](https://example.com)\n`);
|
|
15
|
+
expect(messages).toHaveLength(1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('flags trailing whitespace in link text', () => {
|
|
19
|
+
const messages = lint(`[trailing ](https://example.com)\n`);
|
|
20
|
+
expect(messages).toHaveLength(1);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { lintRule } from 'unified-lint-rule';
|
|
2
|
+
|
|
3
|
+
const curlyQuotes = new Set(['‘', '’', '“', '”']);
|
|
4
|
+
|
|
5
|
+
const remarkLintMuiStraightQuotes = lintRule(
|
|
6
|
+
{
|
|
7
|
+
origin: 'remark-lint:mui-straight-quotes',
|
|
8
|
+
url: 'https://github.com/mui/mui-public',
|
|
9
|
+
},
|
|
10
|
+
/** @param {import('mdast').Root} _tree */
|
|
11
|
+
(_tree, file) => {
|
|
12
|
+
const text = String(file.value);
|
|
13
|
+
let line = 1;
|
|
14
|
+
let lineStart = 0;
|
|
15
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
16
|
+
const char = text[index];
|
|
17
|
+
if (char === '\n') {
|
|
18
|
+
line += 1;
|
|
19
|
+
lineStart = index + 1;
|
|
20
|
+
} else if (curlyQuotes.has(char)) {
|
|
21
|
+
const column = index - lineStart + 1;
|
|
22
|
+
file.message('Use straight quotes instead of curly quotes.', {
|
|
23
|
+
start: { line, column },
|
|
24
|
+
end: { line, column: column + 1 },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export default remarkLintMuiStraightQuotes;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createLintTester } from './createLintTester.mjs';
|
|
3
|
+
import plugin from './straightQuotes.mjs';
|
|
4
|
+
|
|
5
|
+
const lint = createLintTester(plugin);
|
|
6
|
+
|
|
7
|
+
describe('remark-lint-mui-straight-quotes', () => {
|
|
8
|
+
it('accepts straight quotes', () => {
|
|
9
|
+
expect(lint(`# Title\n\nA paragraph with "straight" and 'simple' quotes.\n`)).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('flags curly double quotes with correct location', () => {
|
|
13
|
+
const messages = lint(`# Title\n\nA paragraph with “curly” quotes.\n`);
|
|
14
|
+
expect(messages).toHaveLength(2);
|
|
15
|
+
expect(messages[0]).toMatchObject({ line: 3, column: 18 });
|
|
16
|
+
expect(messages[1]).toMatchObject({ line: 3, column: 24 });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('flags curly single quotes', () => {
|
|
20
|
+
const messages = lint(`Use ‘single’ curly quotes too.\n`);
|
|
21
|
+
expect(messages).toHaveLength(2);
|
|
22
|
+
expect(messages[0]).toMatchObject({ line: 1, column: 5 });
|
|
23
|
+
expect(messages[1]).toMatchObject({ line: 1, column: 12 });
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { lintRule } from 'unified-lint-rule';
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
|
|
4
|
+
const remarkLintMuiTableAlignment = lintRule(
|
|
5
|
+
{
|
|
6
|
+
origin: 'remark-lint:mui-table-alignment',
|
|
7
|
+
url: 'https://github.com/mui/mui-public',
|
|
8
|
+
},
|
|
9
|
+
/** @param {import('mdast').Root} tree */
|
|
10
|
+
(tree, file) => {
|
|
11
|
+
visit(tree, 'table', (node) => {
|
|
12
|
+
const align = node.align ?? [];
|
|
13
|
+
if (align.some((value) => value == null)) {
|
|
14
|
+
file.message(
|
|
15
|
+
'Table columns should declare an explicit alignment (`:---`, `---:`, or `:---:`).',
|
|
16
|
+
node,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export default remarkLintMuiTableAlignment;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createLintTester } from './createLintTester.mjs';
|
|
3
|
+
import plugin from './tableAlignment.mjs';
|
|
4
|
+
|
|
5
|
+
const lint = createLintTester(plugin);
|
|
6
|
+
|
|
7
|
+
describe('remark-lint-mui-table-alignment', () => {
|
|
8
|
+
it('accepts tables with explicit alignment on every column', () => {
|
|
9
|
+
const input = `| A | B |\n| :- | -: |\n| 1 | 2 |\n`;
|
|
10
|
+
expect(lint(input)).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('flags tables with at least one column missing alignment', () => {
|
|
14
|
+
const input = `| A | B |\n| :- | -- |\n| 1 | 2 |\n`;
|
|
15
|
+
const messages = lint(input);
|
|
16
|
+
expect(messages).toHaveLength(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('flags tables with no alignment at all', () => {
|
|
20
|
+
const input = `| A | B |\n| - | - |\n| 1 | 2 |\n`;
|
|
21
|
+
const messages = lint(input);
|
|
22
|
+
expect(messages).toHaveLength(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('does not flag non-table content', () => {
|
|
26
|
+
expect(lint(`# Heading\n\nSome paragraph.\n`)).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { lintRule } from 'unified-lint-rule';
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
|
|
4
|
+
const remarkLintMuiTerminalLanguage = lintRule(
|
|
5
|
+
{
|
|
6
|
+
origin: 'remark-lint:mui-terminal-language',
|
|
7
|
+
url: 'https://github.com/mui/mui-public',
|
|
8
|
+
},
|
|
9
|
+
/** @param {import('mdast').Root} tree */
|
|
10
|
+
(tree, file) => {
|
|
11
|
+
visit(tree, 'code', (node) => {
|
|
12
|
+
if (node.lang === 'sh' && node.position) {
|
|
13
|
+
file.message('Use `bash` instead of `sh` as the code block language.', node);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export default remarkLintMuiTerminalLanguage;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createLintTester } from './createLintTester.mjs';
|
|
3
|
+
import plugin from './terminalLanguage.mjs';
|
|
4
|
+
|
|
5
|
+
const lint = createLintTester(plugin);
|
|
6
|
+
|
|
7
|
+
describe('remark-lint-mui-terminal-language', () => {
|
|
8
|
+
it('accepts bash and other languages', () => {
|
|
9
|
+
expect(lint(`\`\`\`bash\necho hi\n\`\`\`\n`)).toEqual([]);
|
|
10
|
+
expect(lint(`\`\`\`js\nconst x = 1;\n\`\`\`\n`)).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('flags `sh` code fences', () => {
|
|
14
|
+
const messages = lint(`\`\`\`sh\necho hi\n\`\`\`\n`);
|
|
15
|
+
expect(messages).toHaveLength(1);
|
|
16
|
+
});
|
|
17
|
+
});
|