UNPKG

14.8 kBJavaScriptView Raw
1import { version } from 'eslint/package.json';
2import { JSX_TYPES, isJsxNode, openTag, DEFAULT_EXTENSIONS, MARKDOWN_EXTENSIONS } from 'eslint-mdx';
3import reactNoUnescapedEntities from 'eslint-plugin-react/lib/rules/no-unescaped-entities';
4import esLintNoUnusedExpressions from 'eslint/lib/rules/no-unused-expressions';
5import path from 'path';
6import vfile from 'vfile';
7import { cosmiconfigSync } from 'cosmiconfig';
8import remarkMdx from 'remark-mdx';
9import remarkParse from 'remark-parse';
10import remarkStringify from 'remark-stringify';
11import unified from 'unified';
12
13const base = {
14 parser: 'eslint-mdx',
15 plugins: ['mdx'],
16};
17
18const getGlobals = (sources, initialGlobals = {}) => (Array.isArray(sources)
19 ? sources
20 : Object.keys(sources)).reduce((globals, source) => Object.assign(globals, {
21 [source]: false,
22}), initialGlobals);
23
24let rebass;
25try {
26 // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
27 rebass = require('rebass');
28}
29catch (_a) {
30 // `rebass`(or `reflexbox` actually) requires `react` as peerDependency, but not all projects using `mdx` are `React` based, so we fallback to hardcoded `rebass` Components here
31 /* istanbul ignore next */
32 rebass = ['Box', 'Flex', 'Text', 'Heading', 'Link', 'Button', 'Image', 'Card'];
33}
34const overrides = Object.assign(Object.assign({}, base), { globals: getGlobals(rebass, {
35 React: false,
36 }), rules: {
37 'lines-between-class-members': 0,
38 'react/jsx-no-undef': [
39 2,
40 {
41 allowGlobals: true,
42 },
43 ],
44 'react/react-in-jsx-scope': 0,
45 } });
46
47const minorVersion = +version.split('.').slice(0, 2).join('.');
48const recommended = Object.assign(Object.assign({}, base), { rules: {
49 'mdx/no-jsx-html-comments': 2,
50 'mdx/no-unescaped-entities': 1,
51 'mdx/no-unused-expressions': 2,
52 'mdx/remark': 1,
53 'no-unused-expressions': 0,
54 'react/no-unescaped-entities': 0,
55 } });
56const OVERRIDES_AVAILABLE_VERSION = 6.4;
57// overrides in npm pkg is supported after v6.4.0
58// istanbul ignore else
59if (minorVersion >= OVERRIDES_AVAILABLE_VERSION) {
60 const overrides = [
61 {
62 files: '*.mdx',
63 extends: 'plugin:mdx/overrides',
64 },
65 ];
66 try {
67 // eslint-disable-next-line node/no-extraneous-require
68 require.resolve('prettier');
69 // eslint-disable-next-line node/no-extraneous-require
70 require.resolve('eslint-plugin-prettier');
71 overrides.push({
72 files: '*.md',
73 rules: {
74 'prettier/prettier': [
75 2,
76 {
77 parser: 'markdown',
78 },
79 ],
80 },
81 });
82 }
83 catch (_a) { }
84 Object.assign(recommended, {
85 overrides,
86 });
87}
88
89var index = /*#__PURE__*/Object.freeze({
90 __proto__: null,
91 base: base,
92 overrides: overrides,
93 recommended: recommended
94});
95
96const noJsxHtmlComments = {
97 meta: {
98 type: 'problem',
99 docs: {
100 description: 'Forbid invalid html style comments in jsx block',
101 category: 'SyntaxError',
102 recommended: true,
103 },
104 messages: {
105 jsxHtmlComments: 'html style comments are invalid in jsx: {{ origin }}',
106 },
107 fixable: 'code',
108 },
109 create(context) {
110 return {
111 ExpressionStatement(node) {
112 const invalidNodes = context.parserServices
113 .JSXElementsWithHTMLComments;
114 if (!JSX_TYPES.includes(node.expression.type) ||
115 node.parent.type !== 'Program' ||
116 !invalidNodes ||
117 invalidNodes.length === 0) {
118 return;
119 }
120 const invalidNode = invalidNodes.shift();
121 if (invalidNode.data.inline) {
122 return;
123 }
124 const comments = invalidNode.data.comments;
125 for (const { fixed, loc, origin } of comments) {
126 context.report({
127 messageId: 'jsxHtmlComments',
128 data: {
129 origin,
130 },
131 loc,
132 node,
133 fix(fixer) {
134 return fixer.replaceTextRange([loc.start.offset, loc.end.offset], fixed);
135 },
136 });
137 }
138 },
139 };
140 },
141};
142
143/// <reference path="../../typings.d.ts" />
144// copied from `eslint-plugin-react`
145const DEFAULTS = [
146 {
147 char: '>',
148 alternatives: ['&gt;'],
149 },
150 {
151 char: '"',
152 alternatives: ['&quot;', '&ldquo;', '&#34;', '&rdquo;'],
153 },
154 {
155 char: "'",
156 alternatives: ['&apos;', '&lsquo;', '&#39;', '&rsquo;'],
157 },
158 {
159 char: '}',
160 alternatives: ['&#125;'],
161 },
162];
163const EXPRESSION = 'Literal, JSXText';
164const noUnescapedEntities = Object.assign(Object.assign({}, reactNoUnescapedEntities), { create(context) {
165 const configuration = (context.options[0] || {});
166 const entities = configuration.forbid || DEFAULTS;
167 return {
168 // eslint-disable-next-line sonarjs/cognitive-complexity
169 [EXPRESSION](node) {
170 let { parent, loc } = node;
171 if (!isJsxNode(parent)) {
172 return;
173 }
174 while (parent) {
175 if (parent.parent.type === 'Program') {
176 break;
177 }
178 else {
179 parent = parent.parent;
180 }
181 }
182 const { start: { line: startLine, column: startColumn }, end: { line: endLine, column: endColumn }, } = loc;
183 const { lines } = context.getSourceCode();
184 let firstLineOffset = parent.loc.start.line < startLine
185 ? 0
186 : lines
187 .slice(startLine - 1, endLine)
188 .join('\n')
189 .search(openTag);
190 /* istanbul ignore if */
191 if (firstLineOffset < 0) {
192 // should never happen, just for robustness
193 firstLineOffset = 0;
194 }
195 for (let i = startLine; i <= endLine; i++) {
196 let rawLine = lines[i - 1];
197 let start = 0;
198 let end = rawLine.length;
199 if (i === startLine) {
200 start = startColumn + firstLineOffset;
201 }
202 if (i === endLine) {
203 end = endColumn;
204 if (i === startLine) {
205 end += firstLineOffset;
206 }
207 }
208 rawLine = rawLine.slice(start, end);
209 for (const entity of entities) {
210 // eslint-disable-next-line unicorn/no-for-loop
211 for (let index = 0; index < rawLine.length; index++) {
212 const char = rawLine[index];
213 if (typeof entity === 'string') {
214 if (char === entity) {
215 context.report({
216 loc: { line: i, column: start + index },
217 message: `HTML entity, \`${entity}\` , must be escaped.`,
218 node,
219 });
220 }
221 }
222 else if (char === entity.char) {
223 context.report({
224 loc: { line: i, column: start + index },
225 message: `\`${entity.char}\` can be escaped with ${entity.alternatives
226 .map(alt => '``'.split('').join(alt))
227 .join(', ')}.`,
228 node,
229 });
230 }
231 }
232 }
233 }
234 },
235 };
236 } });
237
238/// <reference path="../../typings.d.ts" />
239const noUnusedExpressions = Object.assign(Object.assign({}, esLintNoUnusedExpressions), { create(context) {
240 const esLintRuleListener = esLintNoUnusedExpressions.create(context);
241 return {
242 ExpressionStatement(node) {
243 if (isJsxNode(node.expression) && node.parent.type === 'Program') {
244 return;
245 }
246 esLintRuleListener.ExpressionStatement(node);
247 },
248 };
249 } });
250
251const requirePkg = (plugin, prefix, filePath) => {
252 if (filePath && /^\.\.?([/\\]|$)/.test(plugin)) {
253 plugin = path.resolve(path.dirname(filePath), plugin);
254 }
255 prefix = prefix.endsWith('-') ? prefix : prefix + '-';
256 const packages = [
257 plugin,
258 plugin.startsWith('@')
259 ? plugin.replace('/', '/' + prefix)
260 : prefix + plugin,
261 ];
262 let error;
263 for (const pkg of packages) {
264 try {
265 // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
266 return require(pkg);
267 }
268 catch (err) {
269 if (!error) {
270 error = err;
271 }
272 }
273 }
274 throw error;
275};
276let searchSync;
277let remarkProcessor;
278const getRemarkProcessor = (searchFrom, isMdx) => {
279 if (!searchSync) {
280 searchSync = cosmiconfigSync('remark', {
281 packageProp: 'remarkConfig',
282 }).search;
283 }
284 if (!remarkProcessor) {
285 remarkProcessor = unified().use(remarkParse).freeze();
286 }
287 /* istanbul ignore next */
288 const result = searchSync(searchFrom) || {};
289 /* istanbul ignore next */
290 const { plugins = [], settings } = (result.config ||
291 {});
292 try {
293 // disable this rule automatically since we already have a parser option `extensions`
294 // eslint-disable-next-line node/no-extraneous-require
295 plugins.push([require.resolve('remark-lint-file-extension'), false]);
296 }
297 catch (_a) {
298 // just ignore if the package does not exist
299 }
300 const initProcessor = remarkProcessor().use({ settings }).use(remarkStringify);
301 if (isMdx) {
302 initProcessor.use(remarkMdx);
303 }
304 return plugins
305 .reduce((processor, pluginWithSettings) => {
306 const [plugin, ...pluginSettings] = Array.isArray(pluginWithSettings)
307 ? pluginWithSettings
308 : [pluginWithSettings];
309 return processor.use(
310 /* istanbul ignore next */
311 typeof plugin === 'string'
312 ? requirePkg(plugin, 'remark', result.filepath)
313 : plugin, ...pluginSettings);
314 }, initProcessor)
315 .freeze();
316};
317
318const remark = {
319 meta: {
320 type: 'layout',
321 docs: {
322 description: 'Linter integration with remark plugins',
323 category: 'Stylistic Issues',
324 recommended: true,
325 },
326 messages: {
327 remarkReport: '{{ source }}:{{ ruleId }} - {{ reason }}',
328 },
329 fixable: 'code',
330 },
331 create(context) {
332 const filename = context.getFilename();
333 const extname = path.extname(filename);
334 const sourceCode = context.getSourceCode();
335 const options = context.parserOptions;
336 const isMdx = DEFAULT_EXTENSIONS.concat(options.extensions || []).includes(extname);
337 const isMarkdown = MARKDOWN_EXTENSIONS.concat(options.markdownExtensions || []).includes(extname);
338 return {
339 Program(node) {
340 /* istanbul ignore if */
341 if (!isMdx && !isMarkdown) {
342 return;
343 }
344 const sourceText = sourceCode.getText(node);
345 const remarkProcessor = getRemarkProcessor(filename, isMdx);
346 const file = vfile({
347 path: filename,
348 contents: sourceText,
349 });
350 try {
351 remarkProcessor.processSync(file);
352 }
353 catch (err) {
354 /* istanbul ignore next */
355 if (!file.messages.includes(err)) {
356 file.message(err).fatal = true;
357 }
358 }
359 for (const { source, reason, ruleId, location: { start, end }, } of file.messages) {
360 context.report({
361 messageId: 'remarkReport',
362 data: {
363 reason,
364 source,
365 ruleId,
366 },
367 loc: {
368 // ! eslint ast column is 0-indexed, but unified is 1-indexed
369 start: Object.assign(Object.assign({}, start), { column: start.column - 1 }),
370 end: Object.assign(Object.assign({}, end), { column: end.column - 1 }),
371 },
372 node,
373 fix(fixer) {
374 /* istanbul ignore if */
375 if (start.offset == null) {
376 return null;
377 }
378 const range = [
379 start.offset,
380 /* istanbul ignore next */
381 end.offset == null ? start.offset + 1 : end.offset,
382 ];
383 const partialText = sourceText.slice(...range);
384 const fixed = remarkProcessor.processSync(partialText).toString();
385 return fixer.replaceTextRange(range,
386 /* istanbul ignore next */
387 partialText.endsWith('\n') ? fixed : fixed.slice(0, -1));
388 },
389 });
390 }
391 },
392 };
393 },
394};
395
396const rules = {
397 'no-jsx-html-comments': noJsxHtmlComments,
398 'no-unescaped-entities': noUnescapedEntities,
399 'no-unused-expressions': noUnusedExpressions,
400 remark,
401};
402
403export { index as configs, getGlobals, getRemarkProcessor, noJsxHtmlComments, noUnescapedEntities, noUnusedExpressions, remark, requirePkg, rules };