diff --git a/packages/remark-lint/package.json b/packages/remark-lint/package.json index 47e3f79083a58..4b77244975890 100644 --- a/packages/remark-lint/package.json +++ b/packages/remark-lint/package.json @@ -1,7 +1,7 @@ { "name": "@node-core/remark-lint", "type": "module", - "version": "1.3.0", + "version": "1.4.0", "exports": { ".": "./src/index.mjs", "./api": "./src/api.mjs" @@ -50,6 +50,7 @@ "remark-lint-table-pipes": "^5.0.1", "remark-lint-unordered-list-marker-style": "^4.0.1", "remark-preset-lint-recommended": "^7.0.1", + "remark-validate-links": "^13.1.0", "semver": "^7.8.1", "unified-lint-rule": "^3.0.1", "unist-util-visit": "^5.1.0", diff --git a/packages/remark-lint/src/__tests__/index.test.mjs b/packages/remark-lint/src/__tests__/index.test.mjs new file mode 100644 index 0000000000000..78df4d7f20da2 --- /dev/null +++ b/packages/remark-lint/src/__tests__/index.test.mjs @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import remarkParse from 'remark-parse'; +import remarkValidateLinks from 'remark-validate-links'; +import { unified } from 'unified'; + +import basePreset from '../index.mjs'; + +describe('base preset', () => { + it('reports broken local links', async () => { + const linkValidator = basePreset.plugins.find( + plugin => plugin === remarkValidateLinks + ); + + assert.equal(linkValidator, remarkValidateLinks); + + const processor = unified() + .use(remarkParse) + .use(linkValidator, { repository: false, root: process.cwd() }); + const tree = processor.parse('# Existing heading\n\n[Missing](#missing)'); + const file = await new Promise((resolve, reject) => { + processor.run(tree, { path: 'docs/example.md' }, (error, _, vfile) => { + if (error) { + reject(error); + } else { + resolve(vfile); + } + }); + }); + + assert.ok( + file.messages.some( + message => + message.source.startsWith('remark-validate-links') && + message.ruleId === 'missing-heading' + ) + ); + }); +}); diff --git a/packages/remark-lint/src/index.mjs b/packages/remark-lint/src/index.mjs index bad1d1d8cc956..570dae62844e9 100644 --- a/packages/remark-lint/src/index.mjs +++ b/packages/remark-lint/src/index.mjs @@ -23,6 +23,7 @@ import remarkLintStrongMarker from 'remark-lint-strong-marker'; import remarkLintTableCellPadding from 'remark-lint-table-cell-padding'; import remarkLintTablePipes from 'remark-lint-table-pipes'; import remarkPresetLintRecommended from 'remark-preset-lint-recommended'; +import remarkValidateLinks from 'remark-validate-links'; export default { settings: { @@ -54,6 +55,7 @@ export default { remarkLintNofileNameOuterDashes, // Heading and link rules + remarkValidateLinks, remarkLintFinalDefinition, [remarkLintNoUnusedDefinitions, false], [remarkLintNoLiteralURLs, false], diff --git a/packages/remark-lint/src/rules/__tests__/invalid-type-reference.test.mjs b/packages/remark-lint/src/rules/__tests__/invalid-type-reference.test.mjs index 21f1cc54053f4..c5a11565711a2 100644 --- a/packages/remark-lint/src/rules/__tests__/invalid-type-reference.test.mjs +++ b/packages/remark-lint/src/rules/__tests__/invalid-type-reference.test.mjs @@ -1,3 +1,4 @@ +import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { testRule } from './utils.mjs'; @@ -24,6 +25,19 @@ const testCases = [ input: '[]()', expected: ['Type reference must be wrapped in "{}"; saw ""'], }, + { + name: 'ignores references in non-prose nodes', + input: ` + +\`{invalid}\` + +\`\`\`js +const value = {invalid}; +\`\`\` + +
{invalid}
`, + expected: [], + }, { name: 'multiple references', input: 'Psst, are you a {string | boolean}', @@ -54,9 +68,13 @@ const testCases = [ ]; describe('invalid-type-reference', () => { - for (const { name, input, expected, options } of testCases) { - it(name, () => - testRule(invalidTypeReference, input, expected, {}, options) - ); + for (const { name, input, expected, options, replacement } of testCases) { + it(name, () => { + const tree = testRule(invalidTypeReference, input, expected, {}, options); + + if (replacement) { + assert.equal(tree.children[0].children[0].value, replacement); + } + }); } }); diff --git a/packages/remark-lint/src/rules/__tests__/utils.mjs b/packages/remark-lint/src/rules/__tests__/utils.mjs index e3245368b5173..df98694f0b687 100644 --- a/packages/remark-lint/src/rules/__tests__/utils.mjs +++ b/packages/remark-lint/src/rules/__tests__/utils.mjs @@ -32,6 +32,8 @@ export const testRule = ( vfile.message.mock.calls.map(call => call.arguments[0]), expected ); + + return tree; }; /** diff --git a/packages/remark-lint/src/rules/invalid-type-reference.mjs b/packages/remark-lint/src/rules/invalid-type-reference.mjs index c833847bc2027..4fa4d2c6dcd14 100644 --- a/packages/remark-lint/src/rules/invalid-type-reference.mjs +++ b/packages/remark-lint/src/rules/invalid-type-reference.mjs @@ -6,47 +6,55 @@ import { visit } from 'unist-util-visit'; const MATCH_RE = /\s\||\| /g; const REPLACE_RE = /\s*\| */g; +const isTypeNode = node => { + if (node.type === 'text') { + return QUERIES.normalizeTypes.test(node.value); + } + + if (node.type === 'html') { + return node.value.match(QUERIES.normalizeTypes)?.includes(node.value); + } + + return false; +}; + /** * Ensures that all type references are valid * @type {import('unified-lint-rule').Rule<, import('../api.mjs').Options>} */ const invalidTypeReference = (tree, vfile, { typeMap = {} }) => { - visit( - tree, - ({ value }) => QUERIES.normalizeTypes.test(value), - node => { - const types = node.value.match(QUERIES.normalizeTypes); - - types.forEach(type => { - // Ensure wrapped in {} - if (type[0] !== '{' || type[type.length - 1] !== '}') { - vfile.message( - `Type reference must be wrapped in "{}"; saw "${type}"`, - node - ); - - const newType = `{${type.slice(1, -1)}}`; - node.value = node.value.replace(type, newType); - type = newType; - } - - // Fix spaces around | - if (MATCH_RE.test(type)) { - vfile.message( - `Type reference should be separated by "|", without spaces; saw "${type}"`, - node - ); - - const normalized = type.replace(REPLACE_RE, '|'); - node.value = node.value.replace(type, normalized); - } - - if (transformTypeToReferenceLink(type, typeMap) === type) { - vfile.message(`Invalid type reference: ${type}`, node); - } - }); - } - ); + visit(tree, isTypeNode, node => { + const types = node.value.match(QUERIES.normalizeTypes); + + types.forEach(type => { + // Ensure wrapped in {} + if (type[0] !== '{' || type[type.length - 1] !== '}') { + vfile.message( + `Type reference must be wrapped in "{}"; saw "${type}"`, + node + ); + + const newType = `{${type.slice(1, -1)}}`; + node.value = node.value.replace(type, newType); + type = newType; + } + + // Fix spaces around | + if (MATCH_RE.test(type)) { + vfile.message( + `Type reference should be separated by "|", without spaces; saw "${type}"`, + node + ); + + const normalized = type.replace(REPLACE_RE, '|'); + node.value = node.value.replace(type, normalized); + } + + if (transformTypeToReferenceLink(type, typeMap) === type) { + vfile.message(`Invalid type reference: ${type}`, node); + } + }); + }); }; export default lintRule( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9eca01b6fc450..2a57ef2d9e4a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -642,6 +642,9 @@ importers: remark-preset-lint-recommended: specifier: ^7.0.1 version: 7.0.1 + remark-validate-links: + specifier: ^13.1.0 + version: 13.1.0 semver: specifier: ^7.8.1 version: 7.8.2 @@ -4340,6 +4343,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hosted-git-info@3.0.5': + resolution: {integrity: sha512-Dmngh7U003cOHPhKGyA7LWqrnvcTyILNgNPmNCxlx7j8MIi54iBliiT8XqVLIQ3GchoOjVAyBzNJVyuaJjqokg==} + '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} @@ -6608,6 +6614,10 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + levenshtein-edit-distance@1.0.0: + resolution: {integrity: sha512-gpgBvPn7IFIAL32f0o6Nsh2g+5uOvkt4eK9epTfgE4YVxBxwVhJ/p1888lMm/u8mXdu1ETLSi6zeEmkBI+0F3w==} + hasBin: true + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -7639,6 +7649,9 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + propose@0.0.5: + resolution: {integrity: sha512-Jary1vb+ap2DIwOGfyiadcK4x1Iu3pzpkDBy8tljFPmQvnc9ES3m1PMZOMiWOG50cfoAyYNtGeBzrp+Rlh4G9A==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -7960,6 +7973,9 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remark-validate-links@13.1.0: + resolution: {integrity: sha512-z+glZ4zoRyrWimQHtoqJEFJdPoIR1R1SDr/JoWjmS6EsYlyhxNuCHtIt165gmV7ltOSFJ+rGsipqRGfBPInd7A==} + renderkid@3.0.0: resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} @@ -12795,6 +12811,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/hosted-git-info@3.0.5': {} + '@types/html-minifier-terser@6.1.0': {} '@types/is-empty@1.2.3': {} @@ -15502,6 +15520,8 @@ snapshots: kleur@4.1.5: {} + levenshtein-edit-distance@1.0.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -16763,6 +16783,10 @@ snapshots: property-information@7.1.0: {} + propose@0.0.5: + dependencies: + levenshtein-edit-distance: 1.0.0 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -17452,6 +17476,23 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + remark-validate-links@13.1.0: + dependencies: + '@types/hosted-git-info': 3.0.5 + '@types/mdast': 4.0.4 + github-slugger: 2.0.0 + hosted-git-info: 7.0.2 + mdast-util-to-hast: 13.2.1 + mdast-util-to-string: 4.0.0 + propose: 0.0.5 + trough: 2.2.0 + unified-engine: 11.2.2 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - bluebird + - supports-color + renderkid@3.0.0: dependencies: css-select: 4.3.0