From 0c89552d795dc5ec9a1033e8745740ec0175b28a Mon Sep 17 00:00:00 2001 From: avivkeller Date: Thu, 31 Jul 2025 17:06:48 -0400 Subject: [PATCH 1/4] feat(remark-lint): add 1 --- .github/workflows/publish-packages.yml | 26 +- apps/site/.remarkrc.json | 20 +- apps/site/package.json | 15 +- .../en/learn/modules/how-to-use-streams.md | 1 + .../en/learn/modules/publishing-a-package.mdx | 10 +- .../pages/id/about/security-reporting.mdx | 2 +- packages/remark-lint/.lintstagedrc.json | 4 + packages/remark-lint/README.md | 123 +++++++++ packages/remark-lint/eslint.config.js | 17 ++ packages/remark-lint/package.json | 59 +++++ packages/remark-lint/src/api.mjs | 67 +++++ packages/remark-lint/src/index.mjs | 76 ++++++ .../duplicate-stability-nodes.test.mjs | 59 +++++ .../__tests__/hashed-self-references.test.mjs | 52 ++++ .../__tests__/ordered-references.test.mjs | 65 +++++ .../__tests__/required-metadata.test.mjs | 50 ++++ .../remark-lint/src/rules/__tests__/utils.mjs | 46 ++++ .../__tests__/yaml/ordered-yaml-keys.test.mjs | 62 +++++ .../__tests__/yaml/validate-changes.test.mjs | 147 +++++++++++ .../__tests__/yaml/validate-versions.test.mjs | 77 ++++++ .../src/rules/duplicate-stability-nodes.mjs | 57 ++++ .../src/rules/hashed-self-reference.mjs | 45 ++++ .../src/rules/ordered-references.mjs | 36 +++ .../src/rules/required-metadata.mjs | 46 ++++ packages/remark-lint/src/rules/yaml/index.mjs | 55 ++++ .../src/rules/yaml/ordered-yaml-keys.mjs | 58 +++++ .../src/rules/yaml/validate-changes.mjs | 93 +++++++ .../src/rules/yaml/validate-versions.mjs | 100 +++++++ packages/remark-lint/turbo.json | 15 ++ pnpm-lock.yaml | 246 +++++++++++------- 30 files changed, 1590 insertions(+), 139 deletions(-) create mode 100644 packages/remark-lint/.lintstagedrc.json create mode 100644 packages/remark-lint/README.md create mode 100644 packages/remark-lint/eslint.config.js create mode 100644 packages/remark-lint/package.json create mode 100644 packages/remark-lint/src/api.mjs create mode 100644 packages/remark-lint/src/index.mjs create mode 100644 packages/remark-lint/src/rules/__tests__/duplicate-stability-nodes.test.mjs create mode 100644 packages/remark-lint/src/rules/__tests__/hashed-self-references.test.mjs create mode 100644 packages/remark-lint/src/rules/__tests__/ordered-references.test.mjs create mode 100644 packages/remark-lint/src/rules/__tests__/required-metadata.test.mjs create mode 100644 packages/remark-lint/src/rules/__tests__/utils.mjs create mode 100644 packages/remark-lint/src/rules/__tests__/yaml/ordered-yaml-keys.test.mjs create mode 100644 packages/remark-lint/src/rules/__tests__/yaml/validate-changes.test.mjs create mode 100644 packages/remark-lint/src/rules/__tests__/yaml/validate-versions.test.mjs create mode 100644 packages/remark-lint/src/rules/duplicate-stability-nodes.mjs create mode 100644 packages/remark-lint/src/rules/hashed-self-reference.mjs create mode 100644 packages/remark-lint/src/rules/ordered-references.mjs create mode 100644 packages/remark-lint/src/rules/required-metadata.mjs create mode 100644 packages/remark-lint/src/rules/yaml/index.mjs create mode 100644 packages/remark-lint/src/rules/yaml/ordered-yaml-keys.mjs create mode 100644 packages/remark-lint/src/rules/yaml/validate-changes.mjs create mode 100644 packages/remark-lint/src/rules/yaml/validate-versions.mjs create mode 100644 packages/remark-lint/turbo.json diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index 3d35a9fe01da7..6c6f7e32a7f28 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -74,13 +74,25 @@ jobs: # If a specific package is requested via workflow_dispatch, just publish that one echo "matrix={\"package\":[\"$PACKAGE\"]}" >> $GITHUB_OUTPUT else - # Otherwise, identify all packages with changes since the last commit CHANGED_PACKAGES=() for pkg in $(ls -d packages/*); do PKG_NAME=$(basename "$pkg") - # For manual runs, include all packages. For automatic runs, only include packages with changes + PKG_JSON="$pkg/package.json" + + # Determine if the package has changed (or include all on manual trigger) if [ "$EVENT_NAME" == "workflow_dispatch" ] || ! git diff --quiet $COMMIT_SHA~1 $COMMIT_SHA -- "$pkg/"; then - CHANGED_PACKAGES+=("$PKG_NAME") + HAS_VERSION=$(jq 'has("version")' "$PKG_JSON") + if [ "$HAS_VERSION" == "false" ]; then + # Include packages without version field + CHANGED_PACKAGES+=("$PKG_NAME") + else + # For packages with version field, include only if version changed + OLD_VERSION=$(git show $COMMIT_SHA~1:$PKG_JSON | jq -r '.version') + NEW_VERSION=$(jq -r '.version' "$PKG_JSON") + if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then + CHANGED_PACKAGES+=("$PKG_NAME") + fi + fi fi done @@ -125,8 +137,12 @@ jobs: run: | # Install deps pnpm install --frozen-lockfile - # Create a unique version using the commit SHA as a prerelease identifier - npm version --no-git-tag-version 1.0.1-$COMMIT_SHA + + HAS_VERSION=$(jq 'has("version")' package.json) + if [ "$HAS_VERSION" == "false" ]; then + # Only bump version if package has no version field + npm version --no-git-tag-version 1.0.1-$COMMIT_SHA + fi # Check if a custom publish script exists in package.json if jq -e '.scripts.publish' package.json > /dev/null; then diff --git a/apps/site/.remarkrc.json b/apps/site/.remarkrc.json index 40370dea6d613..71f00f776d25e 100644 --- a/apps/site/.remarkrc.json +++ b/apps/site/.remarkrc.json @@ -1,21 +1,3 @@ { - "settings": { - "bullet": "-", - "resourceLink": true - }, - "plugins": [ - "remark-frontmatter", - "remark-preset-lint-node", - ["remark-gfm", false], - ["remark-lint-fenced-code-flag", false], - ["remark-lint-first-heading-level", false], - ["remark-lint-maximum-line-length", false], - ["remark-lint-no-file-name-articles", false], - ["remark-lint-no-literal-urls", false], - ["remark-lint-no-unused-definitions", false], - ["remark-lint-no-undefined-references", false], - ["remark-lint-prohibited-strings", false], - ["remark-lint-unordered-list-marker-style", "-"], - ["remark-preset-lint-node/remark-lint-nodejs-links.js", false] - ] + "plugins": ["remark-frontmatter", "@node-core/remark-lint"] } diff --git a/apps/site/package.json b/apps/site/package.json index 343abb7f669cb..2c33ea6ee51a3 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -82,6 +82,7 @@ "@eslint/eslintrc": "~3.3.1", "@flarelabs-net/wrangler-build-time-fs-assets-polyfilling": "^0.0.1", "@next/eslint-plugin-next": "15.5.0", + "@node-core/remark-lint": "workspace:*", "@opennextjs/cloudflare": "^1.6.4", "@playwright/test": "^1.54.1", "@testing-library/user-event": "~14.6.1", @@ -95,17 +96,7 @@ "global-jsdom": "^26.0.0", "handlebars": "4.7.8", "jsdom": "^26.0.0", - "remark-frontmatter": "5.0.0", - "remark-lint-fenced-code-flag": "^4.2.0", - "remark-lint-first-heading-level": "^4.0.1", - "remark-lint-maximum-line-length": "^4.1.1", - "remark-lint-no-file-name-articles": "^3.0.1", - "remark-lint-no-literal-urls": "^4.0.1", - "remark-lint-no-undefined-references": "^5.0.2", - "remark-lint-no-unused-definitions": "^4.0.2", - "remark-lint-prohibited-strings": "^4.0.0", - "remark-lint-unordered-list-marker-style": "^4.0.1", - "remark-preset-lint-node": "5.1.2", + "remark-frontmatter": "^5.0.0", "stylelint": "16.23.0", "stylelint-config-standard": "39.0.0", "stylelint-order": "7.0.0", @@ -127,4 +118,4 @@ "./*/index.mjs" ] } -} +} \ No newline at end of file diff --git a/apps/site/pages/en/learn/modules/how-to-use-streams.md b/apps/site/pages/en/learn/modules/how-to-use-streams.md index a98b48bf15a98..f046574e21ef8 100644 --- a/apps/site/pages/en/learn/modules/how-to-use-streams.md +++ b/apps/site/pages/en/learn/modules/how-to-use-streams.md @@ -815,6 +815,7 @@ This work is derived from content published by [Matteo Collina][] in [Platformat [`on('close')`]: https://nodejs.org/api/stream.html#event-close_1 [`on('error')`]: https://nodejs.org/api/stream.html#event-error_1 [`.read()`]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_read_size +[`_read()`]: https://nodejs.org/api/stream.html#readable_readsize [`.write()`]: https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback [`_write`]: https://nodejs.org/api/stream.html#writable_writechunk-encoding-callback [`.end()`]: https://nodejs.org/api/stream.html#writableendchunk-encoding-callback diff --git a/apps/site/pages/en/learn/modules/publishing-a-package.mdx b/apps/site/pages/en/learn/modules/publishing-a-package.mdx index 9d5299d89a7c0..98e1b4423aeba 100644 --- a/apps/site/pages/en/learn/modules/publishing-a-package.mdx +++ b/apps/site/pages/en/learn/modules/publishing-a-package.mdx @@ -6,7 +6,7 @@ authors: JakobJingleheimer # Publishing a package -All the provided `package.json` configurations (not specifically marked “does not work”) work in Node.js 12.22.x (v12 latest, the oldest supported line) and 17.2.0 [current latest at the time][^1], and for grins, with webpack 5.53.0 and 5.63.0 respectively. These are available: [JakobJingleheimer/nodejs-module-config-examples](https://github.com/JakobJingleheimer/nodejs-module-config-examples). +All the provided `package.json` configurations (not specifically marked “does not work”) work in Node.js 12.22.x (v12 latest, the oldest supported line) and 17.2.0 current latest at the time[^1], and for grins, with webpack 5.53.0 and 5.63.0 respectively. These are available: [JakobJingleheimer/nodejs-module-config-examples](https://github.com/JakobJingleheimer/nodejs-module-config-examples). For curious cats, [How did we get here](#how-did-we-get-here) and [Down the rabbit-hole](#down-the-rabbit-hole) provide background and deeper explanations. @@ -337,7 +337,7 @@ We're not in Kansas anymore, Toto. The configurations (there are 2 options) are nearly the same as [ESM source and both CJS & ESM distribution](#esm-source-and-both-cjs-amp-esm-distribution), just exclude `packageJson.exports.import`. -💡 Using [`"type": "module"`][^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbit-hole) and [Gotchas](#gotchas) below. +💡 Using `"type": "module"`[^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbit-hole) and [Gotchas](#gotchas) below. **Working example**: [esm-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/cjs-distro) @@ -386,7 +386,7 @@ If your files explicitly _all_ use `.cjs` and/or `.mjs` file extensions (none us } ``` -💡 Using [`"type": "module"`][^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbit-hole) and [Gotchas](#gotchas) below. +💡 Using `"type": "module"`[^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbit-hole) and [Gotchas](#gotchas) below. #### Publish a CJS distribution with an ESM wrapper @@ -426,7 +426,7 @@ This is also almost identical to the [CJS source and dual distribution using an } ``` -💡 Using [`"type": "module"`][^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbit-hole) and [Gotchas](#gotchas) below. +💡 Using `"type": "module"`[^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbit-hole) and [Gotchas](#gotchas) below. #### Publish both full CJS & ESM distributions @@ -498,7 +498,7 @@ Alternatively, you can use `"default"` and `"node"` keys, which are less counter } ``` -💡 Using [`"type": "module"`][^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbit-hole) and [Gotchas](#gotchas) below. +💡 Using `"type": "module"`[^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbit-hole) and [Gotchas](#gotchas) below. ##### Use the `.mjs` (or equivalent) file extension for all source code files diff --git a/apps/site/pages/id/about/security-reporting.mdx b/apps/site/pages/id/about/security-reporting.mdx index 396acfce25eb6..307463d9aeb0a 100644 --- a/apps/site/pages/id/about/security-reporting.mdx +++ b/apps/site/pages/id/about/security-reporting.mdx @@ -46,7 +46,7 @@ Pemberitahuan keamanan akan didistribusikan melalui metode berikut. ## Komentar tentang kebijakan ini -Jika Anda memiliki saran tentang bagaimana proses ini dapat ditingkatkan, silakan kirimkan [permintaan penarikan](https://github.com/nodejs/nodejs.org) atau [ajukan masalah (https://github.com/nodejs/security -wg/issues/new) untuk didiskusikan. +Jika Anda memiliki saran tentang bagaimana proses ini dapat ditingkatkan, silakan kirimkan [permintaan penarikan](https://github.com/nodejs/nodejs.org) atau [ajukan masalah](https://github.com/nodejs/security-wg/issues/new) untuk didiskusikan. ## Praktik Terbaik OpenSSF diff --git a/packages/remark-lint/.lintstagedrc.json b/packages/remark-lint/.lintstagedrc.json new file mode 100644 index 0000000000000..1d1673ffabe4d --- /dev/null +++ b/packages/remark-lint/.lintstagedrc.json @@ -0,0 +1,4 @@ +{ + "**/*.{js,mjs,ts,tsx,md,mdx}": ["prettier --check --write", "eslint --fix"], + "**/*.{json,yml}": ["prettier --check --write"] +} diff --git a/packages/remark-lint/README.md b/packages/remark-lint/README.md new file mode 100644 index 0000000000000..3097a32207fa7 --- /dev/null +++ b/packages/remark-lint/README.md @@ -0,0 +1,123 @@ +# `@node-core/remark-lint` + +A [`remark-lint`](https://github.com/remarkjs/remark-lint) plugin with configurations tailored to the documentation and contribution standards of the [Node.js GitHub Organization](https://github.com/nodejs). + +## Installation + +```bash +npm install --save-dev @node-core/remark-lint +``` + +## Usage + +Add the plugin to your `.remarkrc` or `remark.config.js`: + +```json +{ + "plugins": ["@node-core/remark-lint"] +} +``` + +Run remark to lint your Markdown files: + +```bash +npx remark . --frail +``` + +## Configuration + +### Released Versions + +Some rules, such as `node-core:yaml-comments`, validate version references against known released Node.js versions. You can provide these using the `releasedVersions` option: + +```json +{ + "plugins": [ + [ + "@node-core/remark-lint", + { + "releasedVersions": ["v18.0.0", "v18.1.0", "v18.2.0", "v20.0.0"] + } + ] + ] +} +``` + +For Node.js projects, these versions can be automatically generated [using `list-released-versions-from-changelogs.mjs`](https://github.com/nodejs/node/blob/main/tools/lint-md/list-released-versions-from-changelogs.mjs). + +If not specified, version-related rules will accept any valid SemVer format. + +## Rules + +### `node-core:duplicate-stability-nodes` + +Prevents redundant stability markers in nested sections. + +**Not allowed:** + +```markdown +# Parent Section + +> Stability: 2 - Stable + +## Child Section + +> Stability: 2 - Stable +``` + +### `node-core:hashed-self-reference` + +Ensures self-references use fragment-only links. + +**Allowed:** + +```markdown +See the [Introduction](#introduction) section. +``` + +**Not allowed:** + +```markdown +See the [Introduction](document.md#introduction) section. +``` + +### `node-core:ordered-references` + +Enforces alphabetical sorting of reference-style link definitions. + +**Allowed:** + +```markdown +[api]: https://example.com/api +[docs]: https://example.com/docs +[info]: https://example.com/info +``` + +### `node-core:required-metadata` + +Requires essential metadata for documentation: + +- `llm_description`: A description for Large Language Models (can be inferred from first paragraph) +- `introduced_in`: API introduction version + +Metadata can be provided in comments: + +```markdown + +``` + +### `node-core:yaml-comments` + +Enforces structure and content of YAML comment blocks: + +- `added`: An array of valid version strings +- `napiVersion`: The N-API version +- `deprecated`: An array of valid version strings +- `removed`: An array of valid version strings +- `changes`: An array of: + - `pr-url`: Pull request URL + - `commit`: Commit hash (only required for security fixes) + - `version`: Valid version string + - `description`: Change description + +All version references must be valid SemVer, or match the provided `releasedVersions`. diff --git a/packages/remark-lint/eslint.config.js b/packages/remark-lint/eslint.config.js new file mode 100644 index 0000000000000..205088e6ad6ae --- /dev/null +++ b/packages/remark-lint/eslint.config.js @@ -0,0 +1,17 @@ +import globals from 'globals'; + +import baseConfig from '../../eslint.config.js'; + +export default [ + ...baseConfig, + { + languageOptions: { + globals: globals.nodeBuiltin, + parserOptions: { + // Allow nullish syntax (i.e. "?." or "??"), + // and top-level await + ecmaVersion: 'latest', + }, + }, + }, +]; diff --git a/packages/remark-lint/package.json b/packages/remark-lint/package.json new file mode 100644 index 0000000000000..72753be7fce5e --- /dev/null +++ b/packages/remark-lint/package.json @@ -0,0 +1,59 @@ +{ + "name": "@node-core/remark-lint", + "type": "module", + "version": "1.0.0", + "exports": { + ".": "./src/index.mjs", + "./api": "./src/api.mjs" + }, + "scripts": { + "lint": "node --run lint:js", + "lint:fix": "node --run lint:js:fix", + "lint:js": "eslint \"**/*.mjs\"", + "lint:js:fix": "node --run lint:js -- --fix", + "test": "node --run test:unit", + "test:unit": "cross-env NODE_NO_WARNINGS=1 node --experimental-test-coverage --test \"**/*.test.mjs\"" + }, + "dependencies": { + "remark-gfm": "^4.0.1", + "remark-lint-blockquote-indentation": "^4.0.1", + "remark-lint-checkbox-character-style": "^5.0.1", + "remark-lint-checkbox-content-indent": "^5.0.1", + "remark-lint-code-block-style": "^4.0.1", + "remark-lint-definition-spacing": "^4.0.1", + "remark-lint-fenced-code-flag": "^4.2.0", + "remark-lint-fenced-code-marker": "^4.0.1", + "remark-lint-final-definition": "^4.0.2", + "remark-lint-heading-style": "^4.0.1", + "remark-lint-maximum-line-length": "^4.1.1", + "remark-lint-no-consecutive-blank-lines": "^5.0.1", + "remark-lint-no-file-name-consecutive-dashes": "^3.0.1", + "remark-lint-no-file-name-outer-dashes": "^3.0.1", + "remark-lint-no-heading-indent": "^5.0.1", + "remark-lint-no-literal-urls": "^4.0.1", + "remark-lint-no-multiple-toplevel-headings": "^4.0.1", + "remark-lint-no-shell-dollars": "^4.0.1", + "remark-lint-no-table-indentation": "^5.0.1", + "remark-lint-no-tabs": "^4.0.1", + "remark-lint-no-trailing-spaces": "^3.0.2", + "remark-lint-no-unused-definitions": "^4.0.2", + "remark-lint-prohibited-strings": "^4.0.0", + "remark-lint-rule-style": "^4.0.1", + "remark-lint-strong-marker": "^4.0.1", + "remark-lint-table-cell-padding": "^5.1.1", + "remark-lint-table-pipes": "^5.0.1", + "remark-lint-unordered-list-marker-style": "^4.0.1", + "remark-preset-lint-recommended": "^7.0.1", + "semver": "^7.7.2", + "unified-lint-rule": "^3.0.1", + "unist-util-visit": "^5.0.0", + "yaml": "^2.8.1" + }, + "devDependencies": { + "cross-env": "catalog:", + "dedent": "^1.6.0", + "globals": "^16.3.0", + "remark-parse": "^11.0.0", + "unified": "^11.0.5" + } +} diff --git a/packages/remark-lint/src/api.mjs b/packages/remark-lint/src/api.mjs new file mode 100644 index 0000000000000..962ad03e715a6 --- /dev/null +++ b/packages/remark-lint/src/api.mjs @@ -0,0 +1,67 @@ +import remarkLintFencedCodeFlag from 'remark-lint-fenced-code-flag'; +import remarkLintMaximumLineLength from 'remark-lint-maximum-line-length'; +import remarkLintNoUnusedDefinitions from 'remark-lint-no-unused-definitions'; +import remarkLintProhibitedStrings from 'remark-lint-prohibited-strings'; +import remarkLintUnorderedListMarkerStyle from 'remark-lint-unordered-list-marker-style'; + +import basePreset from './index.mjs'; +import duplicateStabilityNodes from './rules/duplicate-stability-nodes.mjs'; +import hashedSelfReference from './rules/hashed-self-reference.mjs'; +import orderedReferences from './rules/ordered-references.mjs'; +import requiredMetadata from './rules/required-metadata.mjs'; +import yamlComments from './rules/yaml/index.mjs'; + +/** + * @typedef {Object} Options + * @property {Array} releasedVersions The released versions, for validating the YAML + */ + +/** + * @param {Options} options + */ +export default (options = {}) => ({ + settings: { + ...basePreset.settings, + bullet: '*', + }, + plugins: [ + ...basePreset.plugins, + + // Internal Rules + ...[ + duplicateStabilityNodes, + yamlComments, + hashedSelfReference, + orderedReferences, + requiredMetadata, + ].map(plugin => [plugin, options]), + + // External Rules + remarkLintNoUnusedDefinitions, + [remarkLintFencedCodeFlag, { allowEmpty: false }], + [remarkLintMaximumLineLength, 120], + [remarkLintUnorderedListMarkerStyle, '*'], + [ + remarkLintProhibitedStrings, + [ + { yes: 'End-of-Life' }, + { no: 'filesystem', yes: 'file system' }, + { yes: 'GitHub' }, + { no: 'hostname', yes: 'host name' }, + { yes: 'JavaScript' }, + { no: '[Ll]ong[ -][Tt]erm [Ss]upport', yes: 'Long Term Support' }, + { no: 'Node', yes: 'Node.js', ignoreNextTo: '-API' }, + { yes: 'Node.js' }, + { no: 'Node[Jj][Ss]', yes: 'Node.js' }, + { no: "Node\\.js's?", yes: 'the Node.js' }, + { no: '[Nn]ote that', yes: '' }, + { yes: 'RFC' }, + { no: '[Rr][Ff][Cc]\\d+', yes: 'RFC ' }, + { yes: 'TypeScript' }, + { yes: 'Unix' }, + { yes: 'Valgrind' }, + { yes: 'V8' }, + ], + ], + ], +}); diff --git a/packages/remark-lint/src/index.mjs b/packages/remark-lint/src/index.mjs new file mode 100644 index 0000000000000..bad1d1d8cc956 --- /dev/null +++ b/packages/remark-lint/src/index.mjs @@ -0,0 +1,76 @@ +import remarkGfm from 'remark-gfm'; +import remarkLintBlockquoteIndentation from 'remark-lint-blockquote-indentation'; +import remarkLintCheckboxCharacterStyle from 'remark-lint-checkbox-character-style'; +import remarkLintCheckboxContentIndent from 'remark-lint-checkbox-content-indent'; +import remarkLintCodeBlockStyle from 'remark-lint-code-block-style'; +import remarkLintDefinitionSpacing from 'remark-lint-definition-spacing'; +import remarkLintFencedCodeMarker from 'remark-lint-fenced-code-marker'; +import remarkLintFinalDefinition from 'remark-lint-final-definition'; +import remarkLintHeadingStyle from 'remark-lint-heading-style'; +import remarkLintNoConsecutiveBlankLines from 'remark-lint-no-consecutive-blank-lines'; +import remarkLintNoFileNameConsecutiveDashes from 'remark-lint-no-file-name-consecutive-dashes'; +import remarkLintNofileNameOuterDashes from 'remark-lint-no-file-name-outer-dashes'; +import remarkLintNoHeadingIndent from 'remark-lint-no-heading-indent'; +import remarkLintNoLiteralURLs from 'remark-lint-no-literal-urls'; +import remarkLintNoMultipleToplevelHeadings from 'remark-lint-no-multiple-toplevel-headings'; +import remarkLintNoShellDollars from 'remark-lint-no-shell-dollars'; +import remarkLintNoTableIndentation from 'remark-lint-no-table-indentation'; +import remarkLintNoTabs from 'remark-lint-no-tabs'; +import remarkLintNoTrailingSpaces from 'remark-lint-no-trailing-spaces'; +import remarkLintNoUnusedDefinitions from 'remark-lint-no-unused-definitions'; +import remarkLintRuleStyle from 'remark-lint-rule-style'; +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'; + +export default { + settings: { + tightDefinitions: true, + emphasis: '_', + bullet: '-', + rule: '-', + }, + plugins: [ + // Enable GitHub Flavored Markdown + remarkGfm, + + // Base plugins + remarkPresetLintRecommended, + + // Blockquote and list rules + [remarkLintBlockquoteIndentation, 2], + [remarkLintCheckboxCharacterStyle, { checked: 'x', unchecked: ' ' }], + remarkLintCheckboxContentIndent, + + // Code and formatting rules + [remarkLintCodeBlockStyle, 'fenced'], + [remarkLintFencedCodeMarker, '`'], + [remarkLintRuleStyle, '---'], + [remarkLintStrongMarker, '*'], + + // File and filename rules + remarkLintNoFileNameConsecutiveDashes, + remarkLintNofileNameOuterDashes, + + // Heading and link rules + remarkLintFinalDefinition, + [remarkLintNoUnusedDefinitions, false], + [remarkLintNoLiteralURLs, false], + [remarkLintHeadingStyle, 'atx'], + remarkLintNoHeadingIndent, + remarkLintNoMultipleToplevelHeadings, + + // Layout and spacing rules + remarkLintDefinitionSpacing, + remarkLintNoConsecutiveBlankLines, + remarkLintNoTableIndentation, + remarkLintNoTabs, + remarkLintNoTrailingSpaces, + [remarkLintTableCellPadding, 'padded'], + remarkLintTablePipes, + + // Shell and console rules + remarkLintNoShellDollars, + ], +}; diff --git a/packages/remark-lint/src/rules/__tests__/duplicate-stability-nodes.test.mjs b/packages/remark-lint/src/rules/__tests__/duplicate-stability-nodes.test.mjs new file mode 100644 index 0000000000000..1cb1816f2195f --- /dev/null +++ b/packages/remark-lint/src/rules/__tests__/duplicate-stability-nodes.test.mjs @@ -0,0 +1,59 @@ +import { describe, it } from 'node:test'; + +import dedent from 'dedent'; + +import duplicateStabilityNodes from '../duplicate-stability-nodes.mjs'; +import { testRule } from './utils.mjs'; + +const testCases = [ + { + name: 'no stability nodes', + input: dedent` + # Heading 1 + + > No stability here. + `, + expected: [], + }, + { + name: 'single stability blockquote', + input: dedent` + # Heading 1 + + > Stability: 1.0 + `, + expected: [], + }, + { + name: 'different stabilities under different headings', + input: dedent` + # Heading 1 + + > Stability: 1.0 + + ## Heading 2 + + > Stability: 2.0 + `, + expected: [], + }, + { + name: 'duplicate stability at deeper heading triggers message', + input: dedent` + # Heading 1 + + > Stability: 1.0 + + ## Heading 2 + + > Stability: 1.0 + `, + expected: ['Duplicate stability node'], + }, +]; + +describe('duplicate-stability-nodes', () => { + for (const { name, input, expected } of testCases) { + it(name, () => testRule(duplicateStabilityNodes, input, expected)); + } +}); diff --git a/packages/remark-lint/src/rules/__tests__/hashed-self-references.test.mjs b/packages/remark-lint/src/rules/__tests__/hashed-self-references.test.mjs new file mode 100644 index 0000000000000..70a884ca6be45 --- /dev/null +++ b/packages/remark-lint/src/rules/__tests__/hashed-self-references.test.mjs @@ -0,0 +1,52 @@ +import { describe, it } from 'node:test'; + +import hashedSelfReference from '../hashed-self-reference.mjs'; +import { testRule } from './utils.mjs'; + +const testCases = [ + { + name: 'ignores links to other paths', + input: '[external](./other.md)', + path: 'docs/current.md', + expected: [], + }, + { + name: 'accepts hashed self-reference', + input: '[header](#my-heading)', + path: 'docs/current.md', + expected: [], + }, + { + name: 'reports self-reference with fragment', + input: '[bad link](./current.md#section)', + path: 'docs/current.md', + expected: [ + 'Self-reference must start with hash (expected "#section", got "./current.md#section")', + ], + }, + { + name: 'reports self-reference to path only', + input: '[bad link](./current.md)', + path: 'docs/current.md', + expected: [ + 'Self-reference must start with hash (expected "#", got "./current.md")', + ], + }, + { + name: 'ignores external links', + input: '[nodejs](https://nodejs.org)', + path: 'docs/current.md', + expected: [], + }, +]; + +describe('hashed-self-references', () => { + for (const { name, input, expected, ...options } of testCases) { + it(name, () => + testRule(hashedSelfReference, input, expected, { + ...options, + cwd: process.cwd(), + }) + ); + } +}); diff --git a/packages/remark-lint/src/rules/__tests__/ordered-references.test.mjs b/packages/remark-lint/src/rules/__tests__/ordered-references.test.mjs new file mode 100644 index 0000000000000..0205fc13730d2 --- /dev/null +++ b/packages/remark-lint/src/rules/__tests__/ordered-references.test.mjs @@ -0,0 +1,65 @@ +import { describe, it } from 'node:test'; + +import dedent from 'dedent'; + +import orderedReferences from '../ordered-references.mjs'; +import { testRule } from './utils.mjs'; + +const testCases = [ + { + name: 'no definitions', + input: 'Just some text.', + expected: [], + }, + { + name: 'single definition', + input: '[foo]: https://example.com', + expected: [], + }, + { + name: 'ordered references', + input: dedent` + [alpha]: https://example.com/a + [beta]: https://example.com/b + [charlie]: https://example.com/c + `, + expected: [], + }, + { + name: 'unordered references (simple case)', + input: dedent` + [beta]: https://example.com/b + [alpha]: https://example.com/a + `, + expected: ['Unordered reference ("alpha" should be before "beta")'], + }, + { + name: 'unordered references (deep nesting)', + input: dedent` + [foo]: https://example.com/z + + > Note: + + > [bar]: https://example.com/a + `, + expected: ['Unordered reference ("bar" should be before "foo")'], + }, + { + name: 'multiple unordered references', + input: dedent` + [zulu]: https://z.com + [yankee]: https://y.com + [alpha]: https://a.com + `, + expected: [ + 'Unordered reference ("yankee" should be before "zulu")', + 'Unordered reference ("alpha" should be before "yankee")', + ], + }, +]; + +describe('hashed-self-references', () => { + for (const { name, input, expected } of testCases) { + it(name, () => testRule(orderedReferences, input, expected)); + } +}); diff --git a/packages/remark-lint/src/rules/__tests__/required-metadata.test.mjs b/packages/remark-lint/src/rules/__tests__/required-metadata.test.mjs new file mode 100644 index 0000000000000..fe03986719d8a --- /dev/null +++ b/packages/remark-lint/src/rules/__tests__/required-metadata.test.mjs @@ -0,0 +1,50 @@ +import { describe, it } from 'node:test'; + +import dedent from 'dedent'; + +import requiredMetadata from '../required-metadata.mjs'; +import { testRule } from './utils.mjs'; + +const testCases = [ + { + name: 'both metadata comments present', + input: dedent` + + + `, + expected: [], + }, + { + name: 'missing introduced_in', + input: '', + expected: ['Missing "introduced_in" metadata'], + }, + { + name: 'missing llm_description, but paragraph exists', + input: dedent` + + + This is a short description for LLMs. + `, + expected: [], + }, + { + name: 'missing both metadata entries', + input: '', + expected: [ + 'Missing "introduced_in" metadata', + 'Missing "llm_description" metadata', + ], + }, + { + name: 'only paragraph, no comments at all', + input: 'This is just a paragraph, nothing else.', + expected: ['Missing "introduced_in" metadata'], + }, +]; + +describe('duplicate-stability-nodes', () => { + for (const { name, input, expected } of testCases) { + it(name, () => testRule(requiredMetadata, input, expected)); + } +}); diff --git a/packages/remark-lint/src/rules/__tests__/utils.mjs b/packages/remark-lint/src/rules/__tests__/utils.mjs new file mode 100644 index 0000000000000..ab0e373bac801 --- /dev/null +++ b/packages/remark-lint/src/rules/__tests__/utils.mjs @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict'; +import { mock } from 'node:test'; + +import remarkParse from 'remark-parse'; +import { unified } from 'unified'; + +/** + * Tests a markdown rule against a markdown string + */ +export const testRule = (rule, markdown, expected, vfileOptions = {}) => { + // Parse the markdown once + const tree = unified().use(remarkParse).parse(markdown); + + // Create a mock vfile + const vfile = { + ...vfileOptions, + message: mock.fn(), + messages: [], + }; + + // Execute the rule + rule()(tree, vfile, () => {}); + + // Assert that the expected messages were reported + assert.deepEqual( + vfile.message.mock.calls.map(call => call.arguments[0]), + expected + ); +}; + +/** + * Tests a YAML rule against a YAML string + */ +export function testYamlRule(rule, input, options = {}, expected) { + // Create a mock reporter + const report = mock.fn(); + + // Execute the rule + rule(input, report, options); + + // Assert that the expected messages were reported + assert.deepEqual( + report.mock.calls.flatMap(call => call.arguments), + expected + ); +} diff --git a/packages/remark-lint/src/rules/__tests__/yaml/ordered-yaml-keys.test.mjs b/packages/remark-lint/src/rules/__tests__/yaml/ordered-yaml-keys.test.mjs new file mode 100644 index 0000000000000..44d7d131f708a --- /dev/null +++ b/packages/remark-lint/src/rules/__tests__/yaml/ordered-yaml-keys.test.mjs @@ -0,0 +1,62 @@ +import { describe, it } from 'node:test'; + +import validateKeys from '../../yaml/ordered-yaml-keys.mjs'; +import { testYamlRule } from '../utils.mjs'; + +const testCases = [ + { + name: 'does not report when keys are valid and in correct order', + input: { + added: 'v1.0.0', + napiVersion: 3, + deprecated: 'v2.0.0', + removed: 'v3.0.0', + changes: [], + }, + expected: [], + }, + { + name: 'reports invalid keys', + input: { + added: 'v1.0.0', + unexpected: true, + oops: false, + }, + expected: ['Invalid key(s) found: unexpected, oops'], + }, + { + name: 'reports out-of-order keys', + input: { + removed: 'v3.0.0', + added: 'v1.0.0', + }, + expected: [ + 'Key "added" is out of order. Expected order: added, napiVersion, deprecated, removed, changes', + ], + }, + { + name: 'reports both invalid and out-of-order keys (first invalid)', + input: { + foo: true, + deprecated: 'v2.0.0', + added: 'v1.0.0', + }, + expected: [ + 'Invalid key(s) found: foo', + 'Key "added" is out of order. Expected order: added, napiVersion, deprecated, removed, changes', + ], + }, + { + name: 'does not report on empty object', + input: {}, + expected: [], + }, +]; + +describe('ordered-yaml-keys', () => { + for (const { name, input, options, expected } of testCases) { + it(name, () => { + testYamlRule(validateKeys, input, options, expected); + }); + } +}); diff --git a/packages/remark-lint/src/rules/__tests__/yaml/validate-changes.test.mjs b/packages/remark-lint/src/rules/__tests__/yaml/validate-changes.test.mjs new file mode 100644 index 0000000000000..03d7b690c4ef3 --- /dev/null +++ b/packages/remark-lint/src/rules/__tests__/yaml/validate-changes.test.mjs @@ -0,0 +1,147 @@ +import { describe, it } from 'node:test'; + +import validateChanges from '../../yaml/validate-changes.mjs'; +import { testYamlRule } from '../utils.mjs'; + +const testCases = [ + { + name: 'valid change entry', + input: { + changes: [ + { + version: 'v2.0.0', + 'pr-url': 'https://github.com/nodejs/node/pull/12345', + description: 'This is a valid change.', + }, + ], + }, + expected: [], + }, + { + name: '"changes" is not an array', + input: { + changes: {}, + }, + expected: ['"changes" must be an Array'], + }, + { + name: 'invalid PR URL', + input: { + changes: [ + { + version: 'v1.0.0', + 'pr-url': 'ftp://example.com/invalid', + description: 'Something happened.', + }, + ], + }, + expected: [ + 'In "changes[0]": "ftp://example.com/invalid" is not a valid PR URL.', + ], + }, + { + name: 'invalid key in change', + input: { + changes: [ + { + version: 'v1.0.0', + 'pr-url': 'https://github.com/nodejs/node/pull/123', + description: 'Change made.', + extra: true, + }, + ], + }, + expected: ['In "changes[0]": Invalid key(s) found: extra'], + }, + { + name: 'invalid version in change', + input: { + changes: [ + { + version: 'foo', + 'pr-url': 'https://github.com/nodejs/node/pull/123', + description: 'Change made.', + }, + ], + }, + expected: ['In "changes[0].version": foo is invalid'], + }, + { + name: 'unsorted versions in change', + input: { + changes: [ + { + version: ['v1.0.0', 'v2.0.0'], + 'pr-url': 'https://github.com/nodejs/node/pull/123', + description: 'Out of order.', + }, + ], + }, + expected: [ + 'In "changes[0].version": Versions are unsorted (should be in descending order)', + ], + }, + { + name: 'empty description', + input: { + changes: [ + { + version: 'v1.0.0', + 'pr-url': 'https://github.com/nodejs/node/pull/123', + description: '', + }, + ], + }, + expected: ['In "changes[0]": Description cannot be empty'], + }, + { + name: 'description missing period', + input: { + changes: [ + { + version: 'v1.0.0', + 'pr-url': 'https://github.com/nodejs/node/pull/123', + description: 'Missing period', + }, + ], + }, + expected: ['In "changes[0]": Description must end with a "."'], + }, + { + name: 'valid security-related change (private PR, valid commit)', + input: { + changes: [ + { + version: 'v1.0.0', + 'pr-url': 'https://github.com/nodejs-private/node-private/pull/999', + // A valid commit _must_ be forty characters + commit: '0'.repeat(40), + description: 'Security fix.', + }, + ], + }, + expected: [], + }, + { + name: 'security-related change with invalid commit', + input: { + changes: [ + { + version: 'v1.0.0', + 'pr-url': 'https://github.com/nodejs-private/node-private/pull/999', + commit: 'invalid_commit', + description: 'Security fix.', + }, + ], + }, + expected: ['In "changes[0]": Invalid commit: "invalid_commit"'], + }, +]; + +describe('validate-changes', () => { + for (const { name, input, options, expected } of testCases) { + it(name, () => { + testYamlRule(validateChanges, input, options, expected); + }); + } +}); diff --git a/packages/remark-lint/src/rules/__tests__/yaml/validate-versions.test.mjs b/packages/remark-lint/src/rules/__tests__/yaml/validate-versions.test.mjs new file mode 100644 index 0000000000000..76718be916166 --- /dev/null +++ b/packages/remark-lint/src/rules/__tests__/yaml/validate-versions.test.mjs @@ -0,0 +1,77 @@ +import { describe, it } from 'node:test'; + +import validateVersionRule from '../../yaml/validate-versions.mjs'; +import { testYamlRule } from '../utils.mjs'; + +const testCases = [ + { + name: 'does not report on valid, sorted versions', + input: { + added: ['v2.0.0', 'v1.0.0'], + removed: 'v3.0.0', + }, + expected: [], + }, + { + name: 'reports invalid version format', + input: { + added: 'foo', + }, + expected: ['In "added": foo is invalid'], + }, + { + name: 'reports unsorted versions', + input: { + deprecated: ['v1.0.0', 'v2.0.0'], + }, + expected: [ + 'In "deprecated": Versions are unsorted (should be in descending order)', + ], + }, + { + name: 'handles REPLACEME correctly when alone', + input: { + added: 'REPLACEME', + }, + expected: [], + }, + { + name: 'reports REPLACEME when part of multi-entry array', + input: { + removed: ['REPLACEME', 'v1.0.0'], + }, + expected: ['In "removed": REPLACEME is invalid'], + }, + { + name: 'ignores ancient hardcoded versions (e.g. 0.1.0)', + input: { + added: 'v0.1.0', + }, + expected: [], + }, + { + name: 'supports specifying custom versions', + input: { + added: 'vCUSTOM', + }, + options: { + releasedVersions: 'CUSTOM', + }, + expected: [], + }, + { + name: 'does not report when key is missing', + input: { + napiVersion: 4, + }, + expected: [], + }, +]; + +describe('validate-version', () => { + for (const { name, input, options, expected } of testCases) { + it(name, () => { + testYamlRule(validateVersionRule, input, options, expected); + }); + } +}); diff --git a/packages/remark-lint/src/rules/duplicate-stability-nodes.mjs b/packages/remark-lint/src/rules/duplicate-stability-nodes.mjs new file mode 100644 index 0000000000000..0f41b039d145d --- /dev/null +++ b/packages/remark-lint/src/rules/duplicate-stability-nodes.mjs @@ -0,0 +1,57 @@ +import { lintRule } from 'unified-lint-rule'; +import { visit } from 'unist-util-visit'; + +// TODO(@avivkeller): This is re-used from doc-kit +// Regex to match "Stability: " in blockquotes +const STABILITY = /Stability: ([0-5](?:\.[0-3])?)/; + +/** + * Finds and reports duplicate stability nodes + * @type {import('unified-lint-rule').Rule} + */ +const duplicateStabilityNodes = (tree, vfile) => { + let currentDepth = 0; + let currentStability = -1; + let currentHeaderDepth = 0; + + visit(tree, node => { + // Update the current heading depth whenever a heading node is encountered + if (node.type === 'heading') { + currentHeaderDepth = node.depth; + } + + // Look for blockquotes which may contain stability indicators + if (node.type === 'blockquote') { + // Assume the first child is a paragraph + const paragraph = node.children?.[0]; + // And the first child of that paragraph is text + const text = paragraph?.children?.[0]; + + // Ensure structure is paragraph > text + if (paragraph?.type === 'paragraph' && text?.type === 'text') { + // Try to match "Stability: X" + const match = text.value.match(STABILITY); + if (match) { + const stability = parseFloat(match[1]); + // If the heading got deeper, and stability is valid and matches previous, report a duplicate + if ( + currentHeaderDepth > currentDepth && + stability >= 0 && + stability === currentStability + ) { + vfile.message('Duplicate stability node', node); + } else { + // Otherwise, record this stability and heading depth + currentDepth = currentHeaderDepth; + currentStability = stability; + } + } + } + } + }); +}; + +export default lintRule( + 'node-core:duplicate-stability-nodes', + duplicateStabilityNodes +); diff --git a/packages/remark-lint/src/rules/hashed-self-reference.mjs b/packages/remark-lint/src/rules/hashed-self-reference.mjs new file mode 100644 index 0000000000000..789c41cab3dd4 --- /dev/null +++ b/packages/remark-lint/src/rules/hashed-self-reference.mjs @@ -0,0 +1,45 @@ +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { lintRule } from 'unified-lint-rule'; + +const getLinksRecursively = function* (node) { + if (node.url) { + yield node; + } + + const { children = [] } = node; + + for (const child of children) { + yield* getLinksRecursively(child); + } +}; + +/** + * Ensures that all self-references begin with `#` + * @type {import('unified-lint-rule').Rule} + */ +const hashedSelfReference = (tree, vfile) => { + const currentFileURL = pathToFileURL( + path.isAbsolute(vfile.path) ? vfile.path : path.join(vfile.cwd, vfile.path) + ); + + for (const node of getLinksRecursively(tree)) { + const { url } = node; + + if (!url || url[0] === '#') continue; + + const targetURL = new URL(url, currentFileURL); + + if (targetURL.pathname === currentFileURL.pathname) { + const expected = url.includes('#') ? url.slice(url.indexOf('#')) : '#'; + + vfile.message( + `Self-reference must start with hash (expected "${expected}", got "${url}")`, + node + ); + } + } +}; + +export default lintRule('node-core:hashed-self-reference', hashedSelfReference); diff --git a/packages/remark-lint/src/rules/ordered-references.mjs b/packages/remark-lint/src/rules/ordered-references.mjs new file mode 100644 index 0000000000000..a6366fbdb93f2 --- /dev/null +++ b/packages/remark-lint/src/rules/ordered-references.mjs @@ -0,0 +1,36 @@ +import { lintRule } from 'unified-lint-rule'; + +const getDefinitionsRecursively = function* (node) { + if (node.type === 'definition') { + yield node; + } + + const { children = [] } = node; + + for (const child of children) { + yield* getDefinitionsRecursively(child); + } +}; + +/** + * Ensures references are alphabetical. + * @type {import('unified-lint-rule').Rule} + */ +const orderedReferences = (tree, vfile) => { + let previousLabel; + + for (const node of getDefinitionsRecursively(tree)) { + const { label } = node; + + if (previousLabel && previousLabel > label) { + vfile.message( + `Unordered reference ("${label}" should be before "${previousLabel}")`, + node + ); + } + + previousLabel = label; + } +}; + +export default lintRule('node-core:ordered-references', orderedReferences); diff --git a/packages/remark-lint/src/rules/required-metadata.mjs b/packages/remark-lint/src/rules/required-metadata.mjs new file mode 100644 index 0000000000000..218a44881ec38 --- /dev/null +++ b/packages/remark-lint/src/rules/required-metadata.mjs @@ -0,0 +1,46 @@ +import { lintRule } from 'unified-lint-rule'; +import { visit } from 'unist-util-visit'; + +// Define the required metadata keys that must be present in the Markdown content +const REQUIRED_KEYS = ['introduced_in', 'llm_description']; +const METADATA_CHECKS = REQUIRED_KEYS.map( + key => new RegExp(``) +); + +/** + * Ensures all needed metadata exists + * @type {import('unified-lint-rule').Rule} + */ +const hasRequiredMetadata = (tree, vfile) => { + const foundKeys = new Set(); + let hasParagraph = false; + + visit(tree, ['html', 'paragraph'], node => { + if (node.type === 'html') { + // Check if the HTML node contains any of the required metadata comments + METADATA_CHECKS.forEach((regex, i) => { + if (regex.test(node.value)) { + foundKeys.add(REQUIRED_KEYS[i]); + } + }); + } else { + // Mark that a paragraph exists in the document + hasParagraph = true; + } + }); + + REQUIRED_KEYS.forEach(key => { + if (foundKeys.has(key)) { + return; + } + + // Allow llm_description to be provided as a first paragraph + if (key === 'llm_description' && hasParagraph) { + return; + } + + vfile.message(`Missing "${key}" metadata`, tree); + }); +}; + +export default lintRule('node-core:required-metadata', hasRequiredMetadata); diff --git a/packages/remark-lint/src/rules/yaml/index.mjs b/packages/remark-lint/src/rules/yaml/index.mjs new file mode 100644 index 0000000000000..b1ff0884dcde3 --- /dev/null +++ b/packages/remark-lint/src/rules/yaml/index.mjs @@ -0,0 +1,55 @@ +import { lintRule } from 'unified-lint-rule'; +import { visit } from 'unist-util-visit'; +import { parse } from 'yaml'; + +import orderedYamlKeys from './ordered-yaml-keys.mjs'; +import validateChanges from './validate-changes.mjs'; +import validateVersions from './validate-versions.mjs'; + +const YAML_HTML_COMMENT_RE = /^'; +const RULES = [orderedYamlKeys, validateVersions, validateChanges]; + +/** + * @callback YAMLRule + * @param {Record} yaml - The YAML object to validate. + * @param {(message: string) => void} report - Reporting function + * @param {import('../../api.mjs').Options} options - The options. + */ + +/** + * Determine if a node is a YAML-bearing HTML comment. + * @param {import('unist').Node} node + */ +const isYamlHtmlComment = node => + node.type === 'html' && YAML_HTML_COMMENT_RE.test(node.value); + +/** + * Lints YAML embedded inside HTML comments in Markdown AST. + * @type {import('unified-lint-rule').Rule} + */ +const yamlComments = (tree, vfile, options) => { + visit(tree, isYamlHtmlComment, node => { + const trimmed = node.value.trim(); + + // Consistency check for ""', node); + return; + } + + // "#" comments out the first line ("" + const parsed = parse(`#${trimmed.slice(0, -HTML_COMMENT_CLOSE.length)}`); + + const report = (...args) => vfile.message(...args, node); + + RULES.forEach(rule => rule(parsed, report, options)); + }); +}; + +export default lintRule('node-core:yaml-comments', yamlComments); diff --git a/packages/remark-lint/src/rules/yaml/ordered-yaml-keys.mjs b/packages/remark-lint/src/rules/yaml/ordered-yaml-keys.mjs new file mode 100644 index 0000000000000..95a26346db202 --- /dev/null +++ b/packages/remark-lint/src/rules/yaml/ordered-yaml-keys.mjs @@ -0,0 +1,58 @@ +/** + * Default allowed keys and their required order at the top level. + * Order matters for validation. + */ +export const DEFAULT_VALID_KEYS = [ + 'added', + 'napiVersion', + 'deprecated', + 'removed', + 'changes', +]; + +/** + * Validate that: + * - Only valid keys are present + * - Keys appear in the expected order (relative order respected) + * + * @type {import('./index.mjs').YAMLRule} + * @param {readonly string[]} [validKeys=DEFAULT_VALID_KEYS] - Allowed keys in the expected order. + * @param {string} [prefix=''] - Message prefix for context. + */ +export default function orderedYamlKeys( + yaml, + report, + _, + validKeys, + prefix = '' +) { + if (!yaml || typeof yaml !== 'object' || Array.isArray(yaml)) return; + + const keys = Object.keys(yaml); + + // Check for invalid keys + const invalidKeys = keys.filter(key => !validKeys.includes(key)); + if (invalidKeys.length > 0) { + report(`${prefix}Invalid key(s) found: ${invalidKeys.join(', ')}`); + } + + // Check key order + let lastIndex = -1; + for (const key of keys) { + const index = validKeys.indexOf(key); + + if (index === -1) { + // Non-validated keys are ignored for ordering, since + // they were already reported as invalid above + continue; + } + + if (index < lastIndex) { + report( + `${prefix}Key "${key}" is out of order. Expected order: ${validKeys.join(', ')}` + ); + break; + } + lastIndex = index; + } +} diff --git a/packages/remark-lint/src/rules/yaml/validate-changes.mjs b/packages/remark-lint/src/rules/yaml/validate-changes.mjs new file mode 100644 index 0000000000000..03e82ea7075fe --- /dev/null +++ b/packages/remark-lint/src/rules/yaml/validate-changes.mjs @@ -0,0 +1,93 @@ +import orderedYamlKeys from './ordered-yaml-keys.mjs'; +import { validateVersion } from './validate-versions.mjs'; + +const CHANGE_VALID_KEYS = ['version', 'pr-url', 'description']; +const VALID_PR_URL_RE = + /^https:\/\/github\.com\/nodejs(?:-private)?\/node(?:-private)?\/pull\/\d+$/; +const PRIVATE_PR_STARTER = + 'https://github.com/nodejs-private/node-private/pull/'; +const COMMIT_SHA_RE = /^[0-9a-f]{40}$/i; + +/** + * A change is security-related if it references a PR in the private Node.js repo, + * with a valid commit. + * @param {Record} change + * @returns {boolean} + */ +const isSecurityRelated = change => + typeof change?.['pr-url'] === 'string' && + change['pr-url'].startsWith(PRIVATE_PR_STARTER) && + typeof change?.['commit'] === 'string'; + +/** + * Anything below v1.0 is older than this format. + * @param {Record} change + * @returns {boolean} + */ +const isAncient = change => + typeof change?.version === 'string' && change.version.startsWith('v0.'); + +/** + * Validate the "changes" array within the YAML object. + * @type {import('./index.mjs').YAMLRule} + */ +export default function validateChanges({ changes }, report, options) { + if (changes === undefined) { + // Nothing to validate + return; + } + + if (!Array.isArray(changes)) { + report('"changes" must be an Array'); + return; + } + + changes.forEach((change, index) => { + const prefix = `In "changes[${index}]": `; + + if (!change || typeof change !== 'object' || Array.isArray(change)) { + report(`${prefix}Item must be an object`); + } + + // Security-related validations + if (isSecurityRelated(change)) { + const commit = change.commit; + + if (!COMMIT_SHA_RE.test(commit)) { + report(`${prefix}Invalid commit: "${commit}"`); + } + + // Remove the "commit" key so we can validate keys like normal. + delete change.commit; + } + + // For non-ancient entries, validate PR URL, keys, and description presence + if (!isAncient(change)) { + const prUrl = change['pr-url']; + + if (!VALID_PR_URL_RE.test(prUrl)) { + report(`${prefix}"${prUrl}" is not a valid PR URL.`); + } + + // Key validation + orderedYamlKeys(change, report, CHANGE_VALID_KEYS, prefix); + } + + // Version validation + validateVersion( + change.version, + report, + options, + `changes[${index}].version` + ); + + // Description validation + if (typeof change.description !== 'string') { + report(`${prefix}Description must be a string`); + } else if (change.description.trim().length === 0) { + report(`${prefix}Description cannot be empty`); + } else if (!change.description.endsWith('.')) { + report(`${prefix}Description must end with a "."`); + } + }); +} diff --git a/packages/remark-lint/src/rules/yaml/validate-versions.mjs b/packages/remark-lint/src/rules/yaml/validate-versions.mjs new file mode 100644 index 0000000000000..d4dbb2cf8d9fa --- /dev/null +++ b/packages/remark-lint/src/rules/yaml/validate-versions.mjs @@ -0,0 +1,100 @@ +import { valid, parse, gt } from 'semver'; + +const MAX_SAFE_SEMVER = parse( + `${Number.MAX_SAFE_INTEGER}.${Number.MAX_SAFE_INTEGER}.${Number.MAX_SAFE_INTEGER}` +); +const VERSION_KEYS = ['added', 'removed', 'deprecated']; + +/** + * Checks if a version is a placeholder that should be ignored in validation + * @param {string} version - The version string to check + * @param {number} totalVersions - Total number of versions in the array + */ +const isPlaceholder = (version, totalVersions) => + version === 'REPLACEME' && totalVersions === 1; + +/** + * Checks if a version should be ignored in validation (e.g., very old versions) + * @param {string} version - The version string to check + */ +const isIgnoredVersion = version => { + const parsed = parse(version); + return parsed?.major === 0 && parsed.minor < 2; +}; + +/** + * Determines if a version string is valid according to project rules + * @param {string} version - The version string to validate + * @param {number} totalVersions - Total number of versions in the array + * @param {Array} releasedVersions - The released versions + */ +const isValidVersion = (version, totalVersions, releasedVersions) => { + // Special cases that bypass normal validation + if (isPlaceholder(version, totalVersions) || isIgnoredVersion(version)) { + return true; + } + + // Check against known released versions if available + if (releasedVersions.length > 0) { + return releasedVersions.includes(version.replace(/^v/, '')); + } + + // Fall back to semver validation + return Boolean(valid(version)); +}; + +/** + * Converts a version string to a comparable object + * @param {string} version - The version string to convert + */ +const getComparableVersion = version => + version === 'REPLACEME' ? MAX_SAFE_SEMVER : parse(version); + +/** + * Validates a single version field in the YAML + * @type {import('./index.mjs').YAMLRule} + * @param {string} key + */ +export const validateVersion = ( + input, + report, + { releasedVersions = [] }, + key +) => { + const versions = Array.isArray(input) ? input : [input]; + const totalVersions = versions.length; + + // Validate each version individually + versions.forEach(version => { + if (!isValidVersion(version, totalVersions, releasedVersions)) { + report(`In "${key}": ${version} is invalid`); + } + }); + + // Check if versions are sorted in descending order + for (let i = 1; i < totalVersions; i++) { + const prev = getComparableVersion(versions[i - 1]); + const curr = getComparableVersion(versions[i]); + + if (gt(curr, prev)) { + report( + `In "${key}": Versions are unsorted (should be in descending order)` + ); + break; + } + } +}; + +/** + * Validates version fields in a YAML document + * @type {import('./index.mjs').YAMLRule} + */ +const validateVersions = (yaml, report, options) => { + VERSION_KEYS.forEach(key => { + if (yaml[key]) { + validateVersion(yaml[key], report, options, key); + } + }); +}; + +export default validateVersions; diff --git a/packages/remark-lint/turbo.json b/packages/remark-lint/turbo.json new file mode 100644 index 0000000000000..7a34fa4ca9091 --- /dev/null +++ b/packages/remark-lint/turbo.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "lint:js": { + "inputs": ["src/**/*.mjs"] + }, + "lint:fix": { + "cache": false + }, + "test:unit": { + "inputs": ["src/**/*.mjs"] + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3419a7fbb1242..ff8bfe51452b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,6 +211,9 @@ importers: '@next/eslint-plugin-next': specifier: 15.5.0 version: 15.5.0 + '@node-core/remark-lint': + specifier: workspace:* + version: link:../../packages/remark-lint '@opennextjs/cloudflare': specifier: ^1.6.4 version: 1.6.4(wrangler@4.26.1) @@ -251,38 +254,8 @@ importers: specifier: ^26.0.0 version: 26.1.0 remark-frontmatter: - specifier: 5.0.0 + specifier: ^5.0.0 version: 5.0.0 - remark-lint-fenced-code-flag: - specifier: ^4.2.0 - version: 4.2.0 - remark-lint-first-heading-level: - specifier: ^4.0.1 - version: 4.0.1 - remark-lint-maximum-line-length: - specifier: ^4.1.1 - version: 4.1.1 - remark-lint-no-file-name-articles: - specifier: ^3.0.1 - version: 3.0.1 - remark-lint-no-literal-urls: - specifier: ^4.0.1 - version: 4.0.1 - remark-lint-no-undefined-references: - specifier: ^5.0.2 - version: 5.0.2 - remark-lint-no-unused-definitions: - specifier: ^4.0.2 - version: 4.0.2 - remark-lint-prohibited-strings: - specifier: ^4.0.0 - version: 4.0.0 - remark-lint-unordered-list-marker-style: - specifier: ^4.0.1 - version: 4.0.1 - remark-preset-lint-node: - specifier: 5.1.2 - version: 5.1.2 stylelint: specifier: 16.23.0 version: 16.23.0(typescript@5.8.3) @@ -351,6 +324,124 @@ importers: specifier: 'catalog:' version: 10.0.0 + packages/remark-lint: + dependencies: + remark-gfm: + specifier: ~4.0.1 + version: 4.0.1 + remark-lint-blockquote-indentation: + specifier: ^4.0.1 + version: 4.0.1 + remark-lint-checkbox-character-style: + specifier: ^5.0.1 + version: 5.0.1 + remark-lint-checkbox-content-indent: + specifier: ^5.0.1 + version: 5.0.1 + remark-lint-code-block-style: + specifier: ^4.0.1 + version: 4.0.1 + remark-lint-definition-spacing: + specifier: ^4.0.1 + version: 4.0.1 + remark-lint-fenced-code-flag: + specifier: ^4.2.0 + version: 4.2.0 + remark-lint-fenced-code-marker: + specifier: ^4.0.1 + version: 4.0.1 + remark-lint-final-definition: + specifier: ^4.0.2 + version: 4.0.2 + remark-lint-heading-style: + specifier: ^4.0.1 + version: 4.0.1 + remark-lint-maximum-line-length: + specifier: ^4.1.1 + version: 4.1.1 + remark-lint-no-consecutive-blank-lines: + specifier: ^5.0.1 + version: 5.0.1 + remark-lint-no-file-name-consecutive-dashes: + specifier: ^3.0.1 + version: 3.0.1 + remark-lint-no-file-name-outer-dashes: + specifier: ^3.0.1 + version: 3.0.1 + remark-lint-no-heading-indent: + specifier: ^5.0.1 + version: 5.0.1 + remark-lint-no-literal-urls: + specifier: ^4.0.1 + version: 4.0.1 + remark-lint-no-multiple-toplevel-headings: + specifier: ^4.0.1 + version: 4.0.1 + remark-lint-no-shell-dollars: + specifier: ^4.0.1 + version: 4.0.1 + remark-lint-no-table-indentation: + specifier: ^5.0.1 + version: 5.0.1 + remark-lint-no-tabs: + specifier: ^4.0.1 + version: 4.0.1 + remark-lint-no-trailing-spaces: + specifier: ^3.0.2 + version: 3.0.2 + remark-lint-no-unused-definitions: + specifier: ^4.0.2 + version: 4.0.2 + remark-lint-prohibited-strings: + specifier: ^4.0.0 + version: 4.0.0 + remark-lint-rule-style: + specifier: ^4.0.1 + version: 4.0.1 + remark-lint-strong-marker: + specifier: ^4.0.1 + version: 4.0.1 + remark-lint-table-cell-padding: + specifier: ^5.1.1 + version: 5.1.1 + remark-lint-table-pipes: + specifier: ^5.0.1 + version: 5.0.1 + remark-lint-unordered-list-marker-style: + specifier: ^4.0.1 + version: 4.0.1 + remark-preset-lint-recommended: + specifier: ^7.0.1 + version: 7.0.1 + semver: + specifier: ~7.7.2 + version: 7.7.2 + unified-lint-rule: + specifier: ^3.0.1 + version: 3.0.1 + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 + yaml: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + cross-env: + specifier: 'catalog:' + version: 10.0.0 + dedent: + specifier: ^1.6.0 + version: 1.6.0 + globals: + specifier: ^16.3.0 + version: 16.3.0 + remark-parse: + specifier: ^11.0.0 + version: 11.0.0 + unified: + specifier: ^11.0.5 + version: 11.0.5 + packages/ui-components: dependencies: '@heroicons/react': @@ -4427,6 +4518,14 @@ packages: babel-plugin-macros: optional: true + dedent@1.6.0: + resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -5205,6 +5304,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@16.3.0: + resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -7016,9 +7119,6 @@ packages: remark-lint-final-newline@3.0.1: resolution: {integrity: sha512-q5diKHD6BMbzqWqgvYPOB8AJgLrMzEMBAprNXjcpKoZ/uCRqly+gxjco+qVUMtMWSd+P+KXZZEqoa7Y6QiOudw==} - remark-lint-first-heading-level@4.0.1: - resolution: {integrity: sha512-ZqH476wQU2rk3L2X1Ef/FsdDZJsSkMqTkEjKyeac/hxnwDZ8ZLYYMmm4UKTgVZTtqFUkNYzgGEPAFXtrppHbJA==} - remark-lint-hard-break-spaces@4.1.1: resolution: {integrity: sha512-AKDPDt39fvmr3yk38OKZEWJxxCOOUBE+96AsBfs+ExS5LW6oLa9041X5ahFDQHvHGzdoremEIaaElursaPEkNg==} @@ -7043,9 +7143,6 @@ packages: remark-lint-no-duplicate-definitions@4.0.1: resolution: {integrity: sha512-Ek+A/xDkv5Nn+BXCFmf+uOrFSajCHj6CjhsHjtROgVUeEPj726yYekDBoDRA0Y3+z+U30AsJoHgf/9Jj1IFSug==} - remark-lint-no-file-name-articles@3.0.1: - resolution: {integrity: sha512-h31ZDDJV2T6g9WLBrXg1CJ1m8M170O/tlDPAEPGCa/rxwKvMcfum4yicaot0ZKbUZ1uEPjVSUPDeo3sU0zciCQ==} - remark-lint-no-file-name-consecutive-dashes@3.0.1: resolution: {integrity: sha512-qGJRZ81sowEjv1dBodbHZ29pDZbrFpxiQQ6gBvkkHkkoYPekdnr8iUxmV38HcqH8+JNW1O4ELr+m71AA9/34Mw==} @@ -7121,10 +7218,6 @@ packages: remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - remark-preset-lint-node@5.1.2: - resolution: {integrity: sha512-ukBPfLqD05AomGL+Z3tbmBCKTaEM+9Dv8Pn0r/0vok8F95Z0wj/AY70cFhm038ID1vKBD07anky11dvigDAHlw==} - engines: {node: '>=18.0.0'} - remark-preset-lint-recommended@7.0.1: resolution: {integrity: sha512-j1CY5u48PtZl872BQ40uWSQMT3R4gXKp0FUgevMu5gW7hFMtvaCiDq+BfhzeR8XKKiW9nIMZGfIMZHostz5X4g==} @@ -8237,6 +8330,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -10283,7 +10381,7 @@ snapshots: express: 5.0.1 path-to-regexp: 6.3.0 urlpattern-polyfill: 10.1.0 - yaml: 2.8.0 + yaml: 2.8.1 transitivePeerDependencies: - aws-crt - supports-color @@ -12912,6 +13010,8 @@ snapshots: dedent@1.5.3: {} + dedent@1.6.0: {} + deep-eql@5.0.2: {} deep-is@0.1.4: {} @@ -14014,6 +14114,8 @@ snapshots: globals@14.0.0: {} + globals@16.3.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -15745,7 +15847,7 @@ snapshots: postcss-load-config@5.1.0(jiti@2.4.2)(postcss@8.5.3)(tsx@4.20.3): dependencies: lilconfig: 3.1.3 - yaml: 2.8.0 + yaml: 2.8.1 optionalDependencies: jiti: 2.4.2 postcss: 8.5.3 @@ -16196,6 +16298,7 @@ snapshots: '@types/mdast': 4.0.4 quotation: 2.0.3 unified-lint-rule: 3.0.1 + optional: true remark-lint-final-definition@4.0.2: dependencies: @@ -16217,15 +16320,6 @@ snapshots: unified-lint-rule: 3.0.1 vfile-location: 5.0.3 - remark-lint-first-heading-level@4.0.1: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-mdx: 3.0.0 - unified-lint-rule: 3.0.1 - unist-util-visit-parents: 6.0.1 - transitivePeerDependencies: - - supports-color - remark-lint-hard-break-spaces@4.1.1: dependencies: '@types/mdast': 4.0.4 @@ -16306,11 +16400,6 @@ snapshots: unist-util-visit-parents: 6.0.1 vfile-message: 4.0.2 - remark-lint-no-file-name-articles@3.0.1: - dependencies: - '@types/mdast': 4.0.4 - unified-lint-rule: 3.0.1 - remark-lint-no-file-name-consecutive-dashes@3.0.1: dependencies: '@types/mdast': 4.0.4 @@ -16517,45 +16606,6 @@ snapshots: transitivePeerDependencies: - supports-color - remark-preset-lint-node@5.1.2: - dependencies: - js-yaml: 4.1.0 - remark-gfm: 4.0.1 - remark-lint-blockquote-indentation: 4.0.1 - remark-lint-checkbox-character-style: 5.0.1 - remark-lint-checkbox-content-indent: 5.0.1 - remark-lint-code-block-style: 4.0.1 - remark-lint-definition-spacing: 4.0.1 - remark-lint-fenced-code-flag: 4.2.0 - remark-lint-fenced-code-marker: 4.0.1 - remark-lint-file-extension: 3.0.1 - remark-lint-final-definition: 4.0.2 - remark-lint-first-heading-level: 4.0.1 - remark-lint-heading-style: 4.0.1 - remark-lint-maximum-line-length: 4.1.1 - remark-lint-no-consecutive-blank-lines: 5.0.1 - remark-lint-no-file-name-articles: 3.0.1 - remark-lint-no-file-name-consecutive-dashes: 3.0.1 - remark-lint-no-file-name-outer-dashes: 3.0.1 - remark-lint-no-heading-indent: 5.0.1 - remark-lint-no-multiple-toplevel-headings: 4.0.1 - remark-lint-no-shell-dollars: 4.0.1 - remark-lint-no-table-indentation: 5.0.1 - remark-lint-no-tabs: 4.0.1 - remark-lint-no-trailing-spaces: 3.0.2 - remark-lint-prohibited-strings: 4.0.0 - remark-lint-rule-style: 4.0.1 - remark-lint-strong-marker: 4.0.1 - remark-lint-table-cell-padding: 5.1.1 - remark-lint-table-pipes: 5.0.1 - remark-lint-unordered-list-marker-style: 4.0.1 - remark-preset-lint-recommended: 7.0.1 - semver: 7.7.2 - unified-lint-rule: 3.0.1 - unist-util-visit: 5.0.0 - transitivePeerDependencies: - - supports-color - remark-preset-lint-recommended@7.0.1: dependencies: remark-lint: 10.0.1 @@ -17494,7 +17544,7 @@ snapshots: vfile-message: 4.0.2 vfile-reporter: 8.1.1 vfile-statistics: 3.0.0 - yaml: 2.8.0 + yaml: 2.8.1 transitivePeerDependencies: - bluebird - supports-color @@ -17991,6 +18041,8 @@ snapshots: yaml@2.8.0: {} + yaml@2.8.1: {} + yargs-parser@21.1.1: {} yargs-parser@22.0.0: {} From 95a2c47dc46d61745f3f348bbc60e36cf6d308ed Mon Sep 17 00:00:00 2001 From: avivkeller Date: Fri, 22 Aug 2025 16:47:20 -0400 Subject: [PATCH 2/4] fixup! --- pnpm-lock.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff8bfe51452b5..e7634528d75b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -327,7 +327,7 @@ importers: packages/remark-lint: dependencies: remark-gfm: - specifier: ~4.0.1 + specifier: ^4.0.1 version: 4.0.1 remark-lint-blockquote-indentation: specifier: ^4.0.1 @@ -414,7 +414,7 @@ importers: specifier: ^7.0.1 version: 7.0.1 semver: - specifier: ~7.7.2 + specifier: ^7.7.2 version: 7.7.2 unified-lint-rule: specifier: ^3.0.1 From 5765590fc0fcdbfad1f3829beed4198152d173f2 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Fri, 22 Aug 2025 16:48:26 -0400 Subject: [PATCH 3/4] fixup! --- apps/site/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/package.json b/apps/site/package.json index 2c33ea6ee51a3..7174e6dbe56f6 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -118,4 +118,4 @@ "./*/index.mjs" ] } -} \ No newline at end of file +} From d8d311c086c8ed486d5bb161b557856714d936d0 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Fri, 22 Aug 2025 16:49:42 -0400 Subject: [PATCH 4/4] fixup! --- packages/remark-lint/src/rules/yaml/ordered-yaml-keys.mjs | 2 +- packages/remark-lint/src/rules/yaml/validate-changes.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remark-lint/src/rules/yaml/ordered-yaml-keys.mjs b/packages/remark-lint/src/rules/yaml/ordered-yaml-keys.mjs index 95a26346db202..9126a4b0959df 100644 --- a/packages/remark-lint/src/rules/yaml/ordered-yaml-keys.mjs +++ b/packages/remark-lint/src/rules/yaml/ordered-yaml-keys.mjs @@ -23,7 +23,7 @@ export default function orderedYamlKeys( yaml, report, _, - validKeys, + validKeys = DEFAULT_VALID_KEYS, prefix = '' ) { if (!yaml || typeof yaml !== 'object' || Array.isArray(yaml)) return; diff --git a/packages/remark-lint/src/rules/yaml/validate-changes.mjs b/packages/remark-lint/src/rules/yaml/validate-changes.mjs index 03e82ea7075fe..c22215ac72b5e 100644 --- a/packages/remark-lint/src/rules/yaml/validate-changes.mjs +++ b/packages/remark-lint/src/rules/yaml/validate-changes.mjs @@ -70,7 +70,7 @@ export default function validateChanges({ changes }, report, options) { } // Key validation - orderedYamlKeys(change, report, CHANGE_VALID_KEYS, prefix); + orderedYamlKeys(change, report, options, CHANGE_VALID_KEYS, prefix); } // Version validation