From f5d6e68f04872cc2e072cd83a3420aaa813b37f6 Mon Sep 17 00:00:00 2001 From: Daniil Gaponov Date: Mon, 13 Jul 2026 14:12:28 +0300 Subject: [PATCH 1/7] fix: resolve more intra-repo doc links and add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review on gravity-ui/uikit#2740: - rewriteDocLinks now handles any repo-relative link, not just README.md targets: bare-folder links (../Portal) resolve to the sibling doc, and links to non-shipped files (../types.ts, unshipped legacy/) are unwrapped to plain text — keeping the "no dead link remains" promise. External URLs and in-page anchors are left untouched. - Skip a README sitting directly at a baseDir (empty name → blank index entry). - Add node:test coverage for cleanMarkdown/extractSummary/extractTitle and an integration test for buildDocs link rewriting, and run it in CI. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 2 + package-lock.json | 504 ++++++++++++++++++ package.json | 3 +- src/buildDocs.ts | 52 +- test/buildDocs.test.ts | 54 ++ test/cleanMarkdown.test.ts | 57 ++ test/fixtures/pkg/docs/guide.md | 3 + test/fixtures/pkg/package.json | 3 + .../pkg/src/components/Alpha/README.md | 15 + .../pkg/src/components/Beta/README.md | 3 + .../pkg/src/components/legacy/Old/README.md | 3 + 11 files changed, 680 insertions(+), 19 deletions(-) create mode 100644 test/buildDocs.test.ts create mode 100644 test/cleanMarkdown.test.ts create mode 100644 test/fixtures/pkg/docs/guide.md create mode 100644 test/fixtures/pkg/package.json create mode 100644 test/fixtures/pkg/src/components/Alpha/README.md create mode 100644 test/fixtures/pkg/src/components/Beta/README.md create mode 100644 test/fixtures/pkg/src/components/legacy/Old/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9fa781..c16be4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,3 +26,5 @@ jobs: run: npm run lint - name: Typecheck run: npm run typecheck + - name: Test + run: npm test diff --git a/package-lock.json b/package-lock.json index 4ca11f9..9d77336 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "nano-staged": "^0.8.0", "prettier": "^3.4.2", "tshy": "^3.0.2", + "tsx": "^4.19.2", "typescript": "^5.7.3" }, "peerDependencies": { @@ -964,6 +965,448 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -3032,6 +3475,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -7090,6 +7575,25 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 322c669..c957fb2 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "prettier": "prettier '**/*.{md,yaml,yml,json}'", "typecheck": "tsc --noEmit", "build": "tshy", - "test": "echo \"No tests\"", + "test": "node --import tsx --test test/*.test.ts", "prepublishOnly": "npm run build" }, "keywords": [ @@ -63,6 +63,7 @@ "nano-staged": "^0.8.0", "prettier": "^3.4.2", "tshy": "^3.0.2", + "tsx": "^4.19.2", "typescript": "^5.7.3" }, "nano-staged": { diff --git a/src/buildDocs.ts b/src/buildDocs.ts index f9344c3..81d8252 100644 --- a/src/buildDocs.ts +++ b/src/buildDocs.ts @@ -116,15 +116,24 @@ export function buildDocs(config: DocsConfig = createDefaultDocsConfig()): Build fs.rmSync(outDir, {recursive: true, force: true}); // Resolve every source's items first, so links between docs can be rewritten - // in the second pass regardless of processing order. + // in the second pass regardless of processing order. README docs are also + // keyed by their folder, so bare-folder links (`../Portal`) resolve too. const listed = sources.map((source) => ({source, items: listSource(rootDir, source)})); - const docMap = new Map(listed.flatMap(({items}) => items.map((it) => [it.source, it.outRel]))); + const docMap = new Map(); + for (const {source, items} of listed) { + for (const item of items) { + docMap.set(item.source, item.outRel); + if (source.kind === 'readme') { + docMap.set(path.dirname(item.source), item.outRel); + } + } + } const sections: DocsSection[] = []; for (const {source, items} of listed) { const entries: DocsIndexEntry[] = []; for (const {source: file, name, outRel} of items) { - const cleaned = rewriteReadmeLinks( + const cleaned = rewriteDocLinks( cleanMarkdown(fs.readFileSync(file, 'utf8')), file, outRel, @@ -204,10 +213,18 @@ function writeDoc(outPath: string, content: string): void { function listSource(rootDir: string, {kind, baseDir, outPrefix, exclude}: DocsSource): DocItem[] { const absBase = path.join(rootDir, baseDir); if (kind === 'readme') { - return findReadmes(absBase, exclude).map((source) => { - const name = path.relative(absBase, path.dirname(source)).split(path.sep).join('/'); - return {source, name, outRel: path.posix.join(outPrefix, `${name}.md`)}; - }); + return ( + findReadmes(absBase, exclude) + .map((source) => { + const name = path + .relative(absBase, path.dirname(source)) + .split(path.sep) + .join('/'); + return {source, name, outRel: path.posix.join(outPrefix, `${name}.md`)}; + }) + // Skip a README sitting directly at baseDir (empty name → blank entry). + .filter((item) => item.name !== '') + ); } return findMarkdown(absBase).map((source) => { const rel = path.relative(absBase, source).split(path.sep).join('/'); @@ -215,33 +232,32 @@ function listSource(rootDir: string, {kind, baseDir, outPrefix, exclude}: DocsSo }); } -// Rewrites intra-repo README.md links so they resolve inside the docs output. -// Source links point at sibling source folders (../CopyToClipboard/README.md); -// here every README maps to a flat .md, so links are recomputed relative -// to the current output file. Links to docs that aren't shipped (e.g. legacy/) -// are unwrapped to plain text so no dead link remains. External URLs are kept. -function rewriteReadmeLinks( +// Rewrites repo-relative links so they resolve inside the docs output. A link to +// another shipped doc — by README path (../Popup/README.md) or by its folder +// (../Popup) — is recomputed relative to the current output file. Any other +// repo-relative link (a source file, an unshipped doc like legacy/) is unwrapped +// to plain text so no dead link remains. External URLs (http:, mailto:, …) and +// in-page anchors are left untouched. +function rewriteDocLinks( markdown: string, source: string, outRel: string, docMap: Map, ): string { return markdown.replace(/\[([^\]]*)\]\(([^)]+)\)/g, (whole, text: string, target: string) => { - if (!/readme\.md/i.test(target)) { + if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith('#')) { return whole; } - if (/^[a-z]+:\/\//i.test(target)) { - return whole; // external URL — leave as-is - } const hashIndex = target.indexOf('#'); const relPath = hashIndex === -1 ? target : target.slice(0, hashIndex); const anchor = hashIndex === -1 ? '' : target.slice(hashIndex); + // Resolve either a direct doc path or a bare folder to its shipped output. const absTarget = path.resolve(path.dirname(source), relPath); const targetOut = docMap.get(absTarget); if (!targetOut) { - return text; // target not shipped — drop the link, keep its text + return text; // not a shipped doc — drop the link, keep its text } let relOut = path.posix.relative(path.posix.dirname(outRel), targetOut); diff --git a/test/buildDocs.test.ts b/test/buildDocs.test.ts new file mode 100644 index 0000000..57233b8 --- /dev/null +++ b/test/buildDocs.test.ts @@ -0,0 +1,54 @@ +import * as assert from 'node:assert/strict'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import {test} from 'node:test'; +import {fileURLToPath} from 'node:url'; + +import {buildDocs, createDefaultDocsConfig} from '../src/buildDocs.js'; + +const fixture = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'pkg'); + +function build() { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gulp-utils-docs-')); + const result = buildDocs({...createDefaultDocsConfig(fixture), outDir}); + const read = (rel: string) => fs.readFileSync(path.join(outDir, rel), 'utf8'); + return {outDir, result, read}; +} + +test('rewrites README-path and bare-folder links to shipped docs', () => { + const alpha = build().read('components/Alpha.md'); + + // ../Beta/README.md#props → ./Beta.md#props + assert.match(alpha, /\[Beta\]\(\.\/Beta\.md#props\)/); + // bare folder ../Beta → ./Beta.md + assert.match(alpha, /\[Beta folder\]\(\.\/Beta\.md\)/); +}); + +test('unwraps links to non-shipped targets, leaving no dead link', () => { + const alpha = build().read('components/Alpha.md'); + + assert.doesNotMatch(alpha, /types\.ts/); // ./types.ts#L1 dropped + assert.match(alpha, /a type/); // link text kept +}); + +test('keeps external URLs and strips service markers', () => { + const alpha = build().read('components/Alpha.md'); + + assert.match(alpha, /\(https:\/\/example\.com\)/); + assert.match(alpha, /^# Alpha$/m); + assert.doesNotMatch(alpha, /SANDBOX|GITHUB_BLOCK/); +}); + +test('excludes legacy and indexes every shipped doc', () => { + const {outDir, result, read} = build(); + + assert.ok(!fs.existsSync(path.join(outDir, 'components', 'legacy'))); + + const index = read('INDEX.md'); + assert.match(index, /\[Alpha\]\(\.\/components\/Alpha\.md\)/); + assert.match(index, /\[Beta\]\(\.\/components\/Beta\.md\)/); + assert.match(index, /\[Theming guide\]\(\.\/guides\/guide\.md\)/); // named by heading + + assert.equal(result.total, 3); // Alpha, Beta, guide (legacy excluded) +}); diff --git a/test/cleanMarkdown.test.ts b/test/cleanMarkdown.test.ts new file mode 100644 index 0000000..00603c4 --- /dev/null +++ b/test/cleanMarkdown.test.ts @@ -0,0 +1,57 @@ +import * as assert from 'node:assert/strict'; +import {test} from 'node:test'; + +import {cleanMarkdown, extractSummary, extractTitle} from '../src/cleanMarkdown.js'; + +test('drops SANDBOX and LANDING blocks but keeps GITHUB_BLOCK content', () => { + const out = cleanMarkdown( + [ + '', + '# Button', + '', + '', + '', + '', + '', + '', + 'Body text.', + '', + ].join('\n'), + ); + + assert.match(out, /^# Button$/m); + assert.match(out, /^Body text\.$/m); + assert.doesNotMatch(out, /SANDBOX|LANDING_BLOCK|GITHUB_BLOCK/); +}); + +test('strips badges and images', () => { + const out = cleanMarkdown('# X\n\n![badge](https://img.shields.io/x.svg)\n\nBody.\n'); + + assert.doesNotMatch(out, /shields\.io/); + assert.match(out, /^Body\.$/m); +}); + +test('extractSummary takes the first prose line after the import fence', () => { + const md = '# Button\n\n```tsx\nimport {Button} from "x";\n```\n\nButtons trigger actions.\n'; + + assert.equal(extractSummary(md), 'Buttons trigger actions.'); +}); + +test('extractSummary is empty when a heading follows the title directly', () => { + const md = '# Alert\n\n```tsx\nimport x\n```\n\n### Theme\n\nnormal.\n'; + + assert.equal(extractSummary(md), ''); +}); + +test('extractSummary unwraps links to plain text', () => { + assert.equal(extractSummary('# A\n\nUses [B](./B.md) here.\n'), 'Uses B here.'); +}); + +test('extractTitle returns the first heading text, else empty', () => { + assert.equal(extractTitle('## Table\n\nbody'), 'Table'); + assert.equal(extractTitle('no heading here'), ''); +}); diff --git a/test/fixtures/pkg/docs/guide.md b/test/fixtures/pkg/docs/guide.md new file mode 100644 index 0000000..2cae637 --- /dev/null +++ b/test/fixtures/pkg/docs/guide.md @@ -0,0 +1,3 @@ +# Theming guide + +How to theme the app. diff --git a/test/fixtures/pkg/package.json b/test/fixtures/pkg/package.json new file mode 100644 index 0000000..9c5ade5 --- /dev/null +++ b/test/fixtures/pkg/package.json @@ -0,0 +1,3 @@ +{ + "name": "@scope/fixture" +} diff --git a/test/fixtures/pkg/src/components/Alpha/README.md b/test/fixtures/pkg/src/components/Alpha/README.md new file mode 100644 index 0000000..850a1b0 --- /dev/null +++ b/test/fixtures/pkg/src/components/Alpha/README.md @@ -0,0 +1,15 @@ + + +# Alpha + + + +```tsx +import {Alpha} from '@scope/fixture'; +``` + +Alpha uses [Beta](../Beta/README.md#props), [Beta folder](../Beta), [a type](./types.ts#L1) and [site](https://example.com). + + diff --git a/test/fixtures/pkg/src/components/Beta/README.md b/test/fixtures/pkg/src/components/Beta/README.md new file mode 100644 index 0000000..0e2f3da --- /dev/null +++ b/test/fixtures/pkg/src/components/Beta/README.md @@ -0,0 +1,3 @@ +# Beta + +Beta description. diff --git a/test/fixtures/pkg/src/components/legacy/Old/README.md b/test/fixtures/pkg/src/components/legacy/Old/README.md new file mode 100644 index 0000000..f6fbeb4 --- /dev/null +++ b/test/fixtures/pkg/src/components/legacy/Old/README.md @@ -0,0 +1,3 @@ +# Old + +Deprecated component that must not ship. From d3c256ad3d38a4a1cd66ea6df133da3190650811 Mon Sep 17 00:00:00 2001 From: Daniil Gaponov Date: Tue, 14 Jul 2026 18:27:01 +0300 Subject: [PATCH 2/7] feat: surface README "For AI agents" section in INDEX.md buildDocs now lifts the package README's `For AI agents` section (cleaned) to the top of the generated INDEX.md, and appends a `Documentation for AI agents` pointer section to that README (idempotently) linking to the generated tree. Adds `extractSection` helper and tests for both behaviors. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 +++ src/buildDocs.ts | 60 ++++++++++++++++++++++++++++--- src/cleanMarkdown.ts | 63 ++++++++++++++++++++++++++++++++ src/index.ts | 2 +- test/buildDocs.test.ts | 74 ++++++++++++++++++++++++++++++++++++++ test/cleanMarkdown.test.ts | 42 +++++++++++++++++++++- 6 files changed, 239 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2d61a41..33da97c 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,7 @@ generated `INDEX.md` is read from that `package.json` — pass them explicitly Pass a custom `DocsConfig` to change the sources, output directory or INDEX section order. Intra-repo `README.md` links are rewritten to resolve inside the output; links to docs that aren't shipped are unwrapped to plain text. + +The package README's `For AI agents` section, when present, is cleaned and placed +at the top of the generated `INDEX.md`. A `Documentation for AI agents` pointer +section is also appended once to that README, linking to the generated tree. diff --git a/src/buildDocs.ts b/src/buildDocs.ts index 81d8252..ebb1a68 100644 --- a/src/buildDocs.ts +++ b/src/buildDocs.ts @@ -1,11 +1,16 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import {cleanMarkdown, extractSummary, extractTitle} from './cleanMarkdown.js'; +import {cleanMarkdown, extractSection, extractSummary, extractTitle} from './cleanMarkdown.js'; // Story/test folders hold Storybook doc pages and fixtures, not API docs. const DEFAULT_EXCLUDE = ['__stories__', '__tests__', '__mocks__', '__snapshots__']; +// Heading in the package README whose content is surfaced at the top of INDEX.md. +const README_AI_SECTION = 'For AI agents'; +// Heading of the pointer section buildDocs keeps in the package README. +const README_DOCS_POINTER = 'Documentation for AI agents'; + export type DocsSourceKind = 'readme' | 'markdown'; export interface DocsSource { @@ -105,6 +110,10 @@ export function createDefaultDocsConfig( * output ships inside the npm tarball so an agent working in a consumer project * reads documentation matching the installed version. * + * The package README's `For AI agents` section (if present) is cleaned and placed + * at the top of the generated `INDEX.md`, and a `Documentation for AI agents` + * pointer section is appended to that README (once) linking to the generated tree. + * * @param config docs config; defaults to {@link createDefaultDocsConfig}(). * @returns the generated sections and the total document count. */ @@ -150,7 +159,13 @@ export function buildDocs(config: DocsConfig = createDefaultDocsConfig()): Build } const outRelToRoot = path.relative(rootDir, outDir).split(path.sep).join('/'); - writeDoc(path.join(outDir, 'INDEX.md'), renderIndex(packageName, outRelToRoot, sections)); + const readmePath = path.join(rootDir, 'README.md'); + const aiSection = readAiSection(readmePath); + writeDoc( + path.join(outDir, 'INDEX.md'), + renderIndex(packageName, outRelToRoot, sections, aiSection), + ); + ensureReadmePointer(readmePath, path.posix.join(outRelToRoot, 'INDEX.md')); const total = sections.reduce((sum, section) => sum + section.entries.length, 0); return {sections, total}; @@ -280,7 +295,12 @@ function renderSection(title: string, entries: DocsIndexEntry[]): string { return `## ${title}\n\n${rows}\n`; } -function renderIndex(packageName: string, outRelToRoot: string, sections: DocsSection[]): string { +function renderIndex( + packageName: string, + outRelToRoot: string, + sections: DocsSection[], + aiSection: string, +): string { // e.g. node_modules/@gravity-ui/uikit/build/docs const installedPath = path.posix.join('node_modules', packageName, outRelToRoot); const header = [ @@ -293,7 +313,39 @@ function renderIndex(packageName: string, outRelToRoot: string, sections: DocsSe '', ].join('\n'); - return [header, ...sections.map(({title, entries}) => renderSection(title, entries))] + return [header, aiSection, ...sections.map(({title, entries}) => renderSection(title, entries))] .filter(Boolean) .join('\n'); } + +// Reads and cleans the README's "For AI agents" section so it can lead the INDEX. +// Empty string when the README (or the section) is absent — the INDEX just omits it. +function readAiSection(readmePath: string): string { + if (!fs.existsSync(readmePath)) { + return ''; + } + const section = extractSection(fs.readFileSync(readmePath, 'utf8'), README_AI_SECTION); + // Keep cleanMarkdown's trailing newline so the INDEX join leaves a blank line + // before the first doc section. + return section ? cleanMarkdown(section) : ''; +} + +// Appends a pointer section to the package README so a human (or agent) browsing +// the repo finds the generated docs tree. Idempotent: re-running buildDocs never +// duplicates it, and the distinct heading keeps it out of {@link readAiSection}. +function ensureReadmePointer(readmePath: string, indexRel: string): void { + if (!fs.existsSync(readmePath)) { + return; + } + const content = fs.readFileSync(readmePath, 'utf8'); + if (extractSection(content, README_DOCS_POINTER)) { + return; + } + const section = [ + `## ${README_DOCS_POINTER}`, + '', + `Agent-readable documentation for the installed version is generated to \`${indexRel}\` — start from that \`INDEX.md\`.`, + '', + ].join('\n'); + fs.writeFileSync(readmePath, `${content.replace(/\s+$/, '')}\n\n${section}`); +} diff --git a/src/cleanMarkdown.ts b/src/cleanMarkdown.ts index 2a80d2e..268321c 100644 --- a/src/cleanMarkdown.ts +++ b/src/cleanMarkdown.ts @@ -81,6 +81,69 @@ export function extractSummary(cleanedMarkdown: string, maxLength = 300): string return ''; } +/** + * Extracts a heading and its body from markdown by heading text, matched + * case-insensitively and ignoring emphasis/code markers. The returned block runs + * from the matched heading up to the next heading of the same or higher level. + * Headings inside fenced code blocks are ignored on both ends. + * + * @param markdown raw markdown source. + * @param headingText heading to look for, e.g. `For AI agents`. + * @returns the section (heading included), trimmed; empty string if not found. + */ +export function extractSection(markdown: string, headingText: string): string { + const lines = markdown.replace(/\r\n/g, '\n').split('\n'); + const target = headingText.trim().toLowerCase(); + + const headingAt = (line: string): {level: number; text: string} | null => { + const match = line.match(/^(#{1,6})\s+(.+?)\s*$/); + return match + ? {level: match[1].length, text: match[2].replace(/[*_`]/g, '').trim().toLowerCase()} + : null; + }; + + let start = -1; + let level = 0; + let inFence = false; + for (let i = 0; i < lines.length; i++) { + if (/^\s*```/.test(lines[i])) { + inFence = !inFence; + continue; + } + if (inFence) { + continue; + } + const heading = headingAt(lines[i]); + if (heading && heading.text === target) { + start = i; + level = heading.level; + break; + } + } + if (start === -1) { + return ''; + } + + let end = lines.length; + inFence = false; + for (let i = start + 1; i < lines.length; i++) { + if (/^\s*```/.test(lines[i])) { + inFence = !inFence; + continue; + } + if (inFence) { + continue; + } + const heading = headingAt(lines[i]); + if (heading && heading.level <= level) { + end = i; + break; + } + } + + return lines.slice(start, end).join('\n').trim(); +} + /** * Extracts the document title. * diff --git a/src/index.ts b/src/index.ts index 7495c78..e5ac1b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,4 +9,4 @@ export type { DocsIndexEntry, BuildDocsResult, } from './buildDocs.js'; -export {cleanMarkdown, extractSummary, extractTitle} from './cleanMarkdown.js'; +export {cleanMarkdown, extractSection, extractSummary, extractTitle} from './cleanMarkdown.js'; diff --git a/test/buildDocs.test.ts b/test/buildDocs.test.ts index 57233b8..4b95f16 100644 --- a/test/buildDocs.test.ts +++ b/test/buildDocs.test.ts @@ -52,3 +52,77 @@ test('excludes legacy and indexes every shipped doc', () => { assert.equal(result.total, 3); // Alpha, Beta, guide (legacy excluded) }); + +// A throwaway package with a writable README, so the pointer-writing behavior +// never mutates the shared fixture. Returns the root and readers. +function buildTempPackage(readmeBody?: string) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gulp-utils-pkg-')); + fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({name: '@demo/widgets'})); + fs.mkdirSync(path.join(root, 'docs'), {recursive: true}); + fs.writeFileSync(path.join(root, 'docs', 'theming.md'), '# Theming\n\nHow to theme.\n'); + fs.mkdirSync(path.join(root, 'src', 'components', 'Button'), {recursive: true}); + fs.writeFileSync( + path.join(root, 'src', 'components', 'Button', 'README.md'), + '# Button\n\nA button.\n', + ); + if (readmeBody !== undefined) { + fs.writeFileSync(path.join(root, 'README.md'), readmeBody); + } + + const run = () => buildDocs(createDefaultDocsConfig(root)); + const readIndex = () => fs.readFileSync(path.join(root, 'build', 'docs', 'INDEX.md'), 'utf8'); + const readReadme = () => fs.readFileSync(path.join(root, 'README.md'), 'utf8'); + return {root, run, readIndex, readReadme}; +} + +test('places the README "For AI agents" section at the top of INDEX.md', () => { + const pkg = buildTempPackage( + [ + '# @demo/widgets', + '', + '![badge](https://img.shields.io/x.svg)', + '', + '## For AI agents', + '', + 'Widget primitives for dashboards.', + '', + '### When to use', + '', + '- Building a grid.', + ].join('\n'), + ); + + pkg.run(); + const index = pkg.readIndex(); + + // The section, cleaned of the badge, leads the generated doc sections. + assert.match(index, /## For AI agents/); + assert.match(index, /Widget primitives for dashboards\./); + assert.doesNotMatch(index, /shields\.io/); + assert.ok( + index.indexOf('## For AI agents') < index.indexOf('## Components'), + 'AI section should appear before the doc sections', + ); +}); + +test('appends a Documentation-for-AI-agents pointer to README.md, only once', () => { + const pkg = buildTempPackage('# @demo/widgets\n\n## For AI agents\n\nPrimitives.\n'); + + pkg.run(); + pkg.run(); + const readme = pkg.readReadme(); + + const occurrences = readme.split('## Documentation for AI agents').length - 1; + assert.equal(occurrences, 1, 'pointer section must not be duplicated across runs'); + assert.match(readme, /build\/docs\/INDEX\.md/); +}); + +test('still builds and adds the pointer when README has no AI section', () => { + const pkg = buildTempPackage('# @demo/widgets\n\nJust a description.\n'); + + pkg.run(); + + assert.doesNotMatch(pkg.readIndex(), /## For AI agents/); + assert.match(pkg.readIndex(), /## Components/); + assert.match(pkg.readReadme(), /## Documentation for AI agents/); +}); diff --git a/test/cleanMarkdown.test.ts b/test/cleanMarkdown.test.ts index 00603c4..cabe126 100644 --- a/test/cleanMarkdown.test.ts +++ b/test/cleanMarkdown.test.ts @@ -1,7 +1,7 @@ import * as assert from 'node:assert/strict'; import {test} from 'node:test'; -import {cleanMarkdown, extractSummary, extractTitle} from '../src/cleanMarkdown.js'; +import {cleanMarkdown, extractSection, extractSummary, extractTitle} from '../src/cleanMarkdown.js'; test('drops SANDBOX and LANDING blocks but keeps GITHUB_BLOCK content', () => { const out = cleanMarkdown( @@ -55,3 +55,43 @@ test('extractTitle returns the first heading text, else empty', () => { assert.equal(extractTitle('## Table\n\nbody'), 'Table'); assert.equal(extractTitle('no heading here'), ''); }); + +test('extractSection extracts a heading and its body, case-insensitively', () => { + const md = ['# Title', '', '## For AI Agents', '', 'Body line.', '', '## Next', '', 'x'].join( + '\n', + ); + + assert.equal(extractSection(md, 'for ai agents'), '## For AI Agents\n\nBody line.'); +}); + +test('extractSection stops at the next same-or-higher heading, keeps deeper ones', () => { + const md = [ + '## For AI agents', + '', + 'Intro.', + '', + '### When to use', + '', + '- a', + '', + '## Other', + '', + 'nope', + ].join('\n'); + + const section = extractSection(md, 'For AI agents'); + assert.match(section, /### When to use/); + assert.doesNotMatch(section, /nope/); +}); + +test('extractSection ignores headings inside fenced code blocks', () => { + const md = ['## For AI agents', '', '```sh', '# not a heading', '```', '', 'End.'].join('\n'); + + const section = extractSection(md, 'For AI agents'); + assert.match(section, /# not a heading/); + assert.match(section, /End\./); +}); + +test('extractSection returns an empty string when the heading is absent', () => { + assert.equal(extractSection('# Only a title\n\nText.', 'For AI agents'), ''); +}); From 2548bf8875d3ad860774183d3d213547cc617ef7 Mon Sep 17 00:00:00 2001 From: Daniil Gaponov Date: Wed, 15 Jul 2026 12:21:05 +0300 Subject: [PATCH 3/7] feat(buildDocs): parse READMEs with @gravity-ui/readme-validator Replace the hand-rolled README parsing used to form INDEX.md with @gravity-ui/readme-validator, which parses on a real Markdown AST: - component title/summary via parseComponentReadme - package overview (For AI agents positioning + prose, Install, Usage) via parsePackageReadme Drop the now-unused extractSummary/extractTitle (and their exports); keep cleanMarkdown/extractSection for doc-body cleanup and pointer idempotency. Add an ambient d.ts since the package ships no types, and pin engines.node >=20.19 (readme-validator is ESM-only; CJS consumers require() it). Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 1344 +++++++++++++++++++++++++++++++++--- package.json | 4 + src/buildDocs.ts | 77 ++- src/cleanMarkdown.ts | 73 -- src/index.ts | 1 - src/readme-validator.d.ts | 17 + test/buildDocs.test.ts | 45 +- test/cleanMarkdown.test.ts | 23 +- 8 files changed, 1359 insertions(+), 225 deletions(-) create mode 100644 src/readme-validator.d.ts diff --git a/package-lock.json b/package-lock.json index 9d77336..597a274 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.1.0", "license": "MIT", "dependencies": { + "@gravity-ui/readme-validator": "^1.2.1", "plugin-error": "^2.0.1", "through2": "^4.0.2", "to-through": "^3.0.0", @@ -32,6 +33,9 @@ "tsx": "^4.19.2", "typescript": "^5.7.3" }, + "engines": { + "node": ">=20.19" + }, "peerDependencies": { "typescript": ">=5" } @@ -1564,6 +1568,21 @@ "prettier": "*" } }, + "node_modules/@gravity-ui/readme-validator": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@gravity-ui/readme-validator/-/readme-validator-1.2.1.tgz", + "integrity": "sha512-9bd81YpD9Ubc9OsvXRGFagXerLmP9lX4yGBVc5Sc6gGUJB5NdocXQbIkfAnLuxqRkcIjvmj1kqKS3dyO+5V1iw==", + "license": "MIT", + "dependencies": { + "mdast-util-to-string": "^3.1.1", + "remark-gfm": "^3.0.1", + "remark-parse": "^10.0.1", + "unified": "^10.1.2" + }, + "bin": { + "gravity-readme": "dist/cli.js" + } + }, "node_modules/@gravity-ui/tsconfig": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@gravity-ui/tsconfig/-/tsconfig-1.0.0.tgz", @@ -1834,6 +1853,15 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/expect": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", @@ -1848,6 +1876,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.10.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", @@ -1868,6 +1911,12 @@ "@types/node": "*" } }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/@types/vinyl": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", @@ -2446,6 +2495,16 @@ "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", "license": "Apache-2.0" }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2610,6 +2669,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2637,6 +2706,16 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3131,7 +3210,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3145,6 +3223,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3188,6 +3279,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -4147,6 +4256,12 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4938,6 +5053,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/is-bun-module": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", @@ -5153,6 +5291,18 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5496,6 +5646,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -5616,6 +5775,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5639,6 +5808,16 @@ "yallist": "^3.0.2" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/marked": { "version": "9.1.6", "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", @@ -5710,157 +5889,921 @@ "node": ">= 0.4" } }, - "node_modules/meow": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", - "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", - "dev": true, + "node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", "license": "MIT", - "engines": { - "node": ">=16.10" + "dependencies": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" }, - "engines": { - "node": ">=8.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", + "node_modules/mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" + "node_modules/mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, + "node_modules/mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", "license": "MIT", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/nano-staged": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/nano-staged/-/nano-staged-0.8.0.tgz", - "integrity": "sha512-QSEqPGTCJbkHU2yLvfY6huqYPjdBrOaTMKatO1F8nCSrkQGXeKwtCiCnsdxnuMhbg3DTVywKaeWLGCE5oJpq0g==", - "dev": true, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", "license": "MIT", "dependencies": { - "picocolors": "^1.0.0" + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" }, - "bin": { - "nano-staged": "lib/bin.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-emoji": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", - "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", - "dev": true, + "node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", "license": "MIT", "dependencies": { - "@sindresorhus/is": "^4.6.0", - "char-regex": "^1.0.2", - "emojilib": "^2.4.0", - "skin-tone": "^2.0.0" + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" }, - "engines": { - "node": ">=18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", + "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", + "license": "MIT", + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nano-staged": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/nano-staged/-/nano-staged-0.8.0.tgz", + "integrity": "sha512-QSEqPGTCJbkHU2yLvfY6huqYPjdBrOaTMKatO1F8nCSrkQGXeKwtCiCnsdxnuMhbg3DTVywKaeWLGCE5oJpq0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0" + }, + "bin": { + "nano-staged": "lib/bin.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, @@ -6432,6 +7375,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -6617,6 +7591,18 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -7430,6 +8416,16 @@ "node": ">=10.13.0" } }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", @@ -7761,6 +8757,80 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", @@ -7808,6 +8878,24 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/validate-npm-package-name": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", @@ -7818,6 +8906,36 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vinyl": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", @@ -8155,6 +9273,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index c957fb2..886402d 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,11 @@ "files": [ "dist" ], + "engines": { + "node": ">=20.19" + }, "dependencies": { + "@gravity-ui/readme-validator": "^1.2.1", "plugin-error": "^2.0.1", "through2": "^4.0.2", "to-through": "^3.0.0", diff --git a/src/buildDocs.ts b/src/buildDocs.ts index ebb1a68..f2046b9 100644 --- a/src/buildDocs.ts +++ b/src/buildDocs.ts @@ -1,13 +1,18 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import {cleanMarkdown, extractSection, extractSummary, extractTitle} from './cleanMarkdown.js'; +import {parseComponentReadme, parsePackageReadme} from '@gravity-ui/readme-validator'; + +import {cleanMarkdown, extractSection} from './cleanMarkdown.js'; // Story/test folders hold Storybook doc pages and fixtures, not API docs. const DEFAULT_EXCLUDE = ['__stories__', '__tests__', '__mocks__', '__snapshots__']; -// Heading in the package README whose content is surfaced at the top of INDEX.md. +// Headings under which the package README's parsed sections are surfaced at the +// top of INDEX.md (canonical labels; readme-validator matches heading aliases). const README_AI_SECTION = 'For AI agents'; +const README_INSTALL_SECTION = 'Install'; +const README_USAGE_SECTION = 'Usage'; // Heading of the pointer section buildDocs keeps in the package README. const README_DOCS_POINTER = 'Documentation for AI agents'; @@ -142,17 +147,16 @@ export function buildDocs(config: DocsConfig = createDefaultDocsConfig()): Build for (const {source, items} of listed) { const entries: DocsIndexEntry[] = []; for (const {source: file, name, outRel} of items) { - const cleaned = rewriteDocLinks( - cleanMarkdown(fs.readFileSync(file, 'utf8')), - file, - outRel, - docMap, - ); + // Title/summary come from readme-validator's AST parse of the source; + // the shipped doc body is the same source run through cleanMarkdown. + const raw = fs.readFileSync(file, 'utf8'); + const parsed = parseComponentReadme(raw); + const cleaned = rewriteDocLinks(cleanMarkdown(raw), file, outRel, docMap); writeDoc(path.join(outDir, outRel), cleaned); entries.push({ - name: source.nameFromTitle ? extractTitle(cleaned) || name : name, + name: source.nameFromTitle ? parsed.title || name : name, rel: outRel, - summary: extractSummary(cleaned), + summary: collapseWhitespace(parsed.description ?? ''), }); } sections.push({title: source.title, entries}); @@ -160,10 +164,10 @@ export function buildDocs(config: DocsConfig = createDefaultDocsConfig()): Build const outRelToRoot = path.relative(rootDir, outDir).split(path.sep).join('/'); const readmePath = path.join(rootDir, 'README.md'); - const aiSection = readAiSection(readmePath); + const overview = readPackageOverview(readmePath); writeDoc( path.join(outDir, 'INDEX.md'), - renderIndex(packageName, outRelToRoot, sections, aiSection), + renderIndex(packageName, outRelToRoot, sections, overview), ); ensureReadmePointer(readmePath, path.posix.join(outRelToRoot, 'INDEX.md')); @@ -299,7 +303,7 @@ function renderIndex( packageName: string, outRelToRoot: string, sections: DocsSection[], - aiSection: string, + overview: string, ): string { // e.g. node_modules/@gravity-ui/uikit/build/docs const installedPath = path.posix.join('node_modules', packageName, outRelToRoot); @@ -313,26 +317,59 @@ function renderIndex( '', ].join('\n'); - return [header, aiSection, ...sections.map(({title, entries}) => renderSection(title, entries))] + return [header, overview, ...sections.map(({title, entries}) => renderSection(title, entries))] .filter(Boolean) .join('\n'); } -// Reads and cleans the README's "For AI agents" section so it can lead the INDEX. -// Empty string when the README (or the section) is absent — the INDEX just omits it. -function readAiSection(readmePath: string): string { +// Parses the package README with readme-validator and renders its agent-facing +// overview (positioning + When to use/not/pitfalls) plus the Install and Usage +// sections, so they lead the generated INDEX. Each field is null when its source +// section is absent, so a README that has adopted only part of the template +// degrades section-by-section. Empty string when nothing is present — the INDEX +// just omits the overview. +function readPackageOverview(readmePath: string): string { if (!fs.existsSync(readmePath)) { return ''; } - const section = extractSection(fs.readFileSync(readmePath, 'utf8'), README_AI_SECTION); + const {agentPositioning, agentProse, install, usage} = parsePackageReadme( + fs.readFileSync(readmePath, 'utf8'), + ); + + const parts: string[] = []; + // agentProse already carries its own `###` subheadings (When to use, …). + if (agentPositioning || agentProse) { + parts.push(`## ${README_AI_SECTION}`); + if (agentPositioning) { + parts.push(agentPositioning); + } + if (agentProse) { + parts.push(agentProse); + } + } + if (install) { + parts.push(`## ${README_INSTALL_SECTION}`, install); + } + if (usage) { + parts.push(`## ${README_USAGE_SECTION}`, usage); + } + if (parts.length === 0) { + return ''; + } // Keep cleanMarkdown's trailing newline so the INDEX join leaves a blank line // before the first doc section. - return section ? cleanMarkdown(section) : ''; + return cleanMarkdown(parts.join('\n\n')); +} + +// Collapses internal whitespace so a multi-line description paragraph stays on a +// single INDEX.md list line. +function collapseWhitespace(text: string): string { + return text.replace(/\s+/g, ' ').trim(); } // Appends a pointer section to the package README so a human (or agent) browsing // the repo finds the generated docs tree. Idempotent: re-running buildDocs never -// duplicates it, and the distinct heading keeps it out of {@link readAiSection}. +// duplicates it, and the distinct heading keeps it out of {@link readPackageOverview}. function ensureReadmePointer(readmePath: string, indexRel: string): void { if (!fs.existsSync(readmePath)) { return; diff --git a/src/cleanMarkdown.ts b/src/cleanMarkdown.ts index 268321c..2c0f28a 100644 --- a/src/cleanMarkdown.ts +++ b/src/cleanMarkdown.ts @@ -36,51 +36,6 @@ export function cleanMarkdown(content: string): string { return `${text.trim()}\n`; } -/** - * Extracts a one-line summary for the docs index: the first prose line after the - * title, skipping the leading import/example code block. Returns an empty string - * when a section heading follows the title directly (no intro paragraph). - * - * @param cleanedMarkdown markdown already processed by {@link cleanMarkdown}. - * @param maxLength maximum summary length before it is truncated. - * @returns the summary line, or an empty string. - */ -export function extractSummary(cleanedMarkdown: string, maxLength = 300): string { - const lines = cleanedMarkdown.split('\n'); - - // Anchor on the title (first heading), then read what comes after it. - const titleIndex = lines.findIndex((line) => /^#{1,6}\s+/.test(line.trim())); - if (titleIndex === -1) { - return ''; - } - - for (let i = titleIndex + 1; i < lines.length; i++) { - const line = lines[i].trim(); - - if (!line) { - continue; - } - // Skip the leading import/example code block. - if (line.startsWith('```')) { - i++; - while (i < lines.length && !lines[i].trim().startsWith('```')) { - i++; - } - continue; - } - // A heading right after the title means there is no intro paragraph. - if (line.startsWith('#')) { - return ''; - } - - // Unwrap links to plain text — the index line has its own base path. - const plain = line.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/[*_`]/g, ''); - return truncate(plain, maxLength); - } - - return ''; -} - /** * Extracts a heading and its body from markdown by heading text, matched * case-insensitively and ignoring emphasis/code markers. The returned block runs @@ -143,31 +98,3 @@ export function extractSection(markdown: string, headingText: string): string { return lines.slice(start, end).join('\n').trim(); } - -/** - * Extracts the document title. - * - * @param cleanedMarkdown markdown already processed by {@link cleanMarkdown}. - * @returns the text of the first heading, or an empty string if there is none. - */ -export function extractTitle(cleanedMarkdown: string): string { - for (const line of cleanedMarkdown.split('\n')) { - const match = line.trim().match(/^#{1,6}\s+(.+)$/); - if (match) { - return match[1].replace(/[*_`]/g, '').trim(); - } - } - return ''; -} - -function truncate(text: string, maxLength: number): string { - if (text.length <= maxLength) { - return text; - } - const sentenceEnd = text.slice(0, maxLength).lastIndexOf('. '); - if (sentenceEnd > 40) { - return text.slice(0, sentenceEnd + 1); - } - const wordEnd = text.slice(0, maxLength).lastIndexOf(' '); - return `${text.slice(0, wordEnd > 40 ? wordEnd : maxLength).trim()}…`; -} diff --git a/src/index.ts b/src/index.ts index e5ac1b8..6252336 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,4 +9,3 @@ export type { DocsIndexEntry, BuildDocsResult, } from './buildDocs.js'; -export {cleanMarkdown, extractSection, extractSummary, extractTitle} from './cleanMarkdown.js'; diff --git a/src/readme-validator.d.ts b/src/readme-validator.d.ts new file mode 100644 index 0000000..253baeb --- /dev/null +++ b/src/readme-validator.d.ts @@ -0,0 +1,17 @@ +// @gravity-ui/readme-validator ships JS only (no bundled .d.ts, no `exports` +// map, `types: null`), so declare the slice of its public API that buildDocs +// consumes. Mirrors the package's own `src/index.ts` / `readme-parse.ts`. +declare module '@gravity-ui/readme-validator' { + export interface PackageExtract { + agentPositioning: string | null; + agentProse: string | null; + install: string | null; + usage: string | null; + } + export interface ComponentExtract { + title: string | null; + description: string | null; + } + export function parsePackageReadme(content: string): PackageExtract; + export function parseComponentReadme(content: string): ComponentExtract; +} diff --git a/test/buildDocs.test.ts b/test/buildDocs.test.ts index 4b95f16..d90edf2 100644 --- a/test/buildDocs.test.ts +++ b/test/buildDocs.test.ts @@ -95,9 +95,12 @@ test('places the README "For AI agents" section at the top of INDEX.md', () => { pkg.run(); const index = pkg.readIndex(); - // The section, cleaned of the badge, leads the generated doc sections. + // The section, cleaned of the badge, leads the generated doc sections, with + // positioning and the When-to-use prose both surfaced. assert.match(index, /## For AI agents/); assert.match(index, /Widget primitives for dashboards\./); + assert.match(index, /### When to use/); + assert.match(index, /Building a grid\./); assert.doesNotMatch(index, /shields\.io/); assert.ok( index.indexOf('## For AI agents') < index.indexOf('## Components'), @@ -105,6 +108,46 @@ test('places the README "For AI agents" section at the top of INDEX.md', () => { ); }); +test('surfaces the README Install and Usage sections in INDEX.md', () => { + const pkg = buildTempPackage( + [ + '# @demo/widgets', + '', + '## Installation', + '', + '```sh', + 'npm install @demo/widgets', + '```', + '', + '## Usage', + '', + 'Wrap your app in the provider.', + ].join('\n'), + ); + + pkg.run(); + const index = pkg.readIndex(); + + // `## Installation` is normalized to the canonical `## Install` label. + assert.match(index, /## Install\b/); + assert.match(index, /npm install @demo\/widgets/); + assert.match(index, /## Usage/); + assert.match(index, /Wrap your app in the provider\./); + assert.ok( + index.indexOf('## Usage') < index.indexOf('## Components'), + 'package sections should precede the doc sections', + ); +}); + +test('summarizes a component index entry from its intro paragraph', () => { + const pkg = buildTempPackage('# @demo/widgets\n\n## For AI agents\n\nPrimitives.\n'); + + pkg.run(); + + // Button/README.md is "# Button\n\nA button." → the paragraph becomes the summary. + assert.match(pkg.readIndex(), /\[Button\]\(\.\/components\/Button\.md\) — A button\./); +}); + test('appends a Documentation-for-AI-agents pointer to README.md, only once', () => { const pkg = buildTempPackage('# @demo/widgets\n\n## For AI agents\n\nPrimitives.\n'); diff --git a/test/cleanMarkdown.test.ts b/test/cleanMarkdown.test.ts index cabe126..8670515 100644 --- a/test/cleanMarkdown.test.ts +++ b/test/cleanMarkdown.test.ts @@ -1,7 +1,7 @@ import * as assert from 'node:assert/strict'; import {test} from 'node:test'; -import {cleanMarkdown, extractSection, extractSummary, extractTitle} from '../src/cleanMarkdown.js'; +import {cleanMarkdown, extractSection} from '../src/cleanMarkdown.js'; test('drops SANDBOX and LANDING blocks but keeps GITHUB_BLOCK content', () => { const out = cleanMarkdown( @@ -35,27 +35,6 @@ test('strips badges and images', () => { assert.match(out, /^Body\.$/m); }); -test('extractSummary takes the first prose line after the import fence', () => { - const md = '# Button\n\n```tsx\nimport {Button} from "x";\n```\n\nButtons trigger actions.\n'; - - assert.equal(extractSummary(md), 'Buttons trigger actions.'); -}); - -test('extractSummary is empty when a heading follows the title directly', () => { - const md = '# Alert\n\n```tsx\nimport x\n```\n\n### Theme\n\nnormal.\n'; - - assert.equal(extractSummary(md), ''); -}); - -test('extractSummary unwraps links to plain text', () => { - assert.equal(extractSummary('# A\n\nUses [B](./B.md) here.\n'), 'Uses B here.'); -}); - -test('extractTitle returns the first heading text, else empty', () => { - assert.equal(extractTitle('## Table\n\nbody'), 'Table'); - assert.equal(extractTitle('no heading here'), ''); -}); - test('extractSection extracts a heading and its body, case-insensitively', () => { const md = ['# Title', '', '## For AI Agents', '', 'Body line.', '', '## Next', '', 'x'].join( '\n', From d02a1eb6118223d734b8c99a509afaae969916e5 Mon Sep 17 00:00:00 2001 From: Daniil Gaponov Date: Wed, 15 Jul 2026 12:37:37 +0300 Subject: [PATCH 4/7] fix: require @gravity-ui/readme-validator ^1.2.3 for its CJS build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.2.3 ships a real CommonJS entry (plus matching types), so the CJS build of gulp-utils can require() it — 1.2.2 was ESM-only and broke CommonJS consumers (uikit's gulpfile). Drop the local ambient d.ts now that the package bundles its own type declarations. Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- src/readme-validator.d.ts | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 src/readme-validator.d.ts diff --git a/package.json b/package.json index 886402d..46d6125 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "node": ">=20.19" }, "dependencies": { - "@gravity-ui/readme-validator": "^1.2.1", + "@gravity-ui/readme-validator": "^1.2.3", "plugin-error": "^2.0.1", "through2": "^4.0.2", "to-through": "^3.0.0", diff --git a/src/readme-validator.d.ts b/src/readme-validator.d.ts deleted file mode 100644 index 253baeb..0000000 --- a/src/readme-validator.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @gravity-ui/readme-validator ships JS only (no bundled .d.ts, no `exports` -// map, `types: null`), so declare the slice of its public API that buildDocs -// consumes. Mirrors the package's own `src/index.ts` / `readme-parse.ts`. -declare module '@gravity-ui/readme-validator' { - export interface PackageExtract { - agentPositioning: string | null; - agentProse: string | null; - install: string | null; - usage: string | null; - } - export interface ComponentExtract { - title: string | null; - description: string | null; - } - export function parsePackageReadme(content: string): PackageExtract; - export function parseComponentReadme(content: string): ComponentExtract; -} From dd297511a1600423760ccb9d7352996136eb3d7e Mon Sep 17 00:00:00 2001 From: Daniil Gaponov Date: Wed, 15 Jul 2026 12:56:04 +0300 Subject: [PATCH 5/7] chore: lock @gravity-ui/readme-validator 1.2.3 from registry Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 597a274..c3bcbe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.1.0", "license": "MIT", "dependencies": { - "@gravity-ui/readme-validator": "^1.2.1", + "@gravity-ui/readme-validator": "^1.2.3", "plugin-error": "^2.0.1", "through2": "^4.0.2", "to-through": "^3.0.0", @@ -1569,9 +1569,9 @@ } }, "node_modules/@gravity-ui/readme-validator": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@gravity-ui/readme-validator/-/readme-validator-1.2.1.tgz", - "integrity": "sha512-9bd81YpD9Ubc9OsvXRGFagXerLmP9lX4yGBVc5Sc6gGUJB5NdocXQbIkfAnLuxqRkcIjvmj1kqKS3dyO+5V1iw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@gravity-ui/readme-validator/-/readme-validator-1.2.3.tgz", + "integrity": "sha512-K4pF/0/3wu8wI+z55uAa6YZARSh9aqc+OfO4TJRqrHmd7MbdojVksoMST98c+bd2KK1kqAIM7f1TogP5fSMOqg==", "license": "MIT", "dependencies": { "mdast-util-to-string": "^3.1.1", From 4270cdbeb1a7ced5ba3d87d1602fb78aa0e3adf1 Mon Sep 17 00:00:00 2001 From: Daniil Gaponov Date: Wed, 15 Jul 2026 13:07:35 +0300 Subject: [PATCH 6/7] fix --- README.md | 3 +-- src/buildDocs.ts | 25 +----------------- src/cleanMarkdown.ts | 63 -------------------------------------------- 3 files changed, 2 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 33da97c..0517684 100644 --- a/README.md +++ b/README.md @@ -71,5 +71,4 @@ section order. Intra-repo `README.md` links are rewritten to resolve inside the output; links to docs that aren't shipped are unwrapped to plain text. The package README's `For AI agents` section, when present, is cleaned and placed -at the top of the generated `INDEX.md`. A `Documentation for AI agents` pointer -section is also appended once to that README, linking to the generated tree. +at the top of the generated `INDEX.md`. diff --git a/src/buildDocs.ts b/src/buildDocs.ts index f2046b9..de28965 100644 --- a/src/buildDocs.ts +++ b/src/buildDocs.ts @@ -3,7 +3,7 @@ import * as path from 'node:path'; import {parseComponentReadme, parsePackageReadme} from '@gravity-ui/readme-validator'; -import {cleanMarkdown, extractSection} from './cleanMarkdown.js'; +import {cleanMarkdown} from './cleanMarkdown.js'; // Story/test folders hold Storybook doc pages and fixtures, not API docs. const DEFAULT_EXCLUDE = ['__stories__', '__tests__', '__mocks__', '__snapshots__']; @@ -13,8 +13,6 @@ const DEFAULT_EXCLUDE = ['__stories__', '__tests__', '__mocks__', '__snapshots__ const README_AI_SECTION = 'For AI agents'; const README_INSTALL_SECTION = 'Install'; const README_USAGE_SECTION = 'Usage'; -// Heading of the pointer section buildDocs keeps in the package README. -const README_DOCS_POINTER = 'Documentation for AI agents'; export type DocsSourceKind = 'readme' | 'markdown'; @@ -169,7 +167,6 @@ export function buildDocs(config: DocsConfig = createDefaultDocsConfig()): Build path.join(outDir, 'INDEX.md'), renderIndex(packageName, outRelToRoot, sections, overview), ); - ensureReadmePointer(readmePath, path.posix.join(outRelToRoot, 'INDEX.md')); const total = sections.reduce((sum, section) => sum + section.entries.length, 0); return {sections, total}; @@ -366,23 +363,3 @@ function readPackageOverview(readmePath: string): string { function collapseWhitespace(text: string): string { return text.replace(/\s+/g, ' ').trim(); } - -// Appends a pointer section to the package README so a human (or agent) browsing -// the repo finds the generated docs tree. Idempotent: re-running buildDocs never -// duplicates it, and the distinct heading keeps it out of {@link readPackageOverview}. -function ensureReadmePointer(readmePath: string, indexRel: string): void { - if (!fs.existsSync(readmePath)) { - return; - } - const content = fs.readFileSync(readmePath, 'utf8'); - if (extractSection(content, README_DOCS_POINTER)) { - return; - } - const section = [ - `## ${README_DOCS_POINTER}`, - '', - `Agent-readable documentation for the installed version is generated to \`${indexRel}\` — start from that \`INDEX.md\`.`, - '', - ].join('\n'); - fs.writeFileSync(readmePath, `${content.replace(/\s+$/, '')}\n\n${section}`); -} diff --git a/src/cleanMarkdown.ts b/src/cleanMarkdown.ts index 2c0f28a..9e3b04e 100644 --- a/src/cleanMarkdown.ts +++ b/src/cleanMarkdown.ts @@ -35,66 +35,3 @@ export function cleanMarkdown(content: string): string { return `${text.trim()}\n`; } - -/** - * Extracts a heading and its body from markdown by heading text, matched - * case-insensitively and ignoring emphasis/code markers. The returned block runs - * from the matched heading up to the next heading of the same or higher level. - * Headings inside fenced code blocks are ignored on both ends. - * - * @param markdown raw markdown source. - * @param headingText heading to look for, e.g. `For AI agents`. - * @returns the section (heading included), trimmed; empty string if not found. - */ -export function extractSection(markdown: string, headingText: string): string { - const lines = markdown.replace(/\r\n/g, '\n').split('\n'); - const target = headingText.trim().toLowerCase(); - - const headingAt = (line: string): {level: number; text: string} | null => { - const match = line.match(/^(#{1,6})\s+(.+?)\s*$/); - return match - ? {level: match[1].length, text: match[2].replace(/[*_`]/g, '').trim().toLowerCase()} - : null; - }; - - let start = -1; - let level = 0; - let inFence = false; - for (let i = 0; i < lines.length; i++) { - if (/^\s*```/.test(lines[i])) { - inFence = !inFence; - continue; - } - if (inFence) { - continue; - } - const heading = headingAt(lines[i]); - if (heading && heading.text === target) { - start = i; - level = heading.level; - break; - } - } - if (start === -1) { - return ''; - } - - let end = lines.length; - inFence = false; - for (let i = start + 1; i < lines.length; i++) { - if (/^\s*```/.test(lines[i])) { - inFence = !inFence; - continue; - } - if (inFence) { - continue; - } - const heading = headingAt(lines[i]); - if (heading && heading.level <= level) { - end = i; - break; - } - } - - return lines.slice(start, end).join('\n').trim(); -} From 7ab37dcf071437405bcf2f2035819b7fa09e641c Mon Sep 17 00:00:00 2001 From: Daniil Gaponov Date: Wed, 15 Jul 2026 16:39:51 +0300 Subject: [PATCH 7/7] fix --- src/buildDocs.ts | 5 ++++- test/buildDocs.test.ts | 43 ++++++++++++++++++++------------------ test/cleanMarkdown.test.ts | 42 +------------------------------------ 3 files changed, 28 insertions(+), 62 deletions(-) diff --git a/src/buildDocs.ts b/src/buildDocs.ts index de28965..e5f655e 100644 --- a/src/buildDocs.ts +++ b/src/buildDocs.ts @@ -162,7 +162,10 @@ export function buildDocs(config: DocsConfig = createDefaultDocsConfig()): Build const outRelToRoot = path.relative(rootDir, outDir).split(path.sep).join('/'); const readmePath = path.join(rootDir, 'README.md'); - const overview = readPackageOverview(readmePath); + // Rewrite the overview's links too: README prose (Useful docs, Usage, …) + // references docs by their repo path, but they were copied/flattened into + // outDir. INDEX.md sits at the outDir root, so outRel is empty. + const overview = rewriteDocLinks(readPackageOverview(readmePath), readmePath, '', docMap); writeDoc( path.join(outDir, 'INDEX.md'), renderIndex(packageName, outRelToRoot, sections, overview), diff --git a/test/buildDocs.test.ts b/test/buildDocs.test.ts index d90edf2..65fdf88 100644 --- a/test/buildDocs.test.ts +++ b/test/buildDocs.test.ts @@ -139,33 +139,36 @@ test('surfaces the README Install and Usage sections in INDEX.md', () => { ); }); -test('summarizes a component index entry from its intro paragraph', () => { - const pkg = buildTempPackage('# @demo/widgets\n\n## For AI agents\n\nPrimitives.\n'); +test('rewrites README overview links to their copied doc locations', () => { + // buildTempPackage ships docs/theming.md → guides/theming.md. A README prose + // link to the repo path must be rewritten relative to INDEX.md (outDir root), + // not left pointing at the no-longer-present ./docs/theming.md. + const pkg = buildTempPackage( + [ + '# @demo/widgets', + '', + '## For AI agents', + '', + 'Primitives.', + '', + '### Useful docs', + '', + '- [Theming](./docs/theming.md)', + ].join('\n'), + ); pkg.run(); + const index = pkg.readIndex(); - // Button/README.md is "# Button\n\nA button." → the paragraph becomes the summary. - assert.match(pkg.readIndex(), /\[Button\]\(\.\/components\/Button\.md\) — A button\./); + assert.match(index, /\[Theming\]\(\.\/guides\/theming\.md\)/); + assert.doesNotMatch(index, /docs\/theming\.md/); }); -test('appends a Documentation-for-AI-agents pointer to README.md, only once', () => { +test('summarizes a component index entry from its intro paragraph', () => { const pkg = buildTempPackage('# @demo/widgets\n\n## For AI agents\n\nPrimitives.\n'); - pkg.run(); - pkg.run(); - const readme = pkg.readReadme(); - - const occurrences = readme.split('## Documentation for AI agents').length - 1; - assert.equal(occurrences, 1, 'pointer section must not be duplicated across runs'); - assert.match(readme, /build\/docs\/INDEX\.md/); -}); - -test('still builds and adds the pointer when README has no AI section', () => { - const pkg = buildTempPackage('# @demo/widgets\n\nJust a description.\n'); - pkg.run(); - assert.doesNotMatch(pkg.readIndex(), /## For AI agents/); - assert.match(pkg.readIndex(), /## Components/); - assert.match(pkg.readReadme(), /## Documentation for AI agents/); + // Button/README.md is "# Button\n\nA button." → the paragraph becomes the summary. + assert.match(pkg.readIndex(), /\[Button\]\(\.\/components\/Button\.md\) — A button\./); }); diff --git a/test/cleanMarkdown.test.ts b/test/cleanMarkdown.test.ts index 8670515..b5b3ab5 100644 --- a/test/cleanMarkdown.test.ts +++ b/test/cleanMarkdown.test.ts @@ -1,7 +1,7 @@ import * as assert from 'node:assert/strict'; import {test} from 'node:test'; -import {cleanMarkdown, extractSection} from '../src/cleanMarkdown.js'; +import {cleanMarkdown} from '../src/cleanMarkdown.js'; test('drops SANDBOX and LANDING blocks but keeps GITHUB_BLOCK content', () => { const out = cleanMarkdown( @@ -34,43 +34,3 @@ test('strips badges and images', () => { assert.doesNotMatch(out, /shields\.io/); assert.match(out, /^Body\.$/m); }); - -test('extractSection extracts a heading and its body, case-insensitively', () => { - const md = ['# Title', '', '## For AI Agents', '', 'Body line.', '', '## Next', '', 'x'].join( - '\n', - ); - - assert.equal(extractSection(md, 'for ai agents'), '## For AI Agents\n\nBody line.'); -}); - -test('extractSection stops at the next same-or-higher heading, keeps deeper ones', () => { - const md = [ - '## For AI agents', - '', - 'Intro.', - '', - '### When to use', - '', - '- a', - '', - '## Other', - '', - 'nope', - ].join('\n'); - - const section = extractSection(md, 'For AI agents'); - assert.match(section, /### When to use/); - assert.doesNotMatch(section, /nope/); -}); - -test('extractSection ignores headings inside fenced code blocks', () => { - const md = ['## For AI agents', '', '```sh', '# not a heading', '```', '', 'End.'].join('\n'); - - const section = extractSection(md, 'For AI agents'); - assert.match(section, /# not a heading/); - assert.match(section, /End\./); -}); - -test('extractSection returns an empty string when the heading is absent', () => { - assert.equal(extractSection('# Only a title\n\nText.', 'For AI agents'), ''); -});