From f0e907d237ab6ed3c2c3a001048810c30688b76b Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Mon, 20 Jul 2026 20:33:58 -0700 Subject: [PATCH 1/5] chore(remark-lint): improve for node core --- packages/remark-lint/package.json | 3 +- .../remark-lint/src/__tests__/index.test.mjs | 40 ++++++++++ packages/remark-lint/src/index.mjs | 2 + .../__tests__/invalid-type-reference.test.mjs | 45 ++++++++++- .../remark-lint/src/rules/__tests__/utils.mjs | 2 + .../src/rules/invalid-type-reference.mjs | 80 ++++++++++--------- pnpm-lock.yaml | 41 ++++++++++ 7 files changed, 172 insertions(+), 41 deletions(-) create mode 100644 packages/remark-lint/src/__tests__/index.test.mjs 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..38df48a966d79 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}', @@ -51,12 +65,35 @@ const testCases = [ }, }, }, + { + name: 'replaces collapsed links to types', + input: '[`net.Socket`][]\n\n[`net.Socket`]: net.md#class-netsocket', + expected: ['Type reference should use "{}" syntax; saw "[`net.Socket`][]"'], + replacement: '{net.Socket}', + }, + { + name: 'does not replace collapsed links to non-types', + input: '[`not-a-type`][]\n\n[`not-a-type`]: ./example.md', + expected: [], + }, + { + name: 'does not replace collapsed links to API members', + input: + '[`net.createServer()`][] and [`writable.writableLength`][]\n\n' + + '[`net.createServer()`]: net.md#netcreateserveroptions-connectionlistener\n' + + '[`writable.writableLength`]: stream.md#writablewritablelength', + expected: [], + }, ]; 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 From ee06ec6e1fb30472c376b5a1b0ec17a89e13c4ef Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Mon, 20 Jul 2026 20:38:03 -0700 Subject: [PATCH 2/5] fixup! --- .../__tests__/invalid-type-reference.test.mjs | 19 ------------------- 1 file changed, 19 deletions(-) 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 38df48a966d79..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 @@ -65,25 +65,6 @@ const value = {invalid}; }, }, }, - { - name: 'replaces collapsed links to types', - input: '[`net.Socket`][]\n\n[`net.Socket`]: net.md#class-netsocket', - expected: ['Type reference should use "{}" syntax; saw "[`net.Socket`][]"'], - replacement: '{net.Socket}', - }, - { - name: 'does not replace collapsed links to non-types', - input: '[`not-a-type`][]\n\n[`not-a-type`]: ./example.md', - expected: [], - }, - { - name: 'does not replace collapsed links to API members', - input: - '[`net.createServer()`][] and [`writable.writableLength`][]\n\n' + - '[`net.createServer()`]: net.md#netcreateserveroptions-connectionlistener\n' + - '[`writable.writableLength`]: stream.md#writablewritablelength', - expected: [], - }, ]; describe('invalid-type-reference', () => { From 5254431802bfc2f8bfb666e80e6d9ca821c737a9 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Sat, 25 Jul 2026 15:24:26 -0700 Subject: [PATCH 3/5] fixup! --- apps/site/.remarkrc.json | 12 +- apps/site/eslint.config.js | 2 + apps/site/pages/en/about/eol.mdx | 2 +- .../community/building-nodejs-together.md | 13 +- apps/site/pages/en/blog/release/v8.0.0.md | 2 +- .../evolving-the-node-js-brand.md | 2 +- .../february-2016-security-releases.md | 2 +- .../february-2024-security-releases.md | 2 +- .../july-2022-security-releases.md | 2 +- .../june-2023-security-releases.md | 2 +- .../vulnerability/march-2025-ci-incident.md | 2 +- .../october-2023-security-releases.md | 2 +- packages/remark-lint/README.md | 10 + packages/remark-lint/package.json | 5 +- .../remark-lint/src/__tests__/index.test.mjs | 40 ---- packages/remark-lint/src/index.mjs | 5 +- .../rules/__tests__/validate-links.test.mjs | 185 ++++++++++++++++ .../remark-lint/src/rules/validate-links.mjs | 197 ++++++++++++++++++ pnpm-lock.yaml | 44 +--- 19 files changed, 429 insertions(+), 102 deletions(-) delete mode 100644 packages/remark-lint/src/__tests__/index.test.mjs create mode 100644 packages/remark-lint/src/rules/__tests__/validate-links.test.mjs create mode 100644 packages/remark-lint/src/rules/validate-links.mjs diff --git a/apps/site/.remarkrc.json b/apps/site/.remarkrc.json index 71f00f776d25e..6901ebd39278e 100644 --- a/apps/site/.remarkrc.json +++ b/apps/site/.remarkrc.json @@ -1,3 +1,13 @@ { - "plugins": ["remark-frontmatter", "@node-core/remark-lint"] + "plugins": [ + "remark-frontmatter", + "@node-core/remark-lint", + [ + "@node-core/remark-lint/validate-links", + { + "ignoreFiles": ["pages/!(en)/**/*.{md,mdx}"], + "ignoreLinks": ["/learn", "/learn/**/*", "/static/**/*", "/feed/*"] + } + ] + ] } diff --git a/apps/site/eslint.config.js b/apps/site/eslint.config.js index 67130ffb3ae42..7971c2c082de1 100644 --- a/apps/site/eslint.config.js +++ b/apps/site/eslint.config.js @@ -43,6 +43,8 @@ export default baseConfig.concat([ }, { + // TODO: Enable validate-links for translations once their routes and + // anchors are kept in sync with the English content. ...mdx.flat, processor: mdx.createRemarkProcessor({ lintCodeBlocks: true }), rules: { diff --git a/apps/site/pages/en/about/eol.mdx b/apps/site/pages/en/about/eol.mdx index b9b7d42fcc054..7acf2d60646e8 100644 --- a/apps/site/pages/en/about/eol.mdx +++ b/apps/site/pages/en/about/eol.mdx @@ -20,7 +20,7 @@ Major versions of Node.js are released, patched, and designated End-Of-Life on a -[View the Node.js release schedule](/about/releases/). +[View the Node.js release schedule](/about/previous-releases/). ## What Happens When a Release Line Reaches EOL diff --git a/apps/site/pages/en/blog/community/building-nodejs-together.md b/apps/site/pages/en/blog/community/building-nodejs-together.md index 71e2dc06fd694..5459a0310856b 100644 --- a/apps/site/pages/en/blog/community/building-nodejs-together.md +++ b/apps/site/pages/en/blog/community/building-nodejs-together.md @@ -26,17 +26,17 @@ living breathing website whose content is created by our end users and team. The website should be the canonical location for documentation on how to use Node.js, how Node.js works, and how to find out what's going on in the Node community. We have seeded the initial documentation with [how to -contribute](/about/get-involved/contribute/), [who the core team -is](/about/organization/#index_md_technical_steering_committee), +contribute](https://github.com/nodejs/node/blob/main/CONTRIBUTING.md), [who the core team +is](https://github.com/nodejs/node), and some basic documentation of the [project -itself](/about/organization). From there we're looking to +itself](https://github.com/nodejs/TSC). From there we're looking to enable the community to come in and build out the rest of the framework for documentation. One of the key changes here is that we're extending the tools that generate API documentation to work for the website in general. That means the website is now written in markdown. Contributions work with the same -[pull-request](/about/get-involved/contribute/#code-contributions) +[pull-request](https://github.com/nodejs/node/blob/main/CONTRIBUTING.md#pull-requests) way as contributions to Node itself. The intent here is to be able to quickly generate new documentation and improve it with feedback from the community. @@ -56,8 +56,7 @@ Road](http://blog.nodejs.org/2014/06/11/notes-from-the-road/) events there are often questions about what does and doesn't go into core. How the team identifies what those features are and when you decide to integrate them. I've spent a lot of time talking about that but I've also -[added](/about/organization) it to the new documentation on -the site. +added it to the new documentation on the site. It's pretty straight forward, but in short if Node.js needs an interface to provide an abstraction, or if everyone in the community is using the same @@ -144,7 +143,7 @@ responsible for the entirety of that subsystem, but they guide its progress by communicating with end users, reviewing bugs and pull requests, and identifying test cases and consumers of new features. People come and go from the core team, and recently we've added [some -documentation](/about/organization) that describes how you +documentation](https://github.com/nodejs/TSC) that describes how you find your way onto that team. It's based largely around our contribution process. It's not about who you work for, or about who you know, it's about your ability to provide technical improvement to the project itself. diff --git a/apps/site/pages/en/blog/release/v8.0.0.md b/apps/site/pages/en/blog/release/v8.0.0.md index cf7fffad32e31..c9e51ec2f50d1 100644 --- a/apps/site/pages/en/blog/release/v8.0.0.md +++ b/apps/site/pages/en/blog/release/v8.0.0.md @@ -52,7 +52,7 @@ as well. The [N-API](https://nodejs.org/api/n-api.html) is experimental in Node.js 8.0.0, so significant changes in the implementation and API should be expected. Native addon developers are -[encouraged to begin working with the API](/guides/publishing-napi-modules/) +[encouraged to begin working with the API](/learn/modules/publishing-node-api-modules) as soon as possible and to provide feedback that will be necessary to ensure that the new API meets the needs of the ecosystem. diff --git a/apps/site/pages/en/blog/uncategorized/evolving-the-node-js-brand.md b/apps/site/pages/en/blog/uncategorized/evolving-the-node-js-brand.md index 5c6850b914ed9..abbabd65720ce 100644 --- a/apps/site/pages/en/blog/uncategorized/evolving-the-node-js-brand.md +++ b/apps/site/pages/en/blog/uncategorized/evolving-the-node-js-brand.md @@ -30,6 +30,6 @@ We look forward to exploring this visual language as the technology charges into We hope you'll have fun using it. -To download the new logo, visit [nodejs.org/en/about/resources/](/about/resources/). +To download the new logo, visit [our branding page](https://nodejs.org/en/about/branding) ![Tri-color Node](/static/images/blog/uncategorized/evolving-the-node-js-brand/tri-color-node.png) diff --git a/apps/site/pages/en/blog/vulnerability/february-2016-security-releases.md b/apps/site/pages/en/blog/vulnerability/february-2016-security-releases.md index 16944732014a8..49f17ab8d63c0 100644 --- a/apps/site/pages/en/blog/vulnerability/february-2016-security-releases.md +++ b/apps/site/pages/en/blog/vulnerability/february-2016-security-releases.md @@ -23,7 +23,7 @@ Régis Leroy reported defects in Node.js that can make [request smuggling](https While the impact of this vulnerability is application and network dependent, it is likely to be difficult to assess whether a Node.js deployment is vulnerable to attack. We therefore recommend that all users upgrade. - Versions 0.10.x of Node.js are **vulnerable**, please upgrade to [v0.10.42 (Maintenance)](/blog/release/v0.10.42/). -- Versions 0.12.x of Node.js are **vulnerable**, please upgrade to [v0.12.10 (LTS)](blog/release/v0.12.10/). +- Versions 0.12.x of Node.js are **vulnerable**, please upgrade to [v0.12.10 (LTS)](/blog/release/v0.12.10/). - Versions 4.x, including LTS Argon, of Node.js are **vulnerable**, please upgrade to [v4.3.0 "Argon" (LTS)](/blog/release/v4.3.0/). - Versions 5.x of Node.js are **vulnerable**, please upgrade to [v5.6.0 (Stable)](/blog/release/v5.6.0/). diff --git a/apps/site/pages/en/blog/vulnerability/february-2024-security-releases.md b/apps/site/pages/en/blog/vulnerability/february-2024-security-releases.md index ce0ad4201b9e0..4196cd30d2464 100644 --- a/apps/site/pages/en/blog/vulnerability/february-2024-security-releases.md +++ b/apps/site/pages/en/blog/vulnerability/february-2024-security-releases.md @@ -167,6 +167,6 @@ Releases will be available on, or shortly after, Tuesday February 6 2024. ## Contact and future updates -The current Node.js security policy can be found at [https://nodejs.org/security/](/security/). Please follow the process outlined in if you wish to report a vulnerability in Node.js. +Please follow the process outlined in if you wish to report a vulnerability in Node.js. Subscribe to the low-volume announcement-only nodejs-sec mailing list at to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization. diff --git a/apps/site/pages/en/blog/vulnerability/july-2022-security-releases.md b/apps/site/pages/en/blog/vulnerability/july-2022-security-releases.md index e1eeb0d5426d1..349dd6ee7d5f8 100644 --- a/apps/site/pages/en/blog/vulnerability/july-2022-security-releases.md +++ b/apps/site/pages/en/blog/vulnerability/july-2022-security-releases.md @@ -191,7 +191,7 @@ Releases will be available on, or shortly after, Tuesday, July 5th, 2022. However, when details of the OpenSSL defects are released on the 5th, our team will be making a more detailed assessment on the likely severity for Node.js users. -The OpenSSL release may delay the Node.js release date. See [OpenSSL Security Release](#update-04-07-2022-openssl-security-release) +The OpenSSL release may delay the Node.js release date. Please monitor the **nodejs-sec** Google Group for updates, including a decision within 24 hours after the OpenSSL release regarding release timing, and full details of the defects upon eventual release: https://groups.google.com/forum/#!forum/nodejs-sec diff --git a/apps/site/pages/en/blog/vulnerability/june-2023-security-releases.md b/apps/site/pages/en/blog/vulnerability/june-2023-security-releases.md index 5788c64f5460f..e5d5120beecfc 100644 --- a/apps/site/pages/en/blog/vulnerability/june-2023-security-releases.md +++ b/apps/site/pages/en/blog/vulnerability/june-2023-security-releases.md @@ -212,6 +212,6 @@ Releases will be available on, or shortly after, Tuesday June 20 2023. ## Contact and future updates -The current Node.js security policy can be found at [https://nodejs.org/security/](/security/). Please follow the process outlined in if you wish to report a vulnerability in Node.js. +Please follow the process outlined in if you wish to report a vulnerability in Node.js. Subscribe to the low-volume announcement-only nodejs-sec mailing list at to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization. diff --git a/apps/site/pages/en/blog/vulnerability/march-2025-ci-incident.md b/apps/site/pages/en/blog/vulnerability/march-2025-ci-incident.md index fd299e8e69fcf..c96a5d8bf64d5 100644 --- a/apps/site/pages/en/blog/vulnerability/march-2025-ci-incident.md +++ b/apps/site/pages/en/blog/vulnerability/march-2025-ci-incident.md @@ -78,7 +78,7 @@ A full report of this incident will be available forthcoming. We appreciate the ## Contact and future updates -The current Node.js security policy can be found at [https://nodejs.org/security/](/security/). Please follow the process outlined in if you wish to report a vulnerability in Node.js. +Please follow the process outlined in if you wish to report a vulnerability in Node.js. Subscribe to the low-volume announcement-only nodejs-sec mailing list at to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization. diff --git a/apps/site/pages/en/blog/vulnerability/october-2023-security-releases.md b/apps/site/pages/en/blog/vulnerability/october-2023-security-releases.md index f62aa7ffb16f6..43efb697001ce 100644 --- a/apps/site/pages/en/blog/vulnerability/october-2023-security-releases.md +++ b/apps/site/pages/en/blog/vulnerability/october-2023-security-releases.md @@ -122,6 +122,6 @@ Releases will be available on, or shortly after, Friday October 13 2023. ## Contact and future updates -The current Node.js security policy can be found at [https://nodejs.org/security/](/security/). Please follow the process outlined in if you wish to report a vulnerability in Node.js. +Please follow the process outlined in if you wish to report a vulnerability in Node.js. Subscribe to the low-volume announcement-only nodejs-sec mailing list at to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization. diff --git a/packages/remark-lint/README.md b/packages/remark-lint/README.md index dd58bb3658a44..649812956873a 100644 --- a/packages/remark-lint/README.md +++ b/packages/remark-lint/README.md @@ -109,6 +109,16 @@ Enforces alphabetical sorting of reference-style link definitions. [info]: https://example.com/info ``` +### `node-core:validate-links` + +Checks that local links point to existing files and that fragment-only links +point to headings in the current document. Root-relative website routes are +mapped to the configured content and public directories, and route targets do +not need to include a file extension. Directory routes resolve to `index.*`. + +For example, `/about/previous-releases` resolves to +`pages/en/about/previous-releases.mdx`. + ### `node-core:required-metadata` Requires essential metadata for documentation: diff --git a/packages/remark-lint/package.json b/packages/remark-lint/package.json index 4b77244975890..9cd7945bfdafe 100644 --- a/packages/remark-lint/package.json +++ b/packages/remark-lint/package.json @@ -4,7 +4,8 @@ "version": "1.4.0", "exports": { ".": "./src/index.mjs", - "./api": "./src/api.mjs" + "./api": "./src/api.mjs", + "./validate-links": "./src/rules/validate-links.mjs" }, "repository": { "type": "git", @@ -21,6 +22,7 @@ }, "dependencies": { "@node-core/doc-kit": "^1.2.0", + "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.1", "remark-lint-blockquote-indentation": "^4.0.1", "remark-lint-checkbox-character-style": "^5.0.1", @@ -50,7 +52,6 @@ "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 deleted file mode 100644 index 78df4d7f20da2..0000000000000 --- a/packages/remark-lint/src/__tests__/index.test.mjs +++ /dev/null @@ -1,40 +0,0 @@ -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 570dae62844e9..14c1d52f926e2 100644 --- a/packages/remark-lint/src/index.mjs +++ b/packages/remark-lint/src/index.mjs @@ -23,7 +23,8 @@ 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'; + +import validateLinks from './rules/validate-links.mjs'; export default { settings: { @@ -55,7 +56,7 @@ export default { remarkLintNofileNameOuterDashes, // Heading and link rules - remarkValidateLinks, + [validateLinks, { basePaths: ['pages/en', 'pages', 'public', '.'] }], remarkLintFinalDefinition, [remarkLintNoUnusedDefinitions, false], [remarkLintNoLiteralURLs, false], diff --git a/packages/remark-lint/src/rules/__tests__/validate-links.test.mjs b/packages/remark-lint/src/rules/__tests__/validate-links.test.mjs new file mode 100644 index 0000000000000..5c1d1f51d5f0c --- /dev/null +++ b/packages/remark-lint/src/rules/__tests__/validate-links.test.mjs @@ -0,0 +1,185 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { after, before, describe, it } from 'node:test'; + +import remarkParse from 'remark-parse'; +import { unified } from 'unified'; + +import basePreset from '../../index.mjs'; +import validateLinks from '../validate-links.mjs'; + +const processMarkdown = async ( + cwd, + markdown, + options = {}, + filePath = 'pages/en/example.mdx' +) => { + const processor = unified() + .use(remarkParse) + .use(validateLinks, { + basePaths: ['pages/en', 'pages', 'public', '.'], + ...options, + }); + const tree = processor.parse(markdown); + + return new Promise((resolve, reject) => { + processor.run(tree, { cwd, path: filePath }, (error, _, file) => { + if (error) { + reject(error); + } else { + resolve(file); + } + }); + }); +}; + +describe('validate-links', () => { + let cwd; + + before(async () => { + cwd = await mkdtemp(path.join(tmpdir(), 'node-core-validate-links-')); + + await Promise.all([ + mkdir(path.join(cwd, 'pages/en/about'), { recursive: true }), + mkdir(path.join(cwd, 'pages/en/blog/release'), { recursive: true }), + mkdir(path.join(cwd, 'pages/en/guide'), { recursive: true }), + mkdir(path.join(cwd, 'pages/ja/about'), { recursive: true }), + mkdir(path.join(cwd, 'public/static'), { recursive: true }), + ]); + + await Promise.all([ + writeFile(path.join(cwd, 'pages/en/about/previous-releases.mdx'), ''), + writeFile(path.join(cwd, 'pages/en/blog/release/v20.0.0.md'), ''), + writeFile(path.join(cwd, 'pages/en/guide/index.markdown'), ''), + writeFile(path.join(cwd, 'pages/en/related.mdx'), ''), + writeFile(path.join(cwd, 'pages/ja/about/releases.md'), ''), + writeFile(path.join(cwd, 'public/static/logo.svg'), ''), + ]); + }); + + after(() => rm(cwd, { force: true, recursive: true })); + + it('is enabled by the base preset with route mappings', () => { + assert.ok( + basePreset.plugins.some( + plugin => Array.isArray(plugin) && plugin[0] === validateLinks + ) + ); + }); + + it('maps root-relative routes to files regardless of extension', async () => { + const file = await processMarkdown( + cwd, + [ + '[Releases](/about/previous-releases?from=test)', + '[Dotted route](/blog/release/v20.0.0)', + '[Japanese](/ja/about/releases)', + '[Relative](./related)', + ].join('\n\n') + ); + + assert.deepEqual(file.messages, []); + }); + + it('maps directory routes and public assets', async () => { + const file = await processMarkdown( + cwd, + '[Guide](/guide)\n\n![Node.js logo](/static/logo.svg)' + ); + + assert.deepEqual(file.messages, []); + }); + + it('reports missing route targets', async () => { + const file = await processMarkdown(cwd, '[Missing](/about/missing)'); + + assert.equal(file.messages.length, 1); + assert.equal( + file.messages[0].message, + 'Cannot find file for link `/about/missing`' + ); + assert.equal(file.messages[0].source, 'node-core'); + assert.equal(file.messages[0].ruleId, 'validate-links'); + }); + + it('validates headings in the current document', async () => { + const valid = await processMarkdown( + cwd, + [ + '# Existing heading', + '[Existing](#existing%2Dheading)', + '# child_process', + '[Node.js-style slug](#child-process)', + ].join('\n\n') + ); + const invalid = await processMarkdown( + cwd, + '# Existing heading\n\n[Missing](#missing)' + ); + + assert.deepEqual(valid.messages, []); + assert.deepEqual( + invalid.messages.map(message => message.message), + ['Cannot find heading for `#missing`'] + ); + }); + + it('recognizes custom HTML anchors', async () => { + const file = await processMarkdown( + cwd, + '\n\n[Security advisory](#CVE-2016-6304)' + ); + + assert.deepEqual(file.messages, []); + }); + + it('ignores link paths matching configured globs', async () => { + const file = await processMarkdown( + cwd, + [ + '[Learn article](/learn/getting-started?from=test)', + '[Feed](/feed/blog.xml)', + '[Different missing route](/learning)', + ].join('\n\n'), + { ignoreLinks: ['/learn/*', '/feed/*'] } + ); + + assert.deepEqual( + file.messages.map(message => message.message), + ['Cannot find file for link `/learning`'] + ); + }); + + it('skips files matching configured globs', async () => { + const markdown = '[Missing](/about/missing)'; + const options = { ignoreFiles: ['pages/!(en)/**/*.{md,mdx}'] }; + const translation = await processMarkdown( + cwd, + markdown, + options, + 'pages/fr/example.mdx' + ); + const english = await processMarkdown(cwd, markdown, options); + + assert.deepEqual(translation.messages, []); + assert.deepEqual( + english.messages.map(message => message.message), + ['Cannot find file for link `/about/missing`'] + ); + }); + + it('ignores external links', async () => { + const file = await processMarkdown( + cwd, + [ + '[Website](https://nodejs.org)', + '[Protocol-relative](//nodejs.org)', + '[Email](mailto:test@example.com)', + ].join('\n\n') + ); + + assert.deepEqual(file.messages, []); + }); +}); diff --git a/packages/remark-lint/src/rules/validate-links.mjs b/packages/remark-lint/src/rules/validate-links.mjs new file mode 100644 index 0000000000000..5cfade0ecc93c --- /dev/null +++ b/packages/remark-lint/src/rules/validate-links.mjs @@ -0,0 +1,197 @@ +import { readdir } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import createNodeSlugger from '@node-core/doc-kit/src/generators/metadata/utils/slugger.mjs'; +import { toString } from 'mdast-util-to-string'; +import { lintRule } from 'unified-lint-rule'; +import { visit } from 'unist-util-visit'; + +const directoryCache = new Map(); +const urlBase = new URL('https://nodejs.org'); +const htmlTag = /<[a-z][^>]*>/giu; +const htmlIdentifier = + /\s(?:id|name)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/iu; + +const decode = value => { + try { + return decodeURIComponent(value); + } catch { + return value; + } +}; + +const getUrlParts = (value, basePath) => { + try { + const url = new URL(value, pathToFileURL(basePath)); + + if (url.protocol !== 'file:' || url.host) { + return; + } + + return { + hash: decode(url.hash.slice(1)).toLowerCase(), + // Don't use the basePath as the base here, since `file:///repo/path/to/file` -> `/target` + // will not prefix `/repo`. + pathname: decode(new URL(value, urlBase).pathname), + rootRelative: value.startsWith('/'), + targetPath: fileURLToPath(url), + }; + } catch { + return; + } +}; + +const readDirectory = directory => { + let entries = directoryCache.get(directory); + + if (!entries) { + entries = readdir(directory, { withFileTypes: true }).then( + items => { + const files = items + .filter(item => item.isFile()) + .map(item => item.name); + const result = new Map(files.map(name => [name, name])); + + for (const name of files) { + const extension = path.extname(name); + const stem = extension && name.slice(0, -extension.length); + + if (stem && !result.has(stem)) { + result.set(stem, name); + } + } + + return result; + }, + () => new Map() + ); + + directoryCache.set(directory, entries); + } + + return entries; +}; + +const resolveFile = async target => { + const directory = path.dirname(target); + const file = (await readDirectory(directory)).get(path.basename(target)); + + if (file) { + return path.join(directory, file); + } + + const index = (await readDirectory(target)).get('index'); + + return index && path.join(target, index); +}; + +const getHeadings = tree => { + const headings = new Set(); + const slugger = createNodeSlugger(); + const addIdentifier = identifier => { + if (identifier) { + headings.add(String(identifier).toLowerCase()); + } + }; + + visit(tree, node => { + const properties = node.data?.hProperties; + const identifier = + properties?.id || + properties?.name || + node.data?.id || + node.attributes?.find( + item => + (item.name === 'id' || item.name === 'name') && + typeof item.value === 'string' + )?.value; + + if (identifier) { + addIdentifier(identifier); + } else if (node.type === 'heading') { + headings.add(slugger.slug(toString(node))); + } + + if (node.type === 'html') { + for (const [tag] of node.value.matchAll(htmlTag)) { + const match = htmlIdentifier.exec(tag); + + addIdentifier(match?.[1] || match?.[2] || match?.[3]); + } + } + }); + + return headings; +}; + +/** + * @type {import('unified-lint-rule').Rule} + */ +const validateLinks = async (tree, file, options = {}) => { + const basePaths = [options.basePaths || '.'].flat(); + const ignoredFiles = [options.ignoreFiles || []].flat(); + const ignoredLinks = [options.ignoreLinks || []].flat(); + const cwd = path.resolve(file.cwd || process.cwd()); + const currentPath = file.path && path.resolve(cwd, file.path); + + if ( + currentPath && + ignoredFiles.some(pattern => + path.matchesGlob(path.relative(cwd, currentPath), pattern) + ) + ) { + return; + } + + const basePath = currentPath || path.join(cwd, 'index'); + const links = []; + let headings; + + visit(tree, node => { + if ( + (node.type === 'link' || + node.type === 'image' || + node.type === 'definition') && + node.url + ) { + const parts = getUrlParts(node.url, basePath); + + if ( + parts && + !ignoredLinks.some(pattern => path.matchesGlob(parts.pathname, pattern)) + ) { + links.push([node, parts]); + } + } + }); + + for (const [node, parts] of links) { + const candidates = parts.rootRelative + ? basePaths.map(base => path.join(cwd, base, parts.pathname)) + : [parts.targetPath]; + + let match; + + for (const candidate of candidates) { + match = + candidate === currentPath ? currentPath : await resolveFile(candidate); + + if (match) { + break; + } + } + + if (!match) { + file.message(`Cannot find file for link \`${node.url}\``, node); + } else if (node.type !== 'image' && parts.hash && match === currentPath) { + headings ??= getHeadings(tree); + + if (!headings.has(parts.hash)) { + file.message(`Cannot find heading for \`#${parts.hash}\``, node); + } + } + } +}; + +export default lintRule('node-core:validate-links', validateLinks); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a57ef2d9e4a8..69852138e7138 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -555,6 +555,9 @@ importers: '@node-core/doc-kit': specifier: ^1.2.0 version: 1.2.0(@orama/core@1.2.19)(@types/react@19.2.16)(jiti@2.7.0)(postcss@8.5.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + mdast-util-to-string: + specifier: ^4.0.0 + version: 4.0.0 remark-gfm: specifier: ^4.0.1 version: 4.0.1 @@ -642,9 +645,6 @@ 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 @@ -4343,9 +4343,6 @@ 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==} @@ -6614,10 +6611,6 @@ 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'} @@ -7649,9 +7642,6 @@ 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'} @@ -7973,9 +7963,6 @@ 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==} @@ -12811,8 +12798,6 @@ 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': {} @@ -15520,8 +15505,6 @@ snapshots: kleur@4.1.5: {} - levenshtein-edit-distance@1.0.0: {} - levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -16783,10 +16766,6 @@ 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 @@ -17476,23 +17455,6 @@ 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 From 0276f105a6e7b726b98c21f385ec757af27fb581 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Sat, 25 Jul 2026 15:26:31 -0700 Subject: [PATCH 4/5] fixup! --- packages/remark-lint/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/remark-lint/package.json b/packages/remark-lint/package.json index 9cd7945bfdafe..d92456b793252 100644 --- a/packages/remark-lint/package.json +++ b/packages/remark-lint/package.json @@ -4,8 +4,7 @@ "version": "1.4.0", "exports": { ".": "./src/index.mjs", - "./api": "./src/api.mjs", - "./validate-links": "./src/rules/validate-links.mjs" + "./api": "./src/api.mjs" }, "repository": { "type": "git", From e244251d2f95b5cea451d8acedf27d176ce9574c Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Sat, 25 Jul 2026 19:11:23 -0700 Subject: [PATCH 5/5] Update .remarkrc.json Signed-off-by: Aviv Keller --- apps/site/.remarkrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/.remarkrc.json b/apps/site/.remarkrc.json index 6901ebd39278e..da1cf359db1af 100644 --- a/apps/site/.remarkrc.json +++ b/apps/site/.remarkrc.json @@ -3,7 +3,7 @@ "remark-frontmatter", "@node-core/remark-lint", [ - "@node-core/remark-lint/validate-links", + "@node-core/remark-lint/src/rules/validate-links.mjs", { "ignoreFiles": ["pages/!(en)/**/*.{md,mdx}"], "ignoreLinks": ["/learn", "/learn/**/*", "/static/**/*", "/feed/*"]