diff --git a/.travis.yml b/.travis.yml index 336a164c..7bf3a72a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,6 @@ before_install: install: yarn --frozen-lockfile script: - - yarn build - yarn lint - yarn test diff --git a/package.json b/package.json index b4f37db8..5c79224f 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "lint:es": "cross-env PARSER_NO_WATCH=true eslint . --cache --ext js,md,ts -f friendly", "lint:ts": "tslint -p . -t stylish", "postinstall": "simple-git-hooks && yarn-deduplicate --strategy fewer || exit 0", + "prelint": "yarn build", "prerelease": "yarn build", "release": "lerna publish --conventional-commits --create-release github --yes", "test": "jest", @@ -35,6 +36,7 @@ "lerna": "^4.0.0", "npm-run-all": "^4.1.5", "react": "^17.0.2", + "remark-validate-links": "^10.0.4", "ts-jest": "^26.5.5", "ts-node": "^9.1.1", "tslint": "^6.1.3", diff --git a/packages/eslint-plugin-mdx/package.json b/packages/eslint-plugin-mdx/package.json index 3633c955..5898fb94 100644 --- a/packages/eslint-plugin-mdx/package.json +++ b/packages/eslint-plugin-mdx/package.json @@ -39,6 +39,7 @@ "remark-mdx": "^1.6.22", "remark-parse": "^8.0.3", "remark-stringify": "^8.1.1", + "synckit": "^0.1.5", "tslib": "^2.2.0", "unified": "^9.2.1", "vfile": "^4.2.1" diff --git a/packages/eslint-plugin-mdx/src/rules/remark.ts b/packages/eslint-plugin-mdx/src/rules/remark.ts index 9eb58d56..423d2968 100644 --- a/packages/eslint-plugin-mdx/src/rules/remark.ts +++ b/packages/eslint-plugin-mdx/src/rules/remark.ts @@ -3,10 +3,16 @@ import path from 'path' import type { Rule } from 'eslint' import type { ParserOptions } from 'eslint-mdx' import { DEFAULT_EXTENSIONS, MARKDOWN_EXTENSIONS } from 'eslint-mdx' +import { createSyncFn } from 'synckit' +import type { FrozenProcessor } from 'unified' import vfile from 'vfile' import { getPhysicalFilename, getRemarkProcessor } from './helpers' -import type { RemarkLintMessage } from './types' +import type { ProcessSync, RemarkLintMessage } from './types' + +const processSync = createSyncFn(require.resolve('../worker')) as ProcessSync + +const brokenCache = new WeakMap() export const remark: Rule.RuleModule = { meta: { @@ -36,22 +42,45 @@ export const remark: Rule.RuleModule = { if (!isMdx && !isMarkdown) { return } + + const physicalFilename = getPhysicalFilename(filename) + const sourceText = sourceCode.getText(node) - const remarkProcessor = getRemarkProcessor( - getPhysicalFilename(filename), - isMdx, - ) - const file = vfile({ + const remarkProcessor = getRemarkProcessor(physicalFilename, isMdx) + + const fileOptions = { path: filename, contents: sourceText, - }) + } - try { - remarkProcessor.processSync(file) - } catch (err) { - /* istanbul ignore next */ - if (!file.messages.includes(err)) { - file.message(err).fatal = true + const file = vfile(fileOptions) + + let broken = brokenCache.get(remarkProcessor) + + if (broken) { + const { messages } = processSync(fileOptions, physicalFilename, isMdx) + file.messages = messages + } else { + try { + remarkProcessor.processSync(file) + } catch (err) { + /* istanbul ignore else */ + if ( + (err as Error).message === + '`processSync` finished async. Use `process` instead' + ) { + brokenCache.set(remarkProcessor, (broken = true)) + const { messages } = processSync( + fileOptions, + physicalFilename, + isMdx, + ) + file.messages = messages + } else { + if (!file.messages.includes(err)) { + file.message(err).fatal = true + } + } } } @@ -102,11 +131,14 @@ export const remark: Rule.RuleModule = { end.offset == null ? start.offset + 1 : end.offset, ] const partialText = sourceText.slice(...range) - const fixed = remarkProcessor.processSync(partialText).toString() + const fixed = broken + ? processSync(partialText, physicalFilename, isMdx) + : remarkProcessor.processSync(partialText).toString() return fixer.replaceTextRange( range, - /* istanbul ignore next */ - partialText.endsWith('\n') ? fixed : fixed.slice(0, -1), // remove redundant new line + partialText.endsWith('\n') + ? /* istanbul ignore next */ fixed + : fixed.slice(0, -1), // remove redundant new line ) }, }) diff --git a/packages/eslint-plugin-mdx/src/rules/types.ts b/packages/eslint-plugin-mdx/src/rules/types.ts index ba79e6d4..b3739b21 100644 --- a/packages/eslint-plugin-mdx/src/rules/types.ts +++ b/packages/eslint-plugin-mdx/src/rules/types.ts @@ -1,6 +1,7 @@ import type { Linter } from 'eslint' import type { ExpressionStatement, Node } from 'estree' import type { Attacher } from 'unified' +import type { VFile, VFileOptions } from 'vfile' export interface WithParent { parent: NodeWithParent @@ -25,3 +26,16 @@ export interface RemarkLintMessage { ruleId: string severity: Linter.Severity } + +export interface ProcessSync { + (text: string, physicalFilename: string, isFile: boolean): string + (fileOptions: VFileOptions, physicalFilename: string, isFile: boolean): Pick< + VFile, + 'messages' + > + ( + textOrFileOptions: string | VFileOptions, + physicalFilename: string, + isFile: boolean, + ): string | VFileOptions +} diff --git a/packages/eslint-plugin-mdx/src/worker.ts b/packages/eslint-plugin-mdx/src/worker.ts new file mode 100644 index 00000000..8dcd9a4e --- /dev/null +++ b/packages/eslint-plugin-mdx/src/worker.ts @@ -0,0 +1,27 @@ +import { runAsWorker } from 'synckit' +import type { VFileOptions } from 'vfile' +import vfile from 'vfile' + +import { getRemarkProcessor } from './rules' + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +runAsWorker( + async ( + textOrFileOptions: string | VFileOptions, + physicalFilename: string, + isMdx: boolean, + ) => { + const remarkProcessor = getRemarkProcessor(physicalFilename, isMdx) + const file = vfile(textOrFileOptions) + try { + await remarkProcessor.process(file) + } catch (err) { + if (!file.messages.includes(err)) { + file.message(err).fatal = true + } + } + return typeof textOrFileOptions === 'string' + ? file.toString() + : { messages: file.messages } + }, +) diff --git a/test/fixtures/async/.remarkrc b/test/fixtures/async/.remarkrc new file mode 100644 index 00000000..d5193298 --- /dev/null +++ b/test/fixtures/async/.remarkrc @@ -0,0 +1,8 @@ +{ + "plugins": [ + [ + "validate-links", + 2 + ] + ] +} diff --git a/test/remark.test.ts b/test/remark.test.ts index 06be0cca..6b9510c2 100644 --- a/test/remark.test.ts +++ b/test/remark.test.ts @@ -41,11 +41,16 @@ ruleTester.run('remark 1', remark, { parserOptions, // dark hack get filename() { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access processorCache.clear() return path.resolve(userDir, '../test.md') }, }, + { + code: '
Header6
', + parser, + parserOptions, + filename: path.resolve(__dirname, 'fixtures/async/test.mdx'), + }, ], invalid: [ { @@ -68,5 +73,25 @@ ruleTester.run('remark 1', remark, { }, ], }, + { + code: '[CHANGELOG](./CHANGELOG.md)', + parser, + parserOptions, + filename: path.resolve(__dirname, 'fixtures/async/test.mdx'), + errors: [ + { + message: JSON.stringify({ + reason: 'Link to unknown file: `CHANGELOG.md`', + source: 'remark-validate-links', + ruleId: 'missing-file', + severity: 1, + }), + line: 1, + column: 1, + endLine: 1, + endColumn: 28, + }, + ], + }, ], }) diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 00000000..6055613d --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.base", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "eslint-mdx": ["packages/eslint-mdx/src"], + "eslint-plugin-mdx": ["packages/eslint-plugin-mdx/src"] + } + } +} diff --git a/tsconfig.json b/tsconfig.json index e931e294..b41dbae9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "./tsconfig.base.json", + "extends": "./tsconfig.base", "compilerOptions": { "noEmit": true }, diff --git a/yarn.lock b/yarn.lock index ab02dcf9..ba5e5138 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4847,6 +4847,11 @@ emittery@^0.7.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== +"emoji-regex@>=6.0.0 <=6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e" + integrity sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4= + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -5855,6 +5860,13 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" +github-slugger@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9" + integrity sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q== + dependencies: + emoji-regex ">=6.0.0 <=6.1.1" + glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -6054,6 +6066,13 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== +hosted-git-info@^3.0.0: + version "3.0.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.8.tgz#6e35d4cc87af2c5f816e4cb9ce350ba87a3f370d" + integrity sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw== + dependencies: + lru-cache "^6.0.0" + hosted-git-info@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" @@ -7362,6 +7381,11 @@ leven@^3.1.0: resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== +levenshtein-edit-distance@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/levenshtein-edit-distance/-/levenshtein-edit-distance-1.0.0.tgz#895baf478cce8b5c1a0d27e45d7c1d978a661e49" + integrity sha1-iVuvR4zOi1waDSfkXXwdl4pmHkk= + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -7733,7 +7757,7 @@ mdast-util-heading-style@^1.0.2: resolved "https://registry.yarnpkg.com/mdast-util-heading-style/-/mdast-util-heading-style-1.0.6.tgz#6410418926fd5673d40f519406b35d17da10e3c5" integrity sha512-8ZuuegRqS0KESgjAGW8zTx4tJ3VNIiIaGFNEzFpRSAQBavVc7AvOo9I4g3crcZBfYisHs4seYh0rAVimO6HyOw== -mdast-util-to-string@^1.0.2: +mdast-util-to-string@^1.0.0, mdast-util-to-string@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527" integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A== @@ -9287,6 +9311,13 @@ prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" +propose@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/propose/-/propose-0.0.5.tgz#48a065d9ec7d4c8667f4050b15c4a2d85dbca56b" + integrity sha1-SKBl2ex9TIZn9AULFcSi2F28pWs= + dependencies: + levenshtein-edit-distance "^1.0.0" + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -10343,6 +10374,19 @@ remark-stringify@^8.1.1: unherit "^1.0.4" xtend "^4.0.1" +remark-validate-links@^10.0.4: + version "10.0.4" + resolved "https://registry.yarnpkg.com/remark-validate-links/-/remark-validate-links-10.0.4.tgz#a2711fa794f691c944faf8126767152dcfee0c47" + integrity sha512-oNGRcsoQkL35WoZKLMMBugDwvHfyu0JPA5vSYkEcvR6YBsFKBo4RedpecuokTK1wgD9l01rPxaQ9dPmRQYFhyg== + dependencies: + github-slugger "^1.0.0" + hosted-git-info "^3.0.0" + mdast-util-to-string "^1.0.0" + propose "0.0.5" + to-vfile "^6.0.0" + trough "^1.0.0" + unist-util-visit "^2.0.0" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -11325,6 +11369,14 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +synckit@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.1.5.tgz#f34462b2e3686bba3dbea2ae13b6e01adff2ffb8" + integrity sha512-s9rDbMJAF5SDEwBGH/DvbN/fb5N1Xu1boL4Uv66idbCbtosNX3ikUsFvGhROmPXsvlMYMcT5ksmkU5RSnkFi9Q== + dependencies: + tslib "^2.2.0" + uuid "^8.3.2" + table@^6.0.4: version "6.0.9" resolved "https://registry.yarnpkg.com/table/-/table-6.0.9.tgz#790a12bf1e09b87b30e60419bafd6a1fd85536fb" @@ -11504,6 +11556,14 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +to-vfile@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/to-vfile/-/to-vfile-6.1.0.tgz#5f7a3f65813c2c4e34ee1f7643a5646344627699" + integrity sha512-BxX8EkCxOAZe+D/ToHdDsJcVI4HqQfmw0tCkp31zf3dNP/XWIAjU4CmeuSwsSoOzOTqHPOL0KUzyZqJplkD0Qw== + dependencies: + is-buffer "^2.0.0" + vfile "^4.0.0" + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -12064,7 +12124,7 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0: +uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==