From 6eb597bb92f8513e65d0bb15268cd39219bf8918 Mon Sep 17 00:00:00 2001 From: Victor Solano Date: Wed, 22 Jul 2026 14:18:12 +0200 Subject: [PATCH 1/3] fix(metadata): use short deprecation anchors Assisted-by: OpenAI Codex --- .changeset/short-deprecation-links.md | 5 +++ .../legacy-html/utils/buildContent.mjs | 4 ++- src/generators/metadata/constants.mjs | 3 ++ .../metadata/utils/__tests__/parse.test.mjs | 32 +++++++++++++++++++ src/generators/metadata/utils/parse.mjs | 21 ++++++++++-- 5 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 .changeset/short-deprecation-links.md diff --git a/.changeset/short-deprecation-links.md b/.changeset/short-deprecation-links.md new file mode 100644 index 00000000..8af343c4 --- /dev/null +++ b/.changeset/short-deprecation-links.md @@ -0,0 +1,5 @@ +--- +'@node-core/doc-kit': patch +--- + +Use short `DEP` codes for deprecation heading anchors. diff --git a/src/generators/legacy-html/utils/buildContent.mjs b/src/generators/legacy-html/utils/buildContent.mjs index c22a4396..fd643ab8 100644 --- a/src/generators/legacy-html/utils/buildContent.mjs +++ b/src/generators/legacy-html/utils/buildContent.mjs @@ -29,7 +29,9 @@ const buildHeading = ({ data, children, depth }, index, parent, legacySlug) => { // to be converted into Rehype nodes ...children, // Legacy anchor alias to preserve old external links - createElement('span', createElement(`a#${legacySlug}`)), + ...(legacySlug === data.slug + ? [] + : [createElement('span', createElement(`a#${legacySlug}`))]), // Creates the element that references the link to the heading // (The `#` anchor on the right of each Heading section) createElement( diff --git a/src/generators/metadata/constants.mjs b/src/generators/metadata/constants.mjs index 262e90a0..557b75ad 100644 --- a/src/generators/metadata/constants.mjs +++ b/src/generators/metadata/constants.mjs @@ -12,6 +12,9 @@ export const DOC_API_SLUGS_REPLACEMENTS = [ { from: /^(?!-)(?!-+$).*?(--+)/g, to: '-' }, // Replace multiple hyphens ]; +// Matches Node.js deprecation headings such as "DEP0001: Description". +export const DEPRECATION_HEADING_REGEX = /^(DEP\d+):/; + // These are regular expressions used to determine if a given Markdown heading // is a specific type of API Doc entry (e.g., Event, Class, Method, etc) // and to extract the inner content of said Heading to be used as the API doc entry name diff --git a/src/generators/metadata/utils/__tests__/parse.test.mjs b/src/generators/metadata/utils/__tests__/parse.test.mjs index 4e333ca8..6e614583 100644 --- a/src/generators/metadata/utils/__tests__/parse.test.mjs +++ b/src/generators/metadata/utils/__tests__/parse.test.mjs @@ -69,6 +69,38 @@ describe('parseApiDoc', () => { }); }); + describe('deprecation headings', () => { + it('uses the DEP code as the slug on the deprecations page', () => { + const tree = u('root', [ + h('DEP0001: http.OutgoingMessage.prototype.flush'), + ]); + const [entry] = parseApiDoc({ path: '/deprecations', tree }, typeMap); + + assert.strictEqual(entry.heading.data.slug, 'DEP0001'); + }); + + it('deduplicates repeated DEP code slugs', () => { + const tree = u('root', [ + h('DEP0001: first deprecated API'), + h('DEP0001: second deprecated API'), + ]); + const entries = parseApiDoc({ path: '/deprecations', tree }, typeMap); + + assert.strictEqual(entries[0].heading.data.slug, 'DEP0001'); + assert.strictEqual(entries[1].heading.data.slug, 'DEP0001-1'); + }); + + it('uses normal slugs for DEP headings outside the deprecations page', () => { + const tree = u('root', [h('DEP0190: some section heading')]); + const [entry] = parseApiDoc({ path, tree }, typeMap); + + assert.strictEqual( + entry.heading.data.slug, + 'dep0190-some-section-heading' + ); + }); + }); + describe('YAML metadata', () => { it('extracts added_in', () => { const tree = u('root', [h('fs'), yaml('added: v0.1.0')]); diff --git a/src/generators/metadata/utils/parse.mjs b/src/generators/metadata/utils/parse.mjs index 9f49e0cc..6b7fbfdb 100644 --- a/src/generators/metadata/utils/parse.mjs +++ b/src/generators/metadata/utils/parse.mjs @@ -21,7 +21,10 @@ import { import { UNIST } from '../../../utils/queries/index.mjs'; import { getRemark as remark } from '../../../utils/remark.mjs'; import { relative } from '../../../utils/url.mjs'; -import { IGNORE_STABILITY_STEMS } from '../constants.mjs'; +import { + DEPRECATION_HEADING_REGEX, + IGNORE_STABILITY_STEMS, +} from '../constants.mjs'; import { resolveTypeAnnotations } from './resolveTypes.mjs'; /** @@ -44,6 +47,20 @@ export const parseApiDoc = ({ path, tree, mdx = false }, typeMap) => { // Slug the API (We use a non-class slugger, since we are fairly certain that `path` is unique) const api = slug(path.slice(1).replace(sep, '-')); + /** + * Creates a stable slug for a heading in the current API document. + * @param {string} text Heading text to slug. + * @returns {string} The generated heading slug. + */ + const getHeadingSlug = text => { + const deprecationHeading = + api === 'deprecations' && DEPRECATION_HEADING_REGEX.exec(text); + + return deprecationHeading + ? nodeSlugger.slug(deprecationHeading[1]).toUpperCase() + : nodeSlugger.slug(text); + }; + // Get all Markdown Footnote definitions from the tree const markdownDefinitions = selectAll('definition', tree); @@ -96,7 +113,7 @@ export const parseApiDoc = ({ path, tree, mdx = false }, typeMap) => { }); // Generate slug and update heading data - metadata.heading.data.slug = nodeSlugger.slug(metadata.heading.data.text); + metadata.heading.data.slug = getHeadingSlug(metadata.heading.data.text); // Find the next heading to determine section boundaries const nextHeadingNode = From b1df29b4da9f94ff2ea717269b152eb1e9e12918 Mon Sep 17 00:00:00 2001 From: Victor Solano Date: Thu, 23 Jul 2026 08:02:25 +0200 Subject: [PATCH 2/3] fix(metadata): address deprecation anchor review Add regression coverage for matching primary and legacy anchors. Assisted-by: OpenAI Codex Signed-off-by: Victor Solano --- .../utils/__tests__/buildContent.test.mjs | 42 +++++++++++++++++++ src/generators/metadata/utils/parse.mjs | 5 +-- 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 src/generators/legacy-html/utils/__tests__/buildContent.test.mjs diff --git a/src/generators/legacy-html/utils/__tests__/buildContent.test.mjs b/src/generators/legacy-html/utils/__tests__/buildContent.test.mjs new file mode 100644 index 00000000..ac40da98 --- /dev/null +++ b/src/generators/legacy-html/utils/__tests__/buildContent.test.mjs @@ -0,0 +1,42 @@ +'use strict'; + +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; + +import { setConfig } from '../../../../utils/configuration/index.mjs'; +import buildContent from '../buildContent.mjs'; + +const createEntry = slug => { + const text = 'DEP0001: deprecated API'; + const heading = { + type: 'heading', + depth: 3, + children: [{ type: 'text', value: text }], + data: { name: text, text, slug }, + }; + + return { + api: 'deprecations', + path: '/deprecations', + basename: 'deprecations', + heading, + content: { type: 'root', children: [heading] }, + }; +}; + +describe('buildContent', () => { + before(() => setConfig({})); + + it('does not duplicate a legacy anchor matching the primary slug', () => { + const output = buildContent([], [createEntry('DEP0001')]); + + assert.strictEqual((output.match(/id="DEP0001"/g) ?? []).length, 1); + }); + + it('preserves a legacy anchor that differs from the primary slug', () => { + const output = buildContent([], [createEntry('dep0001-deprecated-api')]); + + assert.match(output, /id="DEP0001"/); + assert.match(output, /id="dep0001-deprecated-api"/); + }); +}); diff --git a/src/generators/metadata/utils/parse.mjs b/src/generators/metadata/utils/parse.mjs index 6b7fbfdb..543a4863 100644 --- a/src/generators/metadata/utils/parse.mjs +++ b/src/generators/metadata/utils/parse.mjs @@ -53,10 +53,9 @@ export const parseApiDoc = ({ path, tree, mdx = false }, typeMap) => { * @returns {string} The generated heading slug. */ const getHeadingSlug = text => { - const deprecationHeading = - api === 'deprecations' && DEPRECATION_HEADING_REGEX.exec(text); + const deprecationHeading = DEPRECATION_HEADING_REGEX.exec(text); - return deprecationHeading + return api === 'deprecations' && deprecationHeading ? nodeSlugger.slug(deprecationHeading[1]).toUpperCase() : nodeSlugger.slug(text); }; From a7ee557d5d7665c952ffaada791174720885346f Mon Sep 17 00:00:00 2001 From: Victor Solano Date: Thu, 23 Jul 2026 08:23:18 +0200 Subject: [PATCH 3/3] fix(metadata): address Bugbot review Share the deprecation heading matcher and classify the published fragment change as minor. Assisted-by: OpenAI Codex Signed-off-by: Victor Solano --- .changeset/short-deprecation-links.md | 2 +- src/generators/legacy-html/constants.mjs | 5 ----- src/generators/legacy-html/utils/slugger.mjs | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 src/generators/legacy-html/constants.mjs diff --git a/.changeset/short-deprecation-links.md b/.changeset/short-deprecation-links.md index 8af343c4..e75e9b3a 100644 --- a/.changeset/short-deprecation-links.md +++ b/.changeset/short-deprecation-links.md @@ -1,5 +1,5 @@ --- -'@node-core/doc-kit': patch +'@node-core/doc-kit': minor --- Use short `DEP` codes for deprecation heading anchors. diff --git a/src/generators/legacy-html/constants.mjs b/src/generators/legacy-html/constants.mjs deleted file mode 100644 index fe07c6e1..00000000 --- a/src/generators/legacy-html/constants.mjs +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -// Matches deprecation headings (e.g., "DEP0001: some title") and captures -// the deprecation code (e.g., "DEP0001") as the first group -export const DEPRECATION_HEADING_REGEX = /^(DEP\d+):/; diff --git a/src/generators/legacy-html/utils/slugger.mjs b/src/generators/legacy-html/utils/slugger.mjs index 7fe769b6..43d805cb 100644 --- a/src/generators/legacy-html/utils/slugger.mjs +++ b/src/generators/legacy-html/utils/slugger.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { DEPRECATION_HEADING_REGEX } from '../constants.mjs'; +import { DEPRECATION_HEADING_REGEX } from '../../metadata/constants.mjs'; /** * Creates a stateful slugger for legacy anchor links.