From 132937e374193ae2aabe982a6ac48122db034e30 Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 21:06:36 +0000 Subject: [PATCH 01/12] Add configuration files for oxfmt and oxlint, update package.json and bun.lock - Introduced .oxfmtrc.jsonc for oxfmt configuration to standardize code formatting. - Added .oxlintrc.json for oxlint configuration, extending core linting rules. - Updated package.json to include new scripts for formatting and linting, and added dependencies for oxfmt and oxlint. - Modified bun.lock to reflect the addition of oxfmt and oxlint packages and their bindings for various platforms. - Configured lefthook for pre-commit checks to run formatting automatically on specified file types. --- .cursor/hooks.json | 10 ++++ .oxfmtrc.jsonc | 23 ++++++++ .oxlintrc.json | 6 ++ bun.lock | 144 ++++++++++++++++++++++++++++++++++++++++++++- lefthook.yml | 12 ++++ package.json | 21 ++++--- 6 files changed, 206 insertions(+), 10 deletions(-) create mode 100644 .cursor/hooks.json create mode 100644 .oxfmtrc.jsonc create mode 100644 .oxlintrc.json create mode 100644 lefthook.yml diff --git a/.cursor/hooks.json b/.cursor/hooks.json new file mode 100644 index 0000000..486fd75 --- /dev/null +++ b/.cursor/hooks.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "hooks": { + "afterFileEdit": [ + { + "command": "bun x ultracite fix" + } + ] + } +} \ No newline at end of file diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc new file mode 100644 index 0000000..f52b3c1 --- /dev/null +++ b/.oxfmtrc.jsonc @@ -0,0 +1,23 @@ +// Ultracite oxfmt Configuration +// https://oxc.rs/docs/guide/usage/formatter/config-file-reference.html +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "endOfLine": "lf", + "experimentalSortPackageJson": true, + "experimentalSortImports": { + "ignoreCase": true, + "newlinesBetween": true, + "order": "asc" + } +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..c435a02 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "extends": [ + "./node_modules/ultracite/config/oxlint/core/.oxlintrc.json" + ] +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index 0ef64ee..1c22ca2 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,11 @@ "@changesets/cli": "^2.29.7", "@types/node": "^24.3.0", "changeset-conventional-commits": "^0.2.5", + "lefthook": "^2.1.1", + "oxfmt": "^0.34.0", + "oxlint": "^1.49.0", "typescript": "^5.9.2", + "ultracite": "7.2.3", "vitest": "^3.2.4", }, }, @@ -63,6 +67,10 @@ "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], + "@clack/core": ["@clack/core@1.0.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g=="], + + "@clack/prompts": ["@clack/prompts@1.0.1", "", { "dependencies": { "@clack/core": "1.0.1", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-/42G73JkuYdyWZ6m8d/CJtBrGl1Hegyc7Fy78m5Ob+jF85TOUmLR5XLce/U3LxYAw0kJ8CT5aI99RIvPHcGp/Q=="], + "@effect/cli": ["@effect/cli@0.73.2", "", { "dependencies": { "ini": "^4.1.3", "toml": "^3.0.0", "yaml": "^2.5.0" }, "peerDependencies": { "@effect/platform": "^0.94.3", "@effect/printer": "^0.47.0", "@effect/printer-ansi": "^0.47.0", "effect": "^3.19.16" } }, "sha512-K8IJo81+qa1LU8dhxcDU4QO/bIjL/dPd3zUOSCpLiuUNz8Y3/T+WNs3GqIXEhMfCFMSlRZERN0YgmtRlEZUREA=="], "@effect/cluster": ["@effect/cluster@0.48.16", "", { "peerDependencies": { "@effect/platform": "^0.90.10", "@effect/rpc": "^0.69.4", "@effect/sql": "^0.44.2", "@effect/workflow": "^0.9.6", "effect": "^3.17.14" } }, "sha512-ZZkrSMVetOvlRDD8mPCX3IcVJtvUZBp6++lUKNGIT6LRIObRP4lVwtei85Z+4g49WpeLvJnSdH0zjPtGieFDHQ=="], @@ -167,6 +175,82 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.34.0", "", { "os": "android", "cpu": "arm" }, "sha512-sqkqjh/Z38l+duOb1HtVqJTAj1grt2ttkobCopC/72+a4Xxz4xUgZPFyQ4HxrYMvyqO/YA0tvM1QbfOu70Gk1Q=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.34.0", "", { "os": "android", "cpu": "arm64" }, "sha512-1KRCtasHcVcGOMwfOP9d5Bus2NFsN8yAYM5cBwi8LBg5UtXC3C49WHKrlEa8iF1BjOS6CR2qIqiFbGoA0DJQNQ=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.34.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b+Rmw9Bva6e/7PBES2wLO8sEU7Mi0+/Kv+pXSe/Y8i4fWNftZZlGwp8P01eECaUqpXATfSgNxdEKy7+ssVNz7g=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.34.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-QGjpevWzf1T9COEokZEWt80kPOtthW1zhRbo7x4Qoz646eTTfi6XsHG2uHeDWJmTbgBoJZPMgj2TAEV/ppEZaA=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.34.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VMSaC02cG75qL59M9M/szEaqq/RsLfgpzQ4nqUu8BUnX1zkiZIW2gTpUv3ZJ6qpWnHxIlAXiRZjQwmcwpvtbcg=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.34.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Klm367PFJhH6vYK3vdIOxFepSJZHPaBfIuqwxdkOcfSQ4qqc/M8sgK0UTFnJWWTA/IkhMIh1kW6uEqiZ/xtQqg=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.34.0", "", { "os": "linux", "cpu": "arm" }, "sha512-nqn0QueVXRfbN9m58/E9Zij0Ap8lzayx591eWBYn0sZrGzY1IRv9RYS7J/1YUXbb0Ugedo0a8qIWzUHU9bWQuA=="], + + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.34.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DDn+dcqW+sMTCEjvLoQvC/VWJjG7h8wcdN/J+g7ZTdf/3/Dx730pQElxPPGsCXPhprb11OsPyMp5FwXjMY3qvA=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.34.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-H+F8+71gHQoGTFPPJ6z4dD0Fzfzi0UP8Zx94h5kUmIFThLvMq5K1Y/bUUubiXwwHfwb5C3MPjUpYijiy0rj51Q=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.34.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dIGnzTNhCXqQD5pzBwduLg8pClm+t8R53qaE9i5h8iua1iaFAJyLffh4847CNZSlASb7gn1Ofuv7KoG/EpoGZg=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.34.0", "", { "os": "linux", "cpu": "none" }, "sha512-FGQ2GTTooilDte/ogwWwkHuuL3lGtcE3uKM2EcC7kOXNWdUfMY6Jx3JCodNVVbFoybv4A+HuCj8WJji2uu1Ceg=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.34.0", "", { "os": "linux", "cpu": "none" }, "sha512-2dGbGneJ7ptOIVKMwEIHdCkdZEomh74X3ggo4hCzEXL/rl9HwfsZDR15MkqfQqAs6nVXMvtGIOMxjDYa5lwKaA=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.34.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-cCtGgmrTrxq3OeSG0UAO+w6yLZTMeOF4XM9SAkNrRUxYhRQELSDQ/iNPCLyHhYNi38uHJQbS5RQweLUDpI4ajA=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.34.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7AvMzmeX+k7GdgitXp99GQoIV/QZIpAS7rwxQvC/T541yWC45nwvk4mpnU8N+V6dE5SPEObnqfhCjO80s7qIsg=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.34.0", "", { "os": "linux", "cpu": "x64" }, "sha512-uNiglhcmivJo1oDMh3hoN/Z0WsbEXOpRXZdQ3W/IkOpyV8WF308jFjSC1ZxajdcNRXWej0zgge9QXba58Owt+g=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.34.0", "", { "os": "none", "cpu": "arm64" }, "sha512-5eFsTjCyji25j6zznzlMc+wQAZJoL9oWy576xhqd2efv+N4g1swIzuSDcb1dz4gpcVC6veWe9pAwD7HnrGjLwg=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.34.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-6id8kK0t5hKfbV6LHDzRO21wRTA6ctTlKGTZIsG/mcoir0rssvaYsedUymF4HDj7tbCUlnxCX/qOajKlEuqbIw=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.34.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHaz+w673mlYqn9v/+fuiKZpjkmagleXQ+NygShDv8tdHpRYX2oYhTJwwt9j1ZfVhRgza1EIUW3JmzCXmtPdhQ=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.34.0", "", { "os": "win32", "cpu": "x64" }, "sha512-CXKQM/VaF+yuvGru8ktleHLJoBdjBtTFmAsLGePiESiTN0NjCI/PiaiOCfHMJ1HdP1LykvARUwMvgaN3tDhcrg=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.49.0", "", { "os": "android", "cpu": "arm" }, "sha512-2WPoh/2oK9r/i2R4o4J18AOrm3HVlWiHZ8TnuCaS4dX8m5ZzRmHW0I3eLxEurQLHWVruhQN7fHgZnah+ag5iQg=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.49.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YqJAGvNB11EzoKm1euVhZntb79alhMvWW/j12bYqdvVxn6xzEQWrEDCJg9BPo3A3tBCSUBKH7bVkAiCBqK/L1w=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.49.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WFocCRlvVkMhChCJ2qpJfp1Gj/IjvyjuifH9Pex8m8yHonxxQa3d8DZYreuDQU3T4jvSY8rqhoRqnpc61Nlbxw=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.49.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BN0KniwvehbUfYztOMwEDkYoojGm/narf5oJf+/ap+6PnzMeWLezMaVARNIS0j3OdMkjHTEP8s3+GdPJ7WDywQ=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.49.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SnkAc/DPIY6joMCiP/+53Q+N2UOGMU6ULvbztpmvPJNF/jYPGhNbKtN982uj2Gs6fpbxYkmyj08QnpkD4fbHJA=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.49.0", "", { "os": "linux", "cpu": "arm" }, "sha512-6Z3EzRvpQVIpO7uFhdiGhdE8Mh3S2VWKLL9xuxVqD6fzPhyI3ugthpYXlCChXzO8FzcYIZ3t1+Kau+h2NY1hqA=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.49.0", "", { "os": "linux", "cpu": "arm" }, "sha512-wdjXaQYAL/L25732mLlngfst4Jdmi/HLPVHb3yfCoP5mE3lO/pFFrmOJpqWodgv29suWY74Ij+RmJ/YIG5VuzQ=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.49.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oSHpm8zmSvAG1BWUumbDRSg7moJbnwoEXKAkwDf/xTQJOzvbUknq95NVQdw/AduZr5dePftalB8rzJNGBogUMg=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.49.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-xeqkMOARgGBlEg9BQuPDf6ZW711X6BT5qjDyeM5XNowCJeTSdmMhpePJjTEiVbbr3t21sIlK8RE6X5bc04nWyQ=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.49.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uvcqRO6PnlJGbL7TeePhTK5+7/JXbxGbN+C6FVmfICDeeRomgQqrfVjf0lUrVpUU8ii8TSkIbNdft3M+oNlOsQ=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-Dw1HkdXAwHNH+ZDserHP2RzXQmhHtpsYYI0hf8fuGAVCIVwvS6w1+InLxpPMY25P8ASRNiFN3hADtoh6lI+4lg=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-EPlMYaA05tJ9km/0dI9K57iuMq3Tw+nHst7TNIegAJZrBPtsOtYaMFZEaWj02HA8FI5QvSnRHMt+CI+RIhXJBQ=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.49.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-yZiQL9qEwse34aMbnMb5VqiAWfDY+fLFuoJbHOuzB1OaJZbN1MRF9Nk+W89PIpGr5DNPDipwjZb8+Q7wOywoUQ=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.49.0", "", { "os": "linux", "cpu": "x64" }, "sha512-CcCDwMMXSchNkhdgvhVn3DLZ4EnBXAD8o8+gRzahg+IdSt/72y19xBgShJgadIRF0TsRcV/MhDUMwL5N/W54aQ=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.49.0", "", { "os": "linux", "cpu": "x64" }, "sha512-u3HfKV8BV6t6UCCbN0RRiyqcymhrnpunVmLFI8sEa5S/EBu+p/0bJ3D7LZ2KT6PsBbrB71SWq4DeFrskOVgIZg=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.49.0", "", { "os": "none", "cpu": "arm64" }, "sha512-dRDpH9fw+oeUMpM4br0taYCFpW6jQtOuEIec89rOgDA1YhqwmeRcx0XYeCv7U48p57qJ1XZHeMGM9LdItIjfzA=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.49.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-6rrKe/wL9tn0qnOy76i1/0f4Dc3dtQnibGlU4HqR/brVHlVjzLSoaH0gAFnLnznh9yQ6gcFTBFOPrcN/eKPDGA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.49.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-CXHLWAtLs2xG/aVy1OZiYJzrULlq0QkYpI6cd7VKMrab+qur4fXVE/B1Bp1m0h1qKTj5/FTGg6oU4qaXMjS/ug=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.49.0", "", { "os": "win32", "cpu": "x64" }, "sha512-VteIelt78kwzSglOozaQcs6BCS4Lk0j+QA+hGV0W8UeyaqQ3XpbZRhDU55NW1PPvCy1tg4VXsTlEaPovqto7nQ=="], + "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="], @@ -281,8 +365,12 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], @@ -299,6 +387,8 @@ "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + "citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], @@ -313,6 +403,8 @@ "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -361,6 +453,8 @@ "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], @@ -395,15 +489,39 @@ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + "lefthook": ["lefthook@2.1.1", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.1.1", "lefthook-darwin-x64": "2.1.1", "lefthook-freebsd-arm64": "2.1.1", "lefthook-freebsd-x64": "2.1.1", "lefthook-linux-arm64": "2.1.1", "lefthook-linux-x64": "2.1.1", "lefthook-openbsd-arm64": "2.1.1", "lefthook-openbsd-x64": "2.1.1", "lefthook-windows-arm64": "2.1.1", "lefthook-windows-x64": "2.1.1" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-Tl9h9c+sG3ShzTHKuR3LAIblnnh+Mgxnm2Ul7yu9cu260Z27LEbO3V6Zw4YZFP59/2rlD42pt/llYsQCkkCFzw=="], + + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.1.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-O/RS1j03/Fnq5zCzEb2r7UOBsqPeBuf1C5pMkIJcO4TSE6hf3rhLUkcorKc2M5ni/n5zLGtzQUXHV08/fSAT3Q=="], + + "lefthook-darwin-x64": ["lefthook-darwin-x64@2.1.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-mm/kdKl81ROPoYnj9XYk5JDqj+/6Al8w/SSPDfhItkLJyl4pqS+hWUOP6gDGrnuRk8S0DvJ2+hzhnDsQnZohWQ=="], + + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.1.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-F7JXlKmjxGqGbCWPLND0bVB4DMQezIe48pEwTlUQZbxh450c2gP5Q8FdttMZKOT163kBGGTqJAJSEC6zW+QSxA=="], + + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.1.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Po8/lJMqNzKSZPuEI46dLuWoBoXtAxCuRpeOh6DAV/M4RhBynaCu8rLMZ9BqF7cVbZEWoplOmYo6HdOuiYpCkQ=="], + + "lefthook-linux-arm64": ["lefthook-linux-arm64@2.1.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mI2ljFgPEqHxI8vrN9nKgnVu63Rz1KisDbPwlvs7BTYNwq3sncdK5ukpGR4zzWdh6saNJ5tCtHEtep5GQI11nw=="], + + "lefthook-linux-x64": ["lefthook-linux-x64@2.1.1", "", { "os": "linux", "cpu": "x64" }, "sha512-m3G/FaxC+crxeg9XeaUuHfEoL+i9gbkg2Hp2KD2IcVVIxprqlyqf0Hb8zbLV2NMXuo5RSGokJu44oAoTO3Ou2g=="], + + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.1.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-gz/8FJPvhjOdOFt1GmFvuvDOe+W+BBRjoeAT1/mTgkN7HCXMXgqNjjvakQKQeGz1I1v08wXG1ZNf5y+T9XBCDQ=="], + + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.1.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-ch3lyMUtbmtWUufaQVn4IoEs/2hjK51XqaCdY1mh5ca//VctR1peknIwQ5feHu+vATCDviWQ7HsdNDewm3HMPg=="], + + "lefthook-windows-arm64": ["lefthook-windows-arm64@2.1.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mm3PZhKDs9FE/jQDimkfWxtoj9xQ2k8uw2MdhtC825bhvIh+MEi0WFj/MOW+ug0RBg0I55tGYzZ5aVuozAWpTQ=="], + + "lefthook-windows-x64": ["lefthook-windows-x64@2.1.1", "", { "os": "win32", "cpu": "x64" }, "sha512-1L2oGIzmhfOTxfwbe5mpSQ+m3ilpvGNymwIhn4UHq6hwHsUL6HEhODqx02GfBn6OXpVIr56bvdBAusjL/SVYGQ=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - "lru-cache": ["lru-cache@4.1.5", "", { "dependencies": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" } }, "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g=="], + "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -413,6 +531,10 @@ "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -431,8 +553,14 @@ "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], + "oxfmt": ["oxfmt@0.34.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.34.0", "@oxfmt/binding-android-arm64": "0.34.0", "@oxfmt/binding-darwin-arm64": "0.34.0", "@oxfmt/binding-darwin-x64": "0.34.0", "@oxfmt/binding-freebsd-x64": "0.34.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.34.0", "@oxfmt/binding-linux-arm-musleabihf": "0.34.0", "@oxfmt/binding-linux-arm64-gnu": "0.34.0", "@oxfmt/binding-linux-arm64-musl": "0.34.0", "@oxfmt/binding-linux-ppc64-gnu": "0.34.0", "@oxfmt/binding-linux-riscv64-gnu": "0.34.0", "@oxfmt/binding-linux-riscv64-musl": "0.34.0", "@oxfmt/binding-linux-s390x-gnu": "0.34.0", "@oxfmt/binding-linux-x64-gnu": "0.34.0", "@oxfmt/binding-linux-x64-musl": "0.34.0", "@oxfmt/binding-openharmony-arm64": "0.34.0", "@oxfmt/binding-win32-arm64-msvc": "0.34.0", "@oxfmt/binding-win32-ia32-msvc": "0.34.0", "@oxfmt/binding-win32-x64-msvc": "0.34.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-t+zTE4XGpzPTK+Zk9gSwcJcFi4pqjl6PwO/ZxPBJiJQ2XCKMucwjPlHxvPHyVKJtkMSyrDGfQ7Ntg/hUr4OgHQ=="], + + "oxlint": ["oxlint@1.49.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.49.0", "@oxlint/binding-android-arm64": "1.49.0", "@oxlint/binding-darwin-arm64": "1.49.0", "@oxlint/binding-darwin-x64": "1.49.0", "@oxlint/binding-freebsd-x64": "1.49.0", "@oxlint/binding-linux-arm-gnueabihf": "1.49.0", "@oxlint/binding-linux-arm-musleabihf": "1.49.0", "@oxlint/binding-linux-arm64-gnu": "1.49.0", "@oxlint/binding-linux-arm64-musl": "1.49.0", "@oxlint/binding-linux-ppc64-gnu": "1.49.0", "@oxlint/binding-linux-riscv64-gnu": "1.49.0", "@oxlint/binding-linux-riscv64-musl": "1.49.0", "@oxlint/binding-linux-s390x-gnu": "1.49.0", "@oxlint/binding-linux-x64-gnu": "1.49.0", "@oxlint/binding-linux-x64-musl": "1.49.0", "@oxlint/binding-openharmony-arm64": "1.49.0", "@oxlint/binding-win32-arm64-msvc": "1.49.0", "@oxlint/binding-win32-ia32-msvc": "1.49.0", "@oxlint/binding-win32-x64-msvc": "1.49.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.14.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-YZffp0gM+63CJoRhHjtjRnwKtAgUnXM6j63YQ++aigji2NVvLGsUlrXo9gJUXZOdcbfShLYtA6RuTu8GZ4lzOQ=="], + "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], @@ -449,6 +577,8 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -497,6 +627,8 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -525,7 +657,7 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], @@ -541,6 +673,8 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "ultracite": ["ultracite@7.2.3", "", { "dependencies": { "@clack/prompts": "^1.0.1", "commander": "^14.0.3", "deepmerge": "^4.3.1", "glob": "^13.0.3", "jsonc-parser": "^3.3.1", "nypm": "^0.6.5" }, "peerDependencies": { "oxlint": "^1.0.0" }, "optionalPeers": ["oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-WKNS2sKAZe4BHu+JGbZebXvy/A1QagDaBnndrK/zwOJAze/mQ8jeHfdG2bPlv3qcJ5fdS3w2Kd7c/eIcH78HvA=="], + "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -587,8 +721,12 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + "changeset-conventional-commits/@changesets/read/@changesets/git": ["@changesets/git@2.0.0", "", { "dependencies": { "@babel/runtime": "^7.20.1", "@changesets/errors": "^0.1.4", "@changesets/types": "^5.2.1", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.2", "spawndamnit": "^2.0.0" } }, "sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A=="], "changeset-conventional-commits/@changesets/read/@changesets/logger": ["@changesets/logger@0.0.5", "", { "dependencies": { "chalk": "^2.1.0" } }, "sha512-gJyZHomu8nASHpaANzc6bkQMO9gU/ib20lqew1rVx753FOxffnCrJlGIeQVxNWCqM+o6OOleCo/ivL8UAO5iFw=="], @@ -621,6 +759,8 @@ "changeset-conventional-commits/@changesets/read/@changesets/parse/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "changeset-conventional-commits/@changesets/read/@changesets/git/spawndamnit/cross-spawn/lru-cache": ["lru-cache@4.1.5", "", { "dependencies": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" } }, "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g=="], + "changeset-conventional-commits/@changesets/read/@changesets/git/spawndamnit/cross-spawn/shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], "changeset-conventional-commits/@changesets/read/@changesets/git/spawndamnit/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..0e19112 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,12 @@ +pre-commit: + jobs: + - run: bun x ultracite fix + glob: + - "**/*.js" + - "**/*.jsx" + - "**/*.ts" + - "**/*.tsx" + - "**/*.json" + - "**/*.jsonc" + - "**/*.css" + stage_fixed: true diff --git a/package.json b/package.json index bba6683..49d4029 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,10 @@ "test": "bunx vitest run", "test:watch": "bunx vitest", "typecheck": "bunx tsc -p tsconfig.json --noEmit", - "prepare": "bun run build", - "changeset": "changeset" + "prepare": "lefthook install", + "changeset": "changeset", + "check": "ultracite check", + "fix": "ultracite fix" }, "keywords": [ "readme", @@ -51,14 +53,17 @@ "@effect/printer": "^0.47.0", "@effect/printer-ansi": "^0.47.0", "effect": "^3.19.16" - }, "devDependencies": { + "@changesets/changelog-github": "^0.5.1", + "@changesets/cli": "^2.29.7", "@types/node": "^24.3.0", + "changeset-conventional-commits": "^0.2.5", + "lefthook": "^2.1.1", + "oxfmt": "^0.34.0", + "oxlint": "^1.49.0", "typescript": "^5.9.2", - "vitest": "^3.2.4", - "@changesets/changelog-github": "^0.5.1", - "@changesets/cli": "^2.29.7", - "changeset-conventional-commits": "^0.2.5" + "ultracite": "7.2.3", + "vitest": "^3.2.4" } -} +} \ No newline at end of file From d9731339b93fb508673591a0ec3f5567cfe75bd7 Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 21:11:35 +0000 Subject: [PATCH 02/12] Refactor configuration files and update package.json - Modified .oxfmtrc.jsonc to include a trailing comma for consistency. - Simplified .oxlintrc.json by removing unnecessary array brackets. - Enhanced package.json with additional metadata including keywords, homepage, bugs, license, author, and repository details. - Ensured proper formatting and structure in various JSON files for improved readability and maintainability. --- .../skills/tsdoc-jsdoc-authoring/AGENTS.md | 228 +++++++++++++ .agents/skills/tsdoc-jsdoc-authoring/SKILL.md | 123 +++++++ .../indexes/jsdoc-authoring-index.md | 30 ++ .../indexes/jsdoc-consistency-index.md | 37 +++ .../indexes/tsdoc-index.md | 39 +++ .../skills/tsdoc-jsdoc-authoring/reference.md | 194 +++++++++++ .../rules/jsdoc/advanced/jsdoc-async.md | 34 ++ .../jsdoc/advanced/jsdoc-class-constructor.md | 34 ++ .../advanced/jsdoc-implements-on-classes.md | 43 +++ .../rules/jsdoc/advanced/jsdoc-module.md | 32 ++ .../jsdoc/advanced/jsdoc-typedef-property.md | 37 +++ .../rules/jsdoc/advanced/jsdoc-yields.md | 32 ++ .../rules/jsdoc/core/jsdoc-check-access.md | 53 ++++ .../rules/jsdoc/core/jsdoc-example.md | 34 ++ .../rules/jsdoc/core/jsdoc-no-defaults.md | 46 +++ .../jsdoc/core/jsdoc-optional-default.md | 34 ++ .../rules/jsdoc/core/jsdoc-param.md | 33 ++ .../jsdoc/core/jsdoc-property-namepaths.md | 34 ++ .../jsdoc/core/jsdoc-require-param-type.md | 29 ++ .../rules/jsdoc/core/jsdoc-require-param.md | 40 +++ .../jsdoc-require-property-description.md | 33 ++ .../jsdoc/core/jsdoc-require-property-name.md | 33 ++ .../jsdoc/core/jsdoc-require-property-type.md | 33 ++ .../jsdoc/core/jsdoc-require-property.md | 45 +++ .../core/jsdoc-require-returns-description.md | 36 +++ .../jsdoc/core/jsdoc-require-returns-type.md | 29 ++ .../rules/jsdoc/core/jsdoc-require-returns.md | 53 ++++ .../rules/jsdoc/core/jsdoc-require-yields.md | 50 +++ .../rules/jsdoc/core/jsdoc-returns.md | 31 ++ .../rules/jsdoc/core/jsdoc-summary.md | 31 ++ .../rules/jsdoc/core/jsdoc-throws.md | 32 ++ .../rules/tsdoc/core/tsdoc-deprecated.md | 31 ++ .../rules/tsdoc/core/tsdoc-example.md | 35 ++ .../rules/tsdoc/core/tsdoc-param.md | 71 +++++ .../rules/tsdoc/core/tsdoc-remarks.md | 36 +++ .../rules/tsdoc/core/tsdoc-returns.md | 31 ++ .../rules/tsdoc/core/tsdoc-summary.md | 31 ++ .../rules/tsdoc/core/tsdoc-throws.md | 32 ++ .../rules/tsdoc/core/tsdoc-typeparam.md | 32 ++ .../rules/tsdoc/crossref/tsdoc-inheritdoc.md | 39 +++ .../rules/tsdoc/crossref/tsdoc-label.md | 33 ++ .../rules/tsdoc/crossref/tsdoc-link.md | 31 ++ .../rules/tsdoc/crossref/tsdoc-see.md | 33 ++ .../rules/tsdoc/policy/tsdoc-default-value.md | 32 ++ .../rules/tsdoc/policy/tsdoc-modifier-tags.md | 35 ++ .../tsdoc/policy/tsdoc-no-jsdoc-braces.md | 33 ++ .../policy/tsdoc-package-documentation.md | 32 ++ .../tsdoc/policy/tsdoc-private-remarks.md | 34 ++ .../rules/tsdoc/policy/tsdoc-release-tags.md | 36 +++ .agents/skills/vitest/GENERATION.md | 5 + .agents/skills/vitest/SKILL.md | 53 ++++ .../references/advanced-environments.md | 264 +++++++++++++++ .../vitest/references/advanced-projects.md | 300 ++++++++++++++++++ .../references/advanced-type-testing.md | 242 ++++++++++++++ .../skills/vitest/references/advanced-vi.md | 251 +++++++++++++++ .agents/skills/vitest/references/core-cli.md | 167 ++++++++++ .../skills/vitest/references/core-config.md | 177 +++++++++++ .../skills/vitest/references/core-describe.md | 193 +++++++++++ .../skills/vitest/references/core-expect.md | 211 ++++++++++++ .../skills/vitest/references/core-hooks.md | 245 ++++++++++++++ .../skills/vitest/references/core-test-api.md | 237 ++++++++++++++ .../vitest/references/features-concurrency.md | 250 +++++++++++++++ .../vitest/references/features-context.md | 240 ++++++++++++++ .../vitest/references/features-coverage.md | 202 ++++++++++++ .../vitest/references/features-filtering.md | 208 ++++++++++++ .../vitest/references/features-mocking.md | 272 ++++++++++++++++ .../vitest/references/features-snapshots.md | 207 ++++++++++++ .changeset/config.json | 18 +- .claude/skills/tsdoc-jsdoc-authoring | 1 + .claude/skills/vitest | 1 + .cursor/hooks.json | 2 +- .cursor/skills/tsdoc-jsdoc-authoring | 1 + .cursor/skills/vitest | 1 + .github/workflows/release.yml | 2 +- .oxfmtrc.jsonc | 4 +- .oxlintrc.json | 21 +- examples/basic/README.md | 4 +- examples/basic/readie.json | 14 +- examples/c15t/README.md | 4 +- examples/c15t/old.md | 4 +- examples/c15t/readie.global.json | 130 ++++---- examples/c15t/readie.json | 70 ++-- package.json | 40 +-- schemas/readie.schema.json | 4 +- src/cli/commands/generate-workspace.ts | 82 +++-- src/cli/commands/generate.ts | 55 ++-- src/cli/commands/init.ts | 50 +-- src/cli/resolve-invocation.ts | 32 +- src/config/load-config.ts | 251 +++++++++------ src/config/starter-config.ts | 19 +- src/config/types.ts | 4 +- src/index.ts | 46 +-- src/readme-generator/generator.ts | 59 ++-- src/readme-generator/template.ts | 157 +++++---- test/generator-global-interpolation.test.ts | 48 +-- test/merge-config.test.ts | 57 ++-- test/resolve-invocation.test.ts | 31 +- test/template.test.ts | 31 +- test/validate-config.test.ts | 38 +-- 99 files changed, 6657 insertions(+), 557 deletions(-) create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/AGENTS.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/SKILL.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-authoring-index.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-consistency-index.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/indexes/tsdoc-index.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/reference.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-async.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-class-constructor.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-implements-on-classes.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-module.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-typedef-property.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-yields.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-check-access.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-example.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-no-defaults.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-optional-default.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-param.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-property-namepaths.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param-type.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-description.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-name.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-type.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-description.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-type.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-yields.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-returns.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-summary.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-throws.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-deprecated.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-example.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-param.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-remarks.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-returns.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-summary.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-throws.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-typeparam.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-inheritdoc.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-label.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-link.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-see.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-default-value.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-modifier-tags.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-package-documentation.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-private-remarks.md create mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-release-tags.md create mode 100644 .agents/skills/vitest/GENERATION.md create mode 100644 .agents/skills/vitest/SKILL.md create mode 100644 .agents/skills/vitest/references/advanced-environments.md create mode 100644 .agents/skills/vitest/references/advanced-projects.md create mode 100644 .agents/skills/vitest/references/advanced-type-testing.md create mode 100644 .agents/skills/vitest/references/advanced-vi.md create mode 100644 .agents/skills/vitest/references/core-cli.md create mode 100644 .agents/skills/vitest/references/core-config.md create mode 100644 .agents/skills/vitest/references/core-describe.md create mode 100644 .agents/skills/vitest/references/core-expect.md create mode 100644 .agents/skills/vitest/references/core-hooks.md create mode 100644 .agents/skills/vitest/references/core-test-api.md create mode 100644 .agents/skills/vitest/references/features-concurrency.md create mode 100644 .agents/skills/vitest/references/features-context.md create mode 100644 .agents/skills/vitest/references/features-coverage.md create mode 100644 .agents/skills/vitest/references/features-filtering.md create mode 100644 .agents/skills/vitest/references/features-mocking.md create mode 100644 .agents/skills/vitest/references/features-snapshots.md create mode 120000 .claude/skills/tsdoc-jsdoc-authoring create mode 120000 .claude/skills/vitest create mode 120000 .cursor/skills/tsdoc-jsdoc-authoring create mode 120000 .cursor/skills/vitest diff --git a/.agents/skills/tsdoc-jsdoc-authoring/AGENTS.md b/.agents/skills/tsdoc-jsdoc-authoring/AGENTS.md new file mode 100644 index 0000000..117d153 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/AGENTS.md @@ -0,0 +1,228 @@ +# TSDoc and JSDoc Best Practices + +**Version 1.0.0** +Custom Authoring Guide +February 2026 + +> **Note:** +> This document is designed for agents and LLMs writing or reviewing TypeScript +> and JavaScript documentation comments using TSDoc and JSDoc. It mirrors a +> rule-driven format where each rule has clear "use" and "avoid" guidance with +> incorrect and correct examples. + +--- + +## Abstract + +Comprehensive TSDoc and JSDoc authoring guide for automated documentation generation and review. Contains focused rules across core contract tags, linking/reuse tags, package/internal docs, policy tags, and JavaScript-focused docstring patterns. Each rule explains when to use the tag, when not to use it, and includes concrete incorrect/correct examples. + +## Read Order (Context-Safe) + +Before using this catalog, route through one focused index: + +- `indexes/tsdoc-index.md` +- `indexes/jsdoc-authoring-index.md` +- `indexes/jsdoc-consistency-index.md` + +This file is a reference catalog. Load only the relevant subsection after choosing an index. + +--- + +## Rule Index + +### 1. Core API Contract Rules + +1.1 [Write Clear Summary Sentences](#11-write-clear-summary-sentences) +1.2 [Use `@remarks` for Long-Form Context](#12-use-remarks-for-long-form-context) +1.3 [Document Parameters with `@param`](#13-document-parameters-with-param) +1.4 [Document Generics with `@typeParam`](#14-document-generics-with-typeparam) +1.5 [Document Return Semantics with `@returns`](#15-document-return-semantics-with-returns) +1.6 [Document Expected Failures with `@throws`](#16-document-expected-failures-with-throws) +1.7 [Use `@example` for Non-Obvious Usage](#17-use-example-for-non-obvious-usage) +1.8 [Use `@deprecated` with Migration Guidance](#18-use-deprecated-with-migration-guidance) + +### 2. Cross-Reference and Reuse Rules + +2.1 [Prefer `{@link ...}` for Symbol References](#21-prefer-link--for-symbol-references) +2.2 [Use `@see` for Related APIs](#22-use-see-for-related-apis) +2.3 [Use `@inheritDoc` Only for Equivalent Contracts](#23-use-inheritdoc-only-for-equivalent-contracts) +2.4 [Use `{@label ...}` Only for Structured References](#24-use-label--only-for-structured-references) + +### 3. Package and Internal Notes + +3.1 [Use `@packageDocumentation` for Entrypoint Docs](#31-use-packagedocumentation-for-entrypoint-docs) +3.2 [Use `@privateRemarks` for Maintainer-Only Notes](#32-use-privateremarks-for-maintainer-only-notes) +3.3 [Document Defaults with `@defaultValue`](#33-document-defaults-with-defaultvalue) + +### 4. Stability and Modifier Policy + +4.1 [Apply Release Tags Consistently](#41-apply-release-tags-consistently) +4.2 [Use Modifier Tags Only When Semantically True](#42-use-modifier-tags-only-when-semantically-true) +4.3 [Do Not Mix JSDoc Type Syntax into TSDoc](#43-do-not-mix-jsdoc-type-syntax-into-tsdoc) + +### 5. JSDoc Rules (JavaScript) + +5.1 [Write JSDoc Summaries with Intent](#51-write-jsdoc-summaries-with-intent) +5.2 [Document Parameters with JSDoc Type Expressions](#52-document-parameters-with-jsdoc-type-expressions) +5.3 [Use Optional and Default Parameter Strings Correctly](#53-use-optional-and-default-parameter-strings-correctly) +5.4 [Use Nested Property Namepaths for Object Inputs](#54-use-nested-property-namepaths-for-object-inputs) +5.5 [Document Return Value Semantics with `@returns`](#55-document-return-value-semantics-with-returns) +5.6 [Document Error Contracts with `@throws`](#56-document-error-contracts-with-throws) +5.7 [Add Practical `@example` Snippets](#57-add-practical-example-snippets) +5.8 [Define Reusable Shapes with `@typedef` and `@property`](#58-define-reusable-shapes-with-typedef-and-property) +5.9 [Document Async Behavior with `@async`](#59-document-async-behavior-with-async) +5.10 [Document Generator Output with `@yields`](#510-document-generator-output-with-yields) +5.11 [Document Constructors and Classes Clearly](#511-document-constructors-and-classes-clearly) +5.12 [Use `@module` for Module-Level Docs](#512-use-module-for-module-level-docs) +5.13 [Validate Access Tags with `check-access`](#513-validate-access-tags-with-check-access) +5.14 [Restrict `@implements` to Classes or Constructors](#514-restrict-implements-to-classes-or-constructors) + +--- + +## 1. Core API Contract Rules + +### 1.1 Write Clear Summary Sentences + +See: `rules/tsdoc/core/tsdoc-summary.md` + +### 1.2 Use `@remarks` for Long-Form Context + +See: `rules/tsdoc/core/tsdoc-remarks.md` + +### 1.3 Document Parameters with `@param` + +See: `rules/tsdoc/core/tsdoc-param.md` + +### 1.4 Document Generics with `@typeParam` + +See: `rules/tsdoc/core/tsdoc-typeparam.md` + +### 1.5 Document Return Semantics with `@returns` + +See: `rules/tsdoc/core/tsdoc-returns.md` + +### 1.6 Document Expected Failures with `@throws` + +See: `rules/tsdoc/core/tsdoc-throws.md` + +### 1.7 Use `@example` for Non-Obvious Usage + +See: `rules/tsdoc/core/tsdoc-example.md` + +### 1.8 Use `@deprecated` with Migration Guidance + +See: `rules/tsdoc/core/tsdoc-deprecated.md` + +## 2. Cross-Reference and Reuse Rules + +### 2.1 Prefer `{@link ...}` for Symbol References + +See: `rules/tsdoc/crossref/tsdoc-link.md` + +### 2.2 Use `@see` for Related APIs + +See: `rules/tsdoc/crossref/tsdoc-see.md` + +### 2.3 Use `@inheritDoc` Only for Equivalent Contracts + +See: `rules/tsdoc/crossref/tsdoc-inheritdoc.md` + +### 2.4 Use `{@label ...}` Only for Structured References + +See: `rules/tsdoc/crossref/tsdoc-label.md` + +## 3. Package and Internal Notes + +### 3.1 Use `@packageDocumentation` for Entrypoint Docs + +See: `rules/tsdoc/policy/tsdoc-package-documentation.md` + +### 3.2 Use `@privateRemarks` for Maintainer-Only Notes + +See: `rules/tsdoc/policy/tsdoc-private-remarks.md` + +### 3.3 Document Defaults with `@defaultValue` + +See: `rules/tsdoc/policy/tsdoc-default-value.md` + +## 4. Stability and Modifier Policy + +### 4.1 Apply Release Tags Consistently + +See: `rules/tsdoc/policy/tsdoc-release-tags.md` + +### 4.2 Use Modifier Tags Only When Semantically True + +See: `rules/tsdoc/policy/tsdoc-modifier-tags.md` + +### 4.3 Do Not Mix JSDoc Type Syntax into TSDoc + +See: `rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md` + +## 5. JSDoc Rules (JavaScript) + +These rules are part of the default documentation standard and should be followed even without an active linter. + +### 5.1 Write JSDoc Summaries with Intent + +See: `rules/jsdoc/core/jsdoc-summary.md` + +### 5.2 Document Parameters with JSDoc Type Expressions + +See: `rules/jsdoc/core/jsdoc-param.md` + +### 5.3 Use Optional and Default Parameter Strings Correctly + +See: `rules/jsdoc/core/jsdoc-optional-default.md` + +### 5.4 Use Nested Property Namepaths for Object Inputs + +See: `rules/jsdoc/core/jsdoc-property-namepaths.md` + +### 5.5 Document Return Value Semantics with `@returns` + +See: `rules/jsdoc/core/jsdoc-returns.md` + +### 5.6 Document Error Contracts with `@throws` + +See: `rules/jsdoc/core/jsdoc-throws.md` + +### 5.7 Add Practical `@example` Snippets + +See: `rules/jsdoc/core/jsdoc-example.md` + +### 5.8 Define Reusable Shapes with `@typedef` and `@property` + +See: `rules/jsdoc/advanced/jsdoc-typedef-property.md` + +### 5.9 Document Async Behavior with `@async` + +See: `rules/jsdoc/advanced/jsdoc-async.md` + +### 5.10 Document Generator Output with `@yields` + +See: `rules/jsdoc/advanced/jsdoc-yields.md` + +### 5.11 Document Constructors and Classes Clearly + +See: `rules/jsdoc/advanced/jsdoc-class-constructor.md` + +### 5.12 Use `@module` for Module-Level Docs + +See: `rules/jsdoc/advanced/jsdoc-module.md` + +### 5.13 Validate Access Tags with `check-access` + +See: `rules/jsdoc/core/jsdoc-check-access.md` + +### 5.14 Restrict `@implements` to Classes or Constructors + +See: `rules/jsdoc/advanced/jsdoc-implements-on-classes.md` + +--- + +## References + +1. [https://tsdoc.org](https://tsdoc.org) +2. [https://github.com/microsoft/tsdoc](https://github.com/microsoft/tsdoc) +3. [https://jsdoc.app](https://jsdoc.app) diff --git a/.agents/skills/tsdoc-jsdoc-authoring/SKILL.md b/.agents/skills/tsdoc-jsdoc-authoring/SKILL.md new file mode 100644 index 0000000..c4e6ec1 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/SKILL.md @@ -0,0 +1,123 @@ +--- +name: tsdoc-jsdoc-authoring +description: Write and review API documentation comments using TSDoc and JSDoc best practices. Use when the user asks for docs, doc comments, TSDoc, JSDoc, @param/@returns help, or documentation quality improvements in JavaScript or TypeScript code. +metadata: + author: custom + version: "1.0.0" + argument-hint: +--- + +# TSDoc and JSDoc Authoring + +Create high-quality documentation comments for functions, classes, interfaces, types, modules, and exported APIs. + +## When to Apply + +Use this skill when the user asks to: + +- add or improve code comments/docs +- write TSDoc or JSDoc +- document params, return values, thrown errors, or examples +- standardize documentation style across files + +## Routing (Read This First) + +Choose one focused index before loading rule files: + +- TypeScript APIs and typed exports: `indexes/tsdoc-index.md` +- JavaScript authoring and shape docs: `indexes/jsdoc-authoring-index.md` +- Strict consistency/lint-safe output standards: `indexes/jsdoc-consistency-index.md` + +## Rule 1: Choose the Correct Standard + +- Use **TSDoc** for TypeScript code and typed public APIs. +- Use **JSDoc** for JavaScript code or projects using JSDoc tooling. +- Do not mix tag syntaxes from both standards in one comment block. + +## Workflow + +1. Read the target file and identify the symbol's purpose and side effects. +2. Prefer documenting exported/public APIs first. +3. Write a one-line summary in plain language. +4. Add only meaningful tags (no empty or redundant tags). +5. Validate tag names and formatting against the relevant standard. +6. Ensure comments match actual behavior (inputs, outputs, errors, async behavior). + +## Authoring Rules (Both Standards) + +- Describe intent and behavior, not obvious implementation details. +- Keep summaries concise and action-oriented ("Returns...", "Creates...", "Parses..."). +- Document every parameter and explain what each parameter does. +- If a parameter is an object, document each object property and what it does. +- Use imperative, consistent phrasing for `@param` descriptions. +- Document thrown errors with `@throws` when behavior depends on error handling. +- Add `@example` only when it clarifies non-obvious usage. +- Do not restate TypeScript types in prose unless it adds semantic meaning. + +## TSDoc Rules + +Use these tags by default when relevant: + +- `@param` for each function argument +- `@typeParam` for generic parameters +- `@returns` for return semantics +- `@remarks` for longer contextual details +- `@throws` for exceptional behavior +- `@example` for practical usage +- `@deprecated` for migration guidance + +TSDoc formatting reminders: + +- Use `@param name - Description` with a hyphen separator. +- Prefer inline links with `{@link SymbolName}`. +- Keep release tags (`@alpha`, `@beta`, `@public`, `@internal`) aligned with project policy. +- For object parameters in TypeScript, prefer a named `type` or `interface` and add `/** ... */` comments on each property so VS Code hovers/autocomplete show per-property docs. + +## JSDoc Rules + +Use these tags by default when relevant: + +- `@param {Type} name` (with optional/default forms when needed) +- `@returns {Type}` (or `@return`) +- `@throws {Type}` when known +- `@typedef` and `@property` for reusable object shapes +- `@example` for usage snippets +- `@async` and `@yields` for async/generator behavior when needed + +JSDoc formatting reminders: + +- Optional params: `@param {string} [name]` +- Optional with default: `@param {string} [name=John Doe]` +- Nested object props: `@param {Object} options` and `@param {string} options.mode` + +## JSDoc Consistency Standard + +Apply these rules by default in all JavaScript documentation, regardless of lint setup. + +- Keep `@access` usage consistent per block (`@access ` or one shorthand access tag). +- Use `@implements` only on classes/constructors. +- Prefer rule-specific guidance from the `rules/jsdoc/**` files when there is a conflict. + +## Output Expectations + +When updating comments: + +- keep existing project conventions unless user requests a migration +- preserve behavior accuracy over verbosity +- avoid adding comments to private/internal symbols unless requested + +## Additional Reference + +For templates and tag cheat sheets, see [reference.md](reference.md). + +## Rule Files + +Use focused indexes to avoid loading unrelated rules: + +- TSDoc index: `indexes/tsdoc-index.md` +- JSDoc authoring index: `indexes/jsdoc-authoring-index.md` +- JSDoc consistency index: `indexes/jsdoc-consistency-index.md` + +## Full Compiled Document + +For the full rule index and category guide: `AGENTS.md` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-authoring-index.md b/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-authoring-index.md new file mode 100644 index 0000000..10cba59 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-authoring-index.md @@ -0,0 +1,30 @@ +--- +title: JSDoc Authoring Index +scope: JavaScript documentation patterns +--- + +# JSDoc Authoring Index + +Use this index for normal JavaScript doc authoring tasks. For strict consistency rules, use `indexes/jsdoc-consistency-index.md`. + +## Core Authoring + +- `rules/jsdoc/core/jsdoc-summary.md` +- `rules/jsdoc/core/jsdoc-param.md` +- `rules/jsdoc/core/jsdoc-optional-default.md` +- `rules/jsdoc/core/jsdoc-property-namepaths.md` +- `rules/jsdoc/core/jsdoc-returns.md` +- `rules/jsdoc/core/jsdoc-throws.md` +- `rules/jsdoc/core/jsdoc-example.md` + +## Advanced Authoring + +- `rules/jsdoc/advanced/jsdoc-typedef-property.md` +- `rules/jsdoc/advanced/jsdoc-async.md` +- `rules/jsdoc/advanced/jsdoc-yields.md` +- `rules/jsdoc/advanced/jsdoc-class-constructor.md` +- `rules/jsdoc/advanced/jsdoc-module.md` + +## Deep Reference + +- `reference.md` (templates and syntax cheatsheets) diff --git a/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-consistency-index.md b/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-consistency-index.md new file mode 100644 index 0000000..0064048 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-consistency-index.md @@ -0,0 +1,37 @@ +--- +title: JSDoc Consistency Index +scope: Lint-safe and standard-safe JSDoc output +--- + +# JSDoc Consistency Index + +Use this index when requests need strict, consistent, lint-safe JSDoc output. These rules are part of the default standard. + +## Access and Class Semantics + +- `rules/jsdoc/core/jsdoc-check-access.md` +- `rules/jsdoc/advanced/jsdoc-implements-on-classes.md` + +## Parameter Requirements + +- `rules/jsdoc/core/jsdoc-no-defaults.md` +- `rules/jsdoc/core/jsdoc-require-param.md` +- `rules/jsdoc/core/jsdoc-require-param-type.md` + +## Property Requirements + +- `rules/jsdoc/core/jsdoc-require-property.md` +- `rules/jsdoc/core/jsdoc-require-property-name.md` +- `rules/jsdoc/core/jsdoc-require-property-type.md` +- `rules/jsdoc/core/jsdoc-require-property-description.md` + +## Return and Yield Requirements + +- `rules/jsdoc/core/jsdoc-require-returns.md` +- `rules/jsdoc/core/jsdoc-require-returns-type.md` +- `rules/jsdoc/core/jsdoc-require-returns-description.md` +- `rules/jsdoc/core/jsdoc-require-yields.md` + +## Deep Reference + +- `reference.md` (consistency checklist and templates) diff --git a/.agents/skills/tsdoc-jsdoc-authoring/indexes/tsdoc-index.md b/.agents/skills/tsdoc-jsdoc-authoring/indexes/tsdoc-index.md new file mode 100644 index 0000000..044e6cc --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/indexes/tsdoc-index.md @@ -0,0 +1,39 @@ +--- +title: TSDoc Index +scope: TypeScript API docs +--- + +# TSDoc Index + +Use this index for TypeScript symbols and typed public APIs. Load only the sections you need. + +## Core Contract + +- `rules/tsdoc/core/tsdoc-summary.md` +- `rules/tsdoc/core/tsdoc-remarks.md` +- `rules/tsdoc/core/tsdoc-param.md` +- `rules/tsdoc/core/tsdoc-typeparam.md` +- `rules/tsdoc/core/tsdoc-returns.md` +- `rules/tsdoc/core/tsdoc-throws.md` +- `rules/tsdoc/core/tsdoc-example.md` +- `rules/tsdoc/core/tsdoc-deprecated.md` + +## Cross-Reference and Reuse + +- `rules/tsdoc/crossref/tsdoc-link.md` +- `rules/tsdoc/crossref/tsdoc-see.md` +- `rules/tsdoc/crossref/tsdoc-inheritdoc.md` +- `rules/tsdoc/crossref/tsdoc-label.md` + +## Package and Policy + +- `rules/tsdoc/policy/tsdoc-package-documentation.md` +- `rules/tsdoc/policy/tsdoc-private-remarks.md` +- `rules/tsdoc/policy/tsdoc-default-value.md` +- `rules/tsdoc/policy/tsdoc-release-tags.md` +- `rules/tsdoc/policy/tsdoc-modifier-tags.md` +- `rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md` + +## Deep Reference + +- `reference.md` (templates and review checklist) diff --git a/.agents/skills/tsdoc-jsdoc-authoring/reference.md b/.agents/skills/tsdoc-jsdoc-authoring/reference.md new file mode 100644 index 0000000..a27f54d --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/reference.md @@ -0,0 +1,194 @@ +# TSDoc Deep Reference + +This file is the detailed TSDoc authoring spec for this skill. Use it when generating or reviewing TypeScript API documentation comments. + +## 1) TSDoc Comment Structure + +Standard order for a high-quality TSDoc block: + +1. Summary sentence(s) +2. Optional `@remarks` (long-form details) +3. Parameter docs (`@typeParam`, then `@param`) +4. Return docs (`@returns`) when applicable +5. Error docs (`@throws`) when behavior depends on error handling +6. Optional `@example` +7. Optional policy tags (`@deprecated`, release tags, modifiers) +8. Optional `@see` + +Template: + +````ts +/** + * One-sentence summary in plain language. + * + * @remarks + * Long-form context, constraints, caveats, and behavior notes. + * + * @typeParam T - Meaning of generic type. + * @param input - What this value represents and constraints. + * @returns What is returned and key semantics. + * @throws ErrorType when and why this fails. + * @example + * ```ts + * const value = fn(input) + * ``` + * @deprecated Use `newFn()` instead. + */ +```` + +## 2) TSDoc Tag Kinds (Syntax Types) + +TSDoc tags are grouped into three syntax kinds: + +- `InlineTag`: used inside prose (example: `{@link Foo}`) +- `BlockTag`: starts a new section block (example: `@remarks`) +- `ModifierTag`: marker-style tags with no body (example: `@public`) + +Use the right syntax kind for the right purpose. Do not write modifier tags as prose blocks. + +## 3) Routing to Focused Rule Sets + +Use focused indexes instead of loading full rule catalogs into context: + +- TSDoc work: `indexes/tsdoc-index.md` +- JSDoc authoring work: `indexes/jsdoc-authoring-index.md` +- JSDoc consistency and lint-safe output: `indexes/jsdoc-consistency-index.md` + +Load only the minimum rule files needed for the current edit/review task. + +## 4) High-Quality TSDoc Patterns + +### Document async behavior precisely + +```ts +/** + * Fetches the current profile. + * @returns The active user profile. + * @throws Error when the auth token is invalid. + */ +export async function getProfile(): Promise {} +``` + +### Document generics meaningfully + +```ts +/** + * Creates a dictionary from items. + * @typeParam T - Source item type. + * @param items - Source collection. + * @param getKey - Derives a stable key from each item. + * @returns A map from key to item. + */ +export function toMap( + items: T[], + getKey: (item: T) => string +): Map {} +``` + +### Document thrown contract only + +```ts +/** + * Parses a signed payload. + * @param token - Signed token string. + * @returns Decoded payload. + * @throws Error when signature validation fails. + */ +export function parseSignedToken(token: string): Payload {} +``` + +## 5) TSDoc Mistakes To Avoid + +- Using JSDoc type braces in TSDoc `@param` lines. +- Missing `@typeParam` for public generic APIs. +- Writing verbose summaries that hide key behavior. +- Adding `@example` blocks that are stale or too long. +- Documenting implementation trivia instead of API contract. +- Tagging release level (`@alpha`, `@beta`, etc.) inconsistently across related APIs. + +## 6) TSDoc Review Checklist + +Use this checklist when reviewing generated comments: + +- Summary accurately states what the API does. +- Every parameter has a meaningful `@param`. +- All generics have `@typeParam` when public. +- `@returns` describes semantics, not just type. +- `@throws` documents expected caller-relevant failures. +- `@remarks` is used only when needed. +- Links use `{@link ...}` where appropriate. +- Release/visibility/modifier tags match project policy. +- Comment behavior matches implementation exactly. + +## 7) JSDoc Quick Appendix (Cross-Standard Projects) + +Use this only for JavaScript files or JSDoc-based tooling. + +### JSDoc function template + +```js +/** + * Normalizes a user profile for rendering. + * @param {Object} input - Raw user profile. + * @param {string} input.id - Stable identifier. + * @param {string} [input.displayName] - Optional display name. + * @returns {Object} Normalized profile object. + * @throws {Error} If required fields are missing. + */ +``` + +### JSDoc typedef template + +```js +/** + * @typedef {Object} UserProfile + * @property {string} id - Stable user identifier. + * @property {string} [displayName] - Optional display name. + * @property {boolean} active - Whether the user is active. + */ +``` + +### JSDoc String Syntax Cheatsheet + +Use these exact JSDoc "strings" for common cases: + +- Required param: `@param {string} name - Description` +- Optional param: `@param {string} [name] - Description` +- Optional with default: `@param {string} [name=John Doe] - Description` +- Object param: `@param {Object} options - Description` +- Nested object field: `@param {string} options.mode - Description` +- Array object field: `@param {string} items[].id - Description` +- Return type: `@returns {Promise} Description` +- Throws type: `@throws {Error} Description` +- Access style (pick one): `@access private` or `@private` (do not mix on one doc block) +- `@implements` placement: use only on classes/constructors (not regular functions) + +### TypeScript Object Param Hover Pattern (Recommended) + +For best VS Code hover and autocomplete docs on object properties, use a named type/interface and `/** ... */` on each property: + +```ts +type SearchOptions = { + /** Query string used to match results. */ + query: string; + /** Maximum number of results to return. */ + limit?: number; +}; + +/** + * @param options - Search configuration. + */ +export function search(options: SearchOptions) {} +``` + +## 8) JSDoc Consistency Checklist + +Use this quick pass before finalizing JSDoc blocks in any project: + +- Access tags: use one access style per block; valid `@access` values are `package|private|protected|public`. +- `@implements` placement: use it only on class constructors or constructor-style functions. + +## 9) Sources Used For This Skill + +- TSDoc: `/microsoft/tsdoc` (Context7) +- JSDoc: `/jsdoc/jsdoc.github.io` (Context7) diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-async.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-async.md new file mode 100644 index 0000000..f4679c7 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-async.md @@ -0,0 +1,34 @@ +--- +title: Document Async Behavior with @async +impact: MEDIUM +impactDescription: clarifies asynchronous control flow in generated docs +tags: jsdoc, async, promises, functions +--- + +## Document Async Behavior with `@async` + +Use `@async` where tooling or virtual comments need explicit async annotation. + +**Use this when:** async behavior is not obvious from implementation context. +**Avoid this when:** async is already clear and the project style avoids redundant tags. + +**Incorrect (no async contract in virtual docs):** + +```js +/** + * Downloads data from a URL. + * @param {string} url - Source URL. + * @returns {Promise} Downloaded content. + */ +``` + +**Correct (explicit async behavior):** + +```js +/** + * Downloads data from a URL. + * @async + * @param {string} url - Source URL. + * @returns {Promise} Downloaded content. + */ +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-class-constructor.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-class-constructor.md new file mode 100644 index 0000000..4f3a1c4 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-class-constructor.md @@ -0,0 +1,34 @@ +--- +title: Document Constructors and Classes Clearly +impact: HIGH +impactDescription: improves class API onboarding and usage clarity +tags: jsdoc, class, constructor, oop +--- + +## Document Constructors and Classes Clearly + +Document constructor arguments and class purpose clearly. + +**Use this when:** documenting class-based JavaScript APIs. +**Avoid this when:** constructor parameters are undocumented. + +**Incorrect (class intent unclear):** + +```js +/** + * @constructor + */ +function Book(title, author) {} +``` + +**Correct (constructor contract documented):** + +```js +/** + * Represents a book in the catalog. + * @constructor + * @param {string} title - Book title. + * @param {string} author - Book author. + */ +function Book(title, author) {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-implements-on-classes.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-implements-on-classes.md new file mode 100644 index 0000000..59b91a3 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-implements-on-classes.md @@ -0,0 +1,43 @@ +--- +title: Restrict @implements to Classes or Constructors +impact: HIGH +impactDescription: prevents invalid interface implementation annotations on non-constructors +tags: jsdoc, implements, classes, constructors, lint +--- + +## Restrict `@implements` to Classes or Constructors + +Use `@implements` only on class constructors or constructor-style functions. + +**Use this when:** documenting classes, constructors, or constructor functions. +**Avoid this when:** annotating regular functions, callbacks, or `@function` docs. + +**Incorrect (non-constructor function):** + +```js +/** + * @implements {SomeClass} + */ +function quux() {} +``` + +**Correct (class constructor):** + +```js +class Foo { + /** + * @implements {SomeClass} + */ + constructor() {} +} +``` + +**Correct (constructor-style function):** + +```js +/** + * @implements {SomeClass} + * @class + */ +function quux() {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-module.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-module.md new file mode 100644 index 0000000..504275f --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-module.md @@ -0,0 +1,32 @@ +--- +title: Use @module for Module-Level Docs +impact: MEDIUM +impactDescription: clarifies module boundaries and exported responsibilities +tags: jsdoc, module, entrypoint, organization +--- + +## Use `@module` for Module-Level Docs + +Use `@module` at file/module scope for package organization and generated docs. + +**Use this when:** documenting JavaScript module entrypoints or grouped exports. +**Avoid this when:** annotating individual functions with module-level semantics. + +**Incorrect (module tag on member doc):** + +```js +/** + * @module auth + * Issues access tokens. + */ +function issueToken(userId) {} +``` + +**Correct (module-level declaration):** + +```js +/** + * Authentication helpers. + * @module auth + */ +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-typedef-property.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-typedef-property.md new file mode 100644 index 0000000..22699e1 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-typedef-property.md @@ -0,0 +1,37 @@ +--- +title: Define Reusable Shapes with @typedef and @property +impact: HIGH +impactDescription: centralizes object contracts and improves consistency +tags: jsdoc, typedef, property, object-shapes +--- + +## Define Reusable Shapes with `@typedef` and `@property` + +Use `@typedef` and `@property` for repeated object shapes. + +**Use this when:** multiple APIs share the same object structure. +**Avoid this when:** duplicating long inline object type expressions everywhere. + +**Incorrect (duplicated inline shape):** + +```js +/** + * @param {{ id: string, active: boolean }} user - User object. + */ +function saveUser(user) {} +``` + +**Correct (reusable typedef):** + +```js +/** + * @typedef {Object} UserRecord + * @property {string} id - Stable user identifier. + * @property {boolean} active - Whether the user is active. + */ + +/** + * @param {UserRecord} user - User object. + */ +function saveUser(user) {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-yields.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-yields.md new file mode 100644 index 0000000..fd3ab87 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-yields.md @@ -0,0 +1,32 @@ +--- +title: Document Generator Output with @yields +impact: MEDIUM +impactDescription: clarifies yielded value contracts for iterators +tags: jsdoc, yields, generators, iterators +--- + +## Document Generator Output with `@yields` + +Use `@yields {Type}` to describe generator output values. + +**Use this when:** documenting generator functions. +**Avoid this when:** function is not a generator. + +**Incorrect (missing yielded type):** + +```js +/** + * Generates Fibonacci numbers. + */ +function* fibonacci() {} +``` + +**Correct (explicit yielded contract):** + +```js +/** + * Generates Fibonacci numbers. + * @yields {number} The next number in the sequence. + */ +function* fibonacci() {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-check-access.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-check-access.md new file mode 100644 index 0000000..5f2d6fe --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-check-access.md @@ -0,0 +1,53 @@ +--- +title: Validate Access Tags with check-access +impact: HIGH +impactDescription: prevents ambiguous visibility semantics in JSDoc comments +tags: jsdoc, access, lint, visibility +--- + +## Validate Access Tags with `check-access` + +When documenting visibility in JSDoc, use exactly one access style per doc block: + +- `@access package|private|protected|public`, or +- one shorthand tag: `@package`, `@private`, `@protected`, or `@public` + +**Use this when:** documenting API/member visibility in JavaScript files. +**Avoid this when:** mixing access styles or using unsupported access values. + +**Incorrect (mixed access styles):** + +```js +/** + * @access private + * @public + */ +function normalizeUser(input) {} +``` + +**Incorrect (invalid access value):** + +```js +/** + * @access internal-only + */ +function normalizeUser(input) {} +``` + +**Correct (single valid access tag):** + +```js +/** + * @access private + */ +function normalizeUser(input) {} +``` + +**Correct (single shorthand access tag):** + +```js +/** + * @private + */ +function normalizeUser(input) {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-example.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-example.md new file mode 100644 index 0000000..10297b9 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-example.md @@ -0,0 +1,34 @@ +--- +title: Add Practical @example Snippets +impact: MEDIUM +impactDescription: reduces misuse by showing expected call patterns +tags: jsdoc, example, usage, snippets +--- + +## Add Practical `@example` Snippets + +Use `@example` for behavior that is not obvious from signature alone. + +**Use this when:** API behavior or output format may be misunderstood. +**Avoid this when:** examples are trivial, stale, or verbose. + +**Incorrect (no concrete usage):** + +```js +/** + * Formats a currency value. + */ +function formatCurrency(amount, currency) {} +``` + +**Correct (practical usage):** + +```js +/** + * Formats a currency value. + * @example + * // "$12.50" + * formatCurrency(12.5, "USD") + */ +function formatCurrency(amount, currency) {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-no-defaults.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-no-defaults.md new file mode 100644 index 0000000..8821a23 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-no-defaults.md @@ -0,0 +1,46 @@ +--- +title: Avoid Defaults in JSDoc Param Tags +impact: MEDIUM +impactDescription: keeps parameter documentation aligned with modern JavaScript defaults +tags: jsdoc, param, defaults, lint +--- + +## Avoid Defaults in JSDoc Param Tags + +Do not include default values inside `@param` or `@default` tag syntax when documenting function parameters. + +**Use this when:** documenting JavaScript function parameters where defaults/optionality are represented in code. +**Avoid this when:** writing bracketed defaults like `[name="value"]` in JSDoc. + +This prevents redundant default notation in docs where ES2015+ default parameters already express runtime behavior. + +**Incorrect (default value in `@param`):** + +```js +/** + * @param {number} [foo="7"] + */ +function quux(foo) {} +``` + +**Correct (required param notation):** + +```js +/** + * @param {number} foo + */ +function quux(foo) {} +``` + +**Correct (untyped required param notation):** + +```js +/** + * @param foo + */ +function quux(foo) {} +``` + +Configuration option: + +- `noOptionalParamNames` (`boolean`, default `false`): when `true`, also report square-bracket optional names on `@param` tags. diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-optional-default.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-optional-default.md new file mode 100644 index 0000000..9c8eaea --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-optional-default.md @@ -0,0 +1,34 @@ +--- +title: Use Optional and Default Parameter Strings Correctly +impact: CRITICAL +impactDescription: prevents caller confusion around optional inputs +tags: jsdoc, param, optional, defaults, syntax +--- + +## Use Optional and Default Parameter Strings Correctly + +Use JSDoc optional/default syntax strings consistently: + +- Optional: `@param {string} [name]` +- Optional with default: `@param {string} [name=John Doe]` + +**Use this when:** documenting optional JavaScript parameters. +**Avoid this when:** optionality is described only in prose and not in tag syntax. + +**Incorrect (optionality hidden):** + +```js +/** + * @param {string} name - Optional display name. + */ +function greet(name) {} +``` + +**Correct (explicit optional/default string syntax):** + +```js +/** + * @param {string} [name=John Doe] - Optional display name. + */ +function greet(name) {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-param.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-param.md new file mode 100644 index 0000000..d1cf937 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-param.md @@ -0,0 +1,33 @@ +--- +title: Document Parameters with JSDoc Type Expressions +impact: CRITICAL +impactDescription: defines input contract for JavaScript APIs +tags: jsdoc, param, types, contracts +--- + +## Document Parameters with JSDoc Type Expressions + +Use `@param {Type} name - Description` for each parameter. + +**Use this when:** documenting JavaScript API parameters. +**Avoid this when:** type expressions are missing or inconsistent. + +Each parameter comment must explain what that specific parameter does. + +**Incorrect (missing type expression):** + +```js +/** + * @param timeoutMs - Wait time. + */ +function waitForReady(timeoutMs) {} +``` + +**Correct (typed parameter contract):** + +```js +/** + * @param {number} timeoutMs - Maximum wait time in milliseconds. + */ +function waitForReady(timeoutMs) {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-property-namepaths.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-property-namepaths.md new file mode 100644 index 0000000..d880947 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-property-namepaths.md @@ -0,0 +1,34 @@ +--- +title: Use Nested Property Namepaths for Object Inputs +impact: HIGH +impactDescription: makes object parameter contracts explicit +tags: jsdoc, param, property, namepath, objects +--- + +## Use Nested Property Namepaths for Object Inputs + +When a parameter is an object, document its fields using namepaths. +Each object property needs its own comment that explains what it does. + +**Use this when:** function expects structured objects or arrays of objects. +**Avoid this when:** only top-level object parameter is documented. + +**Incorrect (missing nested fields):** + +```js +/** + * @param {Object} options - Query options. + */ +function search(options) {} +``` + +**Correct (nested property paths):** + +```js +/** + * @param {Object} options - Query options. + * @param {string} options.query - Search query string. + * @param {number} [options.limit=20] - Maximum number of results. + */ +function search(options) {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param-type.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param-type.md new file mode 100644 index 0000000..d1c3db2 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param-type.md @@ -0,0 +1,29 @@ +--- +title: Require Types in @param Tags +impact: HIGH +impactDescription: ensures parameter contracts include explicit JSDoc type information +tags: jsdoc, param, types, lint +--- + +## Require Types in `@param` Tags + +Ensure each JSDoc `@param` tag includes a type expression in curly braces. + +**Use this when:** documenting JavaScript function parameters with JSDoc. +**Avoid this when:** writing untyped `@param` tags like `@param foo`. + +The parameter type should be documented so callers and tooling can understand expected input shapes. + +**Incorrect (missing type in `@param`):** + +```js +/** @param foo */ +function quux(foo) {} +``` + +**Correct (typed `@param`):** + +```js +/** @param {SomeType} foo */ +function quux(foo) {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param.md new file mode 100644 index 0000000..9c77524 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param.md @@ -0,0 +1,40 @@ +--- +title: Require @param for Function Parameters +impact: HIGH +impactDescription: ensures complete input contracts for documented functions +tags: jsdoc, param, completeness, lint +--- + +## Require `@param` for Function Parameters + +Document all function parameters with JSDoc `@param` tags. + +**Use this when:** writing JSDoc for functions, methods, and callable APIs. +**Avoid this when:** some parameters are undocumented in the JSDoc block. + +This improves code quality and maintainability by making function inputs explicit. + +**Incorrect (missing a parameter tag):** + +```js +/** @param foo */ +function quux(foo, bar) {} +``` + +**Correct (all parameters documented):** + +```js +/** @param foo */ +function quux(foo) {} +``` + +Configuration options: + +- `checkConstructors` (`boolean`, default `false`): whether to check constructor methods. +- `checkDestructured` (`boolean`, default `true`): whether to check destructured parameters. +- `checkDestructuredRoots` (`boolean`, default `true`): whether to require a root `@param` tag for root destructured parameters like `function f({a, b}) {}`. +- `checkGetters` (`boolean`, default `true`): whether to check getter methods. +- `checkRestProperty` (`boolean`, default `false`): whether to check rest properties. +- `checkSetters` (`boolean`, default `true`): whether to check setter methods. +- `checkTypesPattern` (`string`, default `"^(?:[oO]bject|[aA]rray|PlainObject|Generic(?:Object|Array))$"`): regex pattern for types exempted from checking. +- `exemptedBy` (`string[]`, default `["inheritdoc"]`): JSDoc tags that exempt functions from `@param` checking. diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-description.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-description.md new file mode 100644 index 0000000..aa6d664 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-description.md @@ -0,0 +1,33 @@ +--- +title: Require Descriptions for @property Tags +impact: HIGH +impactDescription: improves object-shape clarity by documenting property intent +tags: jsdoc, property, descriptions, lint +--- + +## Require Descriptions for `@property` Tags + +Ensure all `@property` tags include a description. + +**Use this when:** documenting typedef or namespace properties. +**Avoid this when:** leaving property tags without explanatory text. + +Property descriptions should be documented so consumers understand each field's purpose. + +**Incorrect (missing property description):** + +```js +/** + * @typedef {SomeType} SomeTypedef + * @property {number} foo + */ +``` + +**Correct (property description present):** + +```js +/** + * @typedef {SomeType} SomeTypedef + * @property {number} foo Foo. + */ +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-name.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-name.md new file mode 100644 index 0000000..22be0bd --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-name.md @@ -0,0 +1,33 @@ +--- +title: Require Names in @property Tags +impact: HIGH +impactDescription: ensures each documented property is identifiable by name +tags: jsdoc, property, names, lint +--- + +## Require Names in `@property` Tags + +Ensure all `@property` tags include a property name. + +**Use this when:** documenting typedef or namespace object properties. +**Avoid this when:** writing `@property` tags without an identifier. + +Property names should be documented so object fields can be referenced unambiguously. + +**Incorrect (missing property name):** + +```js +/** + * @typedef {SomeType} SomeTypedef + * @property {number} + */ +``` + +**Correct (property name present):** + +```js +/** + * @typedef {SomeType} SomeTypedef + * @property {number} foo + */ +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-type.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-type.md new file mode 100644 index 0000000..1a70bd0 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-type.md @@ -0,0 +1,33 @@ +--- +title: Require Types in @property Tags +impact: HIGH +impactDescription: ensures property contracts include explicit type information +tags: jsdoc, property, types, lint +--- + +## Require Types in `@property` Tags + +Ensure each `@property` tag includes a type expression in curly braces. + +**Use this when:** documenting typedef, namespace, or class properties with JSDoc. +**Avoid this when:** writing untyped `@property` tags like `@property foo`. + +Property types should be documented so consumers and tooling can infer expected data shapes. + +**Incorrect (missing property type):** + +```js +/** + * @typedef {SomeType} SomeTypedef + * @property foo + */ +``` + +**Correct (typed property):** + +```js +/** + * @typedef {SomeType} SomeTypedef + * @property {number} foo + */ +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property.md new file mode 100644 index 0000000..40e5d72 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property.md @@ -0,0 +1,45 @@ +--- +title: Require @property for Object Typedefs and Namespaces +impact: HIGH +impactDescription: prevents incomplete object-shape documentation for typedefs and namespaces +tags: jsdoc, typedef, namespace, property, lint +--- + +## Require `@property` for Object Typedefs and Namespaces + +When using `@typedef` or `@namespace` with plain object types, include one or more `@property` tags. + +**Use this when:** documenting object-based typedefs or namespaces. +**Avoid this when:** leaving object typedefs/namespaces without property definitions. + +Object shapes should have properties defined so consumers can understand the contract. + +**Incorrect (missing `@property`):** + +```js +/** + * @typedef {Object} SomeTypedef + */ + +/** + * @namespace {Object} SomeNamespace + */ +``` + +**Correct (object typedef with properties):** + +```js +/** + * @typedef {Object} SomeTypedef + * @property {SomeType} propName Prop description + */ +``` + +**Correct (object typedef with shorthand property):** + +```js +/** + * @typedef {object} Foo + * @property someProp + */ +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-description.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-description.md new file mode 100644 index 0000000..02b23cb --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-description.md @@ -0,0 +1,36 @@ +--- +title: Require Descriptions in @returns Tags +impact: HIGH +impactDescription: clarifies return-value semantics beyond raw type information +tags: jsdoc, returns, descriptions, lint +--- + +## Require Descriptions in `@returns` Tags + +Ensure `@returns` tags include a description value. + +**Use this when:** documenting return behavior for functions and methods. +**Avoid this when:** leaving `@returns` empty or type-only without meaning. + +A `@returns` tag should explain what is returned, not just indicate that a return exists. + +The error is not reported when the return type is: + +- `void` +- `undefined` +- `Promise` +- `Promise` + +**Incorrect (missing return description):** + +```js +/** @returns */ +function quux(foo) {} +``` + +**Correct (return description present):** + +```js +/** @returns Foo. */ +function quux(foo) {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-type.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-type.md new file mode 100644 index 0000000..5e9bcd2 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-type.md @@ -0,0 +1,29 @@ +--- +title: Require Types in @returns Tags +impact: HIGH +impactDescription: ensures return contracts include explicit type information +tags: jsdoc, returns, types, lint +--- + +## Require Types in `@returns` Tags + +Ensure each `@returns` tag includes a type expression in curly braces. + +**Use this when:** documenting return values in JavaScript JSDoc blocks. +**Avoid this when:** writing untyped `@returns` tags like `@returns`. + +A `@returns` tag should include a type value to clearly define return shape expectations. + +**Incorrect (missing return type):** + +```js +/** @returns */ +function quux(foo) {} +``` + +**Correct (typed return):** + +```js +/** @returns {string} */ +function quux(foo) {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns.md new file mode 100644 index 0000000..142b397 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns.md @@ -0,0 +1,53 @@ +--- +title: Require @returns for Return Statements +impact: HIGH +impactDescription: prevents missing or ambiguous return-value documentation +tags: jsdoc, returns, completeness, lint +--- + +## Require `@returns` for Return Statements + +Document return statements with `@returns` and avoid multiple `@returns` tags in a single doc block. + +**Use this when:** documenting functions or methods that return values. +**Avoid this when:** omitting `@returns` for returning functions or adding duplicate `@returns` tags. + +This prevents missing return contracts and inconsistent return documentation. + +**Incorrect (missing `@returns`):** + +```js +/** Foo. */ +function quux() { + return foo; +} +``` + +**Incorrect (duplicate `@returns` tags):** + +```js +/** + * @returns Foo! + * @returns Foo? + */ +function quux() { + return foo; +} +``` + +**Correct (single `@returns`):** + +```js +/** @returns Foo. */ +function quux() { + return foo; +} +``` + +Configuration options: + +- `checkConstructors` (`boolean`, default `false`): whether to check constructor methods. +- `checkGetters` (`boolean`, default `true`): whether to check getter methods. +- `exemptedBy` (`string[]`, default `["inheritdoc"]`): tags that exempt functions from requiring `@returns`. +- `forceRequireReturn` (`boolean`, default `false`): whether to require `@returns` even if no value is returned. +- `forceReturnsWithAsync` (`boolean`, default `false`): whether to require `@returns` on async functions. diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-yields.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-yields.md new file mode 100644 index 0000000..effc597 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-yields.md @@ -0,0 +1,50 @@ +--- +title: Require @yields for Generator Output +impact: HIGH +impactDescription: ensures generator output contracts are documented and unambiguous +tags: jsdoc, yields, generators, lint +--- + +## Require `@yields` for Generator Output + +Document generator yields with `@yields` and avoid multiple `@yields` tags in a single doc block. + +**Use this when:** documenting generator functions that yield values. +**Avoid this when:** omitting `@yields` on yielding generators or adding duplicate `@yields` tags. + +This prevents missing yield contracts and inconsistent generator documentation. + +**Incorrect (missing `@yields`):** + +```js +function* quux(foo) { + yield foo; +} +``` + +**Incorrect (duplicate `@yields` tags):** + +```js +/** + * @yields {undefined} + * @yields {void} + */ +function* quux(foo) {} +``` + +**Correct (single `@yields`):** + +```js +/** + * @yields Foo + */ +function* quux(foo) { + yield foo; +} +``` + +Configuration options: + +- `exemptedBy` (`string[]`, default `["inheritdoc"]`): functions with these tags are exempt. +- `forceRequireYields` (`boolean`, default `false`): require `@yields` on all generators, even empty/non-yielding ones. +- `withGeneratorTag` (`boolean`, default `false`): require `@yields` when a `@generator` tag is present. diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-returns.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-returns.md new file mode 100644 index 0000000..44f55aa --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-returns.md @@ -0,0 +1,31 @@ +--- +title: Document Return Value Semantics with @returns +impact: HIGH +impactDescription: clarifies output behavior and expected shape +tags: jsdoc, returns, output, contracts +--- + +## Document Return Value Semantics with @returns + +Use `@returns {Type}` with semantic description. + +**Use this when:** functions return a value or Promise payload. +**Avoid this when:** only stating raw type without meaning. + +**Incorrect (type-only):** + +```js +/** + * @returns {Object} + */ +function parseConfig() {} +``` + +**Correct (semantics included):** + +```js +/** + * @returns {{ retries: number, timeoutMs: number }} Normalized runtime configuration. + */ +function parseConfig() {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-summary.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-summary.md new file mode 100644 index 0000000..740866d --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-summary.md @@ -0,0 +1,31 @@ +--- +title: Write JSDoc Summaries with Intent +impact: HIGH +impactDescription: improves API readability for JavaScript consumers +tags: jsdoc, summary, readability +--- + +## Write JSDoc Summaries with Intent + +Write concise summaries that explain behavior and purpose. + +**Use this when:** documenting JavaScript functions, classes, and modules. +**Avoid this when:** repeating symbol names without useful meaning. + +**Incorrect (name restatement):** + +```js +/** + * Gets data. + */ +function getData() {} +``` + +**Correct (behavior-focused):** + +```js +/** + * Fetches and parses JSON data from the configured endpoint. + */ +function getData() {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-throws.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-throws.md new file mode 100644 index 0000000..fa932dd --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-throws.md @@ -0,0 +1,32 @@ +--- +title: Document Error Contracts with @throws +impact: HIGH +impactDescription: enables predictable caller error handling +tags: jsdoc, throws, errors, reliability +--- + +## Document Error Contracts with @throws + +Use `@throws {Type} ...` for expected, caller-relevant failures. + +**Use this when:** function may throw known errors callers should handle. +**Avoid this when:** listing incidental internal errors with no contract value. + +**Incorrect (no throws contract):** + +```js +/** + * Parses a signed token. + */ +function parseSignedToken(token) {} +``` + +**Correct (typed throws contract):** + +```js +/** + * Parses a signed token. + * @throws {Error} If the token signature is invalid. + */ +function parseSignedToken(token) {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-deprecated.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-deprecated.md new file mode 100644 index 0000000..9bd7587 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-deprecated.md @@ -0,0 +1,31 @@ +--- +title: Use @deprecated with Migration Guidance +impact: HIGH +impactDescription: prevents dead-end API usage and migration confusion +tags: tsdoc, deprecated, migration, lifecycle +--- + +## Use @deprecated with Migration Guidance + +Use `@deprecated` only with explicit replacement guidance. + +**Use this when:** a symbol should no longer be used. +**Avoid this when:** deprecation is unclear or no replacement exists. + +**Incorrect (no migration path):** + +```ts +/** + * @deprecated + */ +export function oldHash(input: string): string {} +``` + +**Correct (clear migration path):** + +```ts +/** + * @deprecated Use `hashSha256()` for stable cross-platform output. + */ +export function oldHash(input: string): string {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-example.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-example.md new file mode 100644 index 0000000..2639e8a --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-example.md @@ -0,0 +1,35 @@ +--- +title: Use @example for Non-Obvious Usage +impact: MEDIUM +impactDescription: reduces misuse and clarifies edge cases +tags: tsdoc, example, usage, snippets +--- + +## Use @example for Non-Obvious Usage + +Use `@example` for tricky, non-obvious, or high-value usage patterns. + +**Use this when:** behavior is subtle or edge-case driven. +**Avoid this when:** examples are trivial, stale, or excessively long. + +**Incorrect (no usage guidance for tricky API):** + +```ts +/** + * Retries an operation with backoff. + */ +export async function retry(fn: () => Promise): Promise {} +``` + +**Correct (clear example):** + +````ts +/** + * Retries an operation with backoff. + * @example + * ```ts + * const result = await retry(() => fetchJson("/api/data")) + * ``` + */ +export async function retry(fn: () => Promise): Promise {} +```` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-param.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-param.md new file mode 100644 index 0000000..ffc754c --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-param.md @@ -0,0 +1,71 @@ +--- +title: Document Parameters with @param +impact: CRITICAL +impactDescription: clarifies caller obligations and input semantics +tags: tsdoc, param, function-contract, api-docs +--- + +## Document Parameters with @param + +Use one `@param` line per parameter: `@param name - Description`. + +**Use this when:** documenting any function or method parameters. +**Avoid this when:** repeating type annotations already in the signature. + +Each parameter comment must explain what that specific parameter does. +If a parameter is an object, document each relevant property and what each property does. +For TypeScript object params, prefer a named object type/interface with `/** ... */` comments on each property. + +**Incorrect (missing semantics):** + +```ts +/** + * @param timeoutMs - number + */ +export function waitForReady(timeoutMs: number): Promise {} +``` + +**Correct (caller-relevant semantics):** + +```ts +/** + * @param timeoutMs - Maximum wait time in milliseconds before timing out. + */ +export function waitForReady(timeoutMs: number): Promise {} +``` + +**Incorrect (object parameter properties undocumented):** + +```ts +/** + * @param options - Search configuration. + */ +export function search(options: { query: string; limit?: number }): Result[] {} +``` + +**Correct (object parameter properties documented):** + +```ts +/** + * @param options - Search configuration. + * @param options.query - Query string used to match results. + * @param options.limit - Maximum number of results to return. + */ +export function search(options: { query: string; limit?: number }): Result[] {} +``` + +**Preferred (VS Code hover/autocomplete friendly pattern):** + +```ts +type SearchOptions = { + /** Query string used to match results. */ + query: string; + /** Maximum number of results to return. */ + limit?: number; +}; + +/** + * @param options - Search configuration. + */ +export function search(options: SearchOptions): Result[] {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-remarks.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-remarks.md new file mode 100644 index 0000000..7be061f --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-remarks.md @@ -0,0 +1,36 @@ +--- +title: Use @remarks for Long-Form Context +impact: HIGH +impactDescription: prevents overloaded summaries and preserves detail +tags: tsdoc, remarks, context, contracts +--- + +## Use @remarks for Long-Form Context + +Use `@remarks` for deeper context, caveats, and non-trivial behavior. + +**Use this when:** summary alone cannot express constraints or trade-offs. +**Avoid this when:** a one-line summary is sufficient. + +**Incorrect (long narrative in summary):** + +```ts +/** + * Generates cache keys and also normalizes locale and strips unsupported + * fields and retries once if key generation collides. + */ +export function buildCacheKey(input: Input): string {} +``` + +**Correct (summary + remarks):** + +```ts +/** + * Builds a deterministic cache key for request inputs. + * + * @remarks + * Normalizes locale casing, removes unsupported fields, and retries once on + * hash collisions to preserve key uniqueness guarantees. + */ +export function buildCacheKey(input: Input): string {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-returns.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-returns.md new file mode 100644 index 0000000..a9d2660 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-returns.md @@ -0,0 +1,31 @@ +--- +title: Document Return Semantics with @returns +impact: HIGH +impactDescription: clarifies output guarantees and expectations +tags: tsdoc, returns, output, contracts +--- + +## Document Return Semantics with @returns + +Use `@returns` to describe return meaning, guarantees, and ordering. + +**Use this when:** function returns non-void values (including Promise payloads). +**Avoid this when:** output is `void` and no return contract exists. + +**Incorrect (type-only restatement):** + +```ts +/** + * @returns A string. + */ +export function getCacheKey(): string {} +``` + +**Correct (semantic output contract):** + +```ts +/** + * @returns A deterministic cache key that is stable for equivalent inputs. + */ +export function getCacheKey(): string {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-summary.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-summary.md new file mode 100644 index 0000000..2f3b66b --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-summary.md @@ -0,0 +1,31 @@ +--- +title: Write Clear Summary Sentences +impact: HIGH +impactDescription: improves API discoverability and comprehension +tags: tsdoc, summary, readability, api-docs +--- + +## Write Clear Summary Sentences + +Write a short summary that explains behavior and intent in plain language. + +**Use this when:** every documented symbol. +**Avoid this when:** copying symbol names or implementation trivia. + +**Incorrect (name restatement, no meaning):** + +```ts +/** + * Gets user. + */ +export function getUser(id: string): Promise {} +``` + +**Correct (behavior and outcome):** + +```ts +/** + * Fetches a user by ID from the primary data source. + */ +export function getUser(id: string): Promise {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-throws.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-throws.md new file mode 100644 index 0000000..4b1f360 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-throws.md @@ -0,0 +1,32 @@ +--- +title: Document Expected Failures with @throws +impact: HIGH +impactDescription: defines caller-facing error-handling contracts +tags: tsdoc, throws, errors, reliability +--- + +## Document Expected Failures with @throws + +Use `@throws` for expected, contract-relevant failures. + +**Use this when:** callers should handle known failure modes. +**Avoid this when:** documenting incidental low-level errors with no API contract value. + +**Incorrect (no throw contract):** + +```ts +/** + * Parses a signed token. + */ +export function parseToken(token: string): Payload {} +``` + +**Correct (caller-relevant error path):** + +```ts +/** + * Parses a signed token. + * @throws Error if token signature verification fails. + */ +export function parseToken(token: string): Payload {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-typeparam.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-typeparam.md new file mode 100644 index 0000000..88fdb3b --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-typeparam.md @@ -0,0 +1,32 @@ +--- +title: Document Generics with @typeParam +impact: CRITICAL +impactDescription: preserves generic API intent for consumers +tags: tsdoc, typeparam, generics, api-contract +--- + +## Document Generics with @typeParam + +Use `@typeParam` for each public generic parameter. + +**Use this when:** function/class/interface/type alias has generic type params. +**Avoid this when:** symbol has no generic parameters. + +**Incorrect (undocumented generic intent):** + +```ts +/** + * Builds an index from items. + */ +export function toIndex(items: T[]): Map {} +``` + +**Correct (clear generic role):** + +```ts +/** + * Builds an index from items. + * @typeParam T - Source item type stored in the resulting map. + */ +export function toIndex(items: T[]): Map {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-inheritdoc.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-inheritdoc.md new file mode 100644 index 0000000..f3c4d59 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-inheritdoc.md @@ -0,0 +1,39 @@ +--- +title: Use @inheritDoc Only for Equivalent Contracts +impact: HIGH +impactDescription: prevents inherited docs from drifting from real behavior +tags: tsdoc, inheritdoc, inheritance, contracts +--- + +## Use @inheritDoc Only for Equivalent Contracts + +Use `@inheritDoc` only when behavior is truly equivalent to the referenced declaration. + +**Use this when:** override/wrapper keeps the same contract. +**Avoid this when:** behavior, defaults, errors, or side effects differ. + +**Incorrect (inherits despite changed behavior):** + +```ts +/** + * @inheritDoc BaseClient.fetch + */ +export class CachedClient extends BaseClient { + public override fetch(id: string): Promise {} +} +``` + +**Correct (document differences explicitly):** + +```ts +/** + * Fetches an item by ID, serving stale-while-revalidate responses from cache. + * + * @remarks + * Unlike `BaseClient.fetch`, this method may return cached data immediately + * and refresh the cache asynchronously. + */ +export class CachedClient extends BaseClient { + public override fetch(id: string): Promise {} +} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-label.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-label.md new file mode 100644 index 0000000..dd6c85d --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-label.md @@ -0,0 +1,33 @@ +--- +title: Use {@label ...} Only for Structured References +impact: LOW +impactDescription: avoids unnecessary complexity in standard docs +tags: tsdoc, label, inline-tags, references +--- + +## Use `{@label ...}` Only for Structured References + +Use `{@label ...}` sparingly for advanced reference labeling scenarios. + +**Use this when:** your doc tooling relies on explicit labels for cross-reference flows. +**Avoid this when:** ordinary symbol linking via `{@link ...}` is sufficient. + +**Incorrect (using labels as normal links):** + +```ts +/** + * {@label parseToken} + * Parses a signed token. + */ +export function parseToken(token: string): Payload {} +``` + +**Correct (use link for normal references):** + +```ts +/** + * Parses a signed token. + * See {@link verifyToken} for signature checks. + */ +export function parseToken(token: string): Payload {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-link.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-link.md new file mode 100644 index 0000000..96a10b6 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-link.md @@ -0,0 +1,31 @@ +--- +title: Prefer {@link ...} for Symbol References +impact: MEDIUM +impactDescription: improves navigation and reference accuracy +tags: tsdoc, link, cross-reference, discoverability +--- + +## Prefer `{@link ...}` for Symbol References + +Use inline `{@link ...}` when referring to symbols or canonical URLs. + +**Use this when:** references should be navigable and unambiguous. +**Avoid this when:** plain text names could be confused or drift over time. + +**Incorrect (non-linking text reference):** + +```ts +/** + * Works with TokenVerifier.verify for signature validation. + */ +export function parseSignedToken(token: string): Payload {} +``` + +**Correct (navigable reference):** + +```ts +/** + * Works with {@link TokenVerifier.verify} for signature validation. + */ +export function parseSignedToken(token: string): Payload {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-see.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-see.md new file mode 100644 index 0000000..739971b --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-see.md @@ -0,0 +1,33 @@ +--- +title: Use @see for Related APIs +impact: MEDIUM +impactDescription: connects related APIs and reduces duplicated docs +tags: tsdoc, see, references, api-navigation +--- + +## Use @see for Related APIs + +Use `@see` to point readers to related entry points, alternatives, or companion APIs. + +**Use this when:** relationship is important for correct API selection. +**Avoid this when:** references are irrelevant or redundant. + +**Incorrect (no relationship guidance):** + +```ts +/** + * Verifies token signatures. + */ +export function verifyToken(token: string): boolean {} +``` + +**Correct (points to related APIs):** + +```ts +/** + * Verifies token signatures. + * @see {@link decodeToken} + * @see {@link parseSignedToken} + */ +export function verifyToken(token: string): boolean {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-default-value.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-default-value.md new file mode 100644 index 0000000..0a4cd32 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-default-value.md @@ -0,0 +1,32 @@ +--- +title: Document Defaults with @defaultValue +impact: MEDIUM +impactDescription: prevents ambiguity in option and property behavior +tags: tsdoc, defaultValue, options, configuration +--- + +## Document Defaults with `@defaultValue` + +Use `@defaultValue` to document meaningful default behavior. + +**Use this when:** options/properties have user-visible defaults. +**Avoid this when:** defaults are unstable, implicit, or undocumented in implementation. + +**Incorrect (default hidden from docs):** + +```ts +/** + * Maximum retry attempts. + */ +export const DEFAULT_RETRY_ATTEMPTS = 3; +``` + +**Correct (explicit default):** + +```ts +/** + * Maximum retry attempts. + * @defaultValue 3 + */ +export const DEFAULT_RETRY_ATTEMPTS = 3; +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-modifier-tags.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-modifier-tags.md new file mode 100644 index 0000000..b628b71 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-modifier-tags.md @@ -0,0 +1,35 @@ +--- +title: Use Modifier Tags Only When Semantically True +impact: HIGH +impactDescription: prevents misleading API semantics in generated docs +tags: tsdoc, modifier-tags, readonly, override, virtual, sealed, decorator, eventProperty +--- + +## Use Modifier Tags Only When Semantically True + +Use modifier tags only when they match real API semantics. + +Common modifier tags: `@readonly`, `@override`, `@virtual`, `@sealed`, `@decorator`, `@eventProperty`. + +**Use this when:** code behavior and design contract actually match the tag. +**Avoid this when:** tags are decorative or used for emphasis. + +**Incorrect (tag conflicts with implementation):** + +```ts +/** + * Tracks current request count. + * @readonly + */ +export let requestCount = 0; +``` + +**Correct (tag matches behavior):** + +```ts +/** + * Current build version. + * @readonly + */ +export const BUILD_VERSION = "1.0.0"; +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md new file mode 100644 index 0000000..82cc164 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md @@ -0,0 +1,33 @@ +--- +title: Do Not Mix JSDoc Type Syntax into TSDoc +impact: CRITICAL +impactDescription: avoids parser/tooling incompatibilities and inconsistent style +tags: tsdoc, jsdoc, syntax, consistency +--- + +## Do Not Mix JSDoc Type Syntax into TSDoc + +In TSDoc, avoid JSDoc-style type braces in tag lines. + +**Use this when:** writing TSDoc in TypeScript codebases. +**Avoid this when:** authoring JavaScript files that intentionally use JSDoc tooling. + +**Incorrect (JSDoc-style braces in TSDoc):** + +```ts +/** + * @param {string} id - User identifier. + * @returns {Promise} User record. + */ +export async function getUser(id: string): Promise {} +``` + +**Correct (TSDoc syntax):** + +```ts +/** + * @param id - User identifier. + * @returns User record. + */ +export async function getUser(id: string): Promise {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-package-documentation.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-package-documentation.md new file mode 100644 index 0000000..d56c2f6 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-package-documentation.md @@ -0,0 +1,32 @@ +--- +title: Use @packageDocumentation for Entrypoint Docs +impact: MEDIUM +impactDescription: improves package-level discoverability and onboarding +tags: tsdoc, packageDocumentation, modules, entrypoints +--- + +## Use `@packageDocumentation` for Entrypoint Docs + +Use `@packageDocumentation` on module/entrypoint docs, not regular members. + +**Use this when:** documenting package/module purpose and usage at top level. +**Avoid this when:** documenting individual functions, classes, or fields. + +**Incorrect (tag on a normal function):** + +```ts +/** + * @packageDocumentation + * Creates a token. + */ +export function createToken(userId: string): string {} +``` + +**Correct (tag on entrypoint-level docs):** + +```ts +/** + * @packageDocumentation + * Authentication helpers for issuing, parsing, and validating signed tokens. + */ +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-private-remarks.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-private-remarks.md new file mode 100644 index 0000000..ab914a4 --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-private-remarks.md @@ -0,0 +1,34 @@ +--- +title: Use @privateRemarks for Maintainer-Only Notes +impact: MEDIUM +impactDescription: separates internal guidance from public API docs +tags: tsdoc, privateRemarks, maintainers, internals +--- + +## Use `@privateRemarks` for Maintainer-Only Notes + +Use `@privateRemarks` for internal notes that should not appear in public docs. + +**Use this when:** maintainers need migration or implementation caveats. +**Avoid this when:** information is essential for API consumers. + +**Incorrect (internal rollout note in public remarks):** + +```ts +/** + * @remarks + * Keep legacy payload shape until mobile v4 rollout finishes. + */ +export function serializeUser(user: User): Payload {} +``` + +**Correct (internal detail isolated):** + +```ts +/** + * Serializes user data for API responses. + * @privateRemarks + * Keep legacy payload shape until mobile v4 rollout finishes. + */ +export function serializeUser(user: User): Payload {} +``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-release-tags.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-release-tags.md new file mode 100644 index 0000000..6b3bfaf --- /dev/null +++ b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-release-tags.md @@ -0,0 +1,36 @@ +--- +title: Apply Release Tags Consistently +impact: CRITICAL +impactDescription: ensures stable public API lifecycle communication +tags: tsdoc, release-tags, public, internal, alpha, beta, experimental +--- + +## Apply Release Tags Consistently + +Apply one clear release/visibility policy and keep it consistent across related APIs. + +Supported policy tags include `@public`, `@internal`, `@alpha`, `@beta`, and `@experimental`. + +**Use this when:** API lifecycle and visibility matter to consumers. +**Avoid this when:** tags are mixed arbitrarily or conflict with actual support policy. + +**Incorrect (conflicting policy):** + +```ts +/** + * Creates session tokens. + * @public + * @internal + */ +export function createSessionToken(userId: string): string {} +``` + +**Correct (single clear policy):** + +```ts +/** + * Creates session tokens. + * @beta + */ +export function createSessionToken(userId: string): string {} +``` diff --git a/.agents/skills/vitest/GENERATION.md b/.agents/skills/vitest/GENERATION.md new file mode 100644 index 0000000..9bc7664 --- /dev/null +++ b/.agents/skills/vitest/GENERATION.md @@ -0,0 +1,5 @@ +# Generation Info + +- **Source:** `sources/vitest` +- **Git SHA:** `4a7321e10672f00f0bb698823a381c2cc245b8f7` +- **Generated:** 2026-01-28 diff --git a/.agents/skills/vitest/SKILL.md b/.agents/skills/vitest/SKILL.md new file mode 100644 index 0000000..d813e2c --- /dev/null +++ b/.agents/skills/vitest/SKILL.md @@ -0,0 +1,53 @@ +--- +name: vitest +description: Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures. +metadata: + author: Anthony Fu + version: "2026.1.28" + source: Generated from https://github.com/vitest-dev/vitest, scripts located at https://github.com/antfu/skills +--- + +Vitest is a next-generation testing framework powered by Vite. It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box. Vitest shares the same config, transformers, resolvers, and plugins with your Vite app. + +**Key Features:** + +- Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates +- Jest-compatible: Drop-in replacement for most Jest test suites +- Smart watch mode: Only reruns affected tests based on module graph +- Native ESM, TypeScript, JSX support without configuration +- Multi-threaded workers for parallel test execution +- Built-in coverage via V8 or Istanbul +- Snapshot testing, mocking, and spy utilities + +> The skill is based on Vitest 3.x, generated at 2026-01-28. + +## Core + +| Topic | Description | Reference | +| ------------- | --------------------------------------------------------------- | -------------------------------------------- | +| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) | +| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) | +| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) | +| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) | +| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) | +| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) | + +## Features + +| Topic | Description | Reference | +| ------------ | -------------------------------------------------------------- | ---------------------------------------------------------- | +| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) | +| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) | +| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) | +| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) | +| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) | +| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) | + +## Advanced + +| Topic | Description | Reference | +| ------------ | ------------------------------------------------------- | ------------------------------------------------------------ | +| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) | +| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) | +| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) | +| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) | diff --git a/.agents/skills/vitest/references/advanced-environments.md b/.agents/skills/vitest/references/advanced-environments.md new file mode 100644 index 0000000..69c60ca --- /dev/null +++ b/.agents/skills/vitest/references/advanced-environments.md @@ -0,0 +1,264 @@ +--- +name: test-environments +description: Configure environments like jsdom, happy-dom for browser APIs +--- + +# Test Environments + +## Available Environments + +- `node` (default) - Node.js environment +- `jsdom` - Browser-like with DOM APIs +- `happy-dom` - Faster alternative to jsdom +- `edge-runtime` - Vercel Edge Runtime + +## Configuration + +```ts +// vitest.config.ts +defineConfig({ + test: { + environment: "jsdom", + + // Environment-specific options + environmentOptions: { + jsdom: { + url: "http://localhost", + }, + }, + }, +}); +``` + +## Installing Environment Packages + +```bash +# jsdom +npm i -D jsdom + +# happy-dom (faster, fewer APIs) +npm i -D happy-dom +``` + +## Per-File Environment + +Use magic comment at top of file: + +```ts +// @vitest-environment jsdom + +import { expect, test } from "vitest"; + +test("DOM test", () => { + const div = document.createElement("div"); + expect(div).toBeInstanceOf(HTMLDivElement); +}); +``` + +## jsdom Environment + +Full browser environment simulation: + +```ts +// @vitest-environment jsdom + +test("DOM manipulation", () => { + document.body.innerHTML = '
'; + + const app = document.getElementById("app"); + app.textContent = "Hello"; + + expect(app.textContent).toBe("Hello"); +}); + +test("window APIs", () => { + expect(window.location.href).toBeDefined(); + expect(localStorage).toBeDefined(); +}); +``` + +### jsdom Options + +```ts +defineConfig({ + test: { + environmentOptions: { + jsdom: { + url: "http://localhost:3000", + html: "", + userAgent: "custom-agent", + resources: "usable", + }, + }, + }, +}); +``` + +## happy-dom Environment + +Faster but fewer APIs: + +```ts +// @vitest-environment happy-dom + +test("basic DOM", () => { + const el = document.createElement("div"); + el.className = "test"; + expect(el.className).toBe("test"); +}); +``` + +## Multiple Environments per Project + +Use projects for different environments: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: "unit", + include: ["tests/unit/**/*.test.ts"], + environment: "node", + }, + }, + { + test: { + name: "dom", + include: ["tests/dom/**/*.test.ts"], + environment: "jsdom", + }, + }, + ], + }, +}); +``` + +## Custom Environment + +Create custom environment package: + +```ts +// vitest-environment-custom/index.ts +import type { Environment } from "vitest/runtime"; + +export default { + name: "custom", + viteEnvironment: "ssr", // or 'client' + + setup() { + // Setup global state + globalThis.myGlobal = "value"; + + return { + teardown() { + delete globalThis.myGlobal; + }, + }; + }, +}; +``` + +Use with: + +```ts +defineConfig({ + test: { + environment: "custom", + }, +}); +``` + +## Environment with VM + +For full isolation: + +```ts +export default { + name: "isolated", + viteEnvironment: "ssr", + + async setupVM() { + const vm = await import("node:vm"); + const context = vm.createContext(); + + return { + getVmContext() { + return context; + }, + teardown() {}, + }; + }, + + setup() { + return { teardown() {} }; + }, +}; +``` + +## Browser Mode (Separate from Environments) + +For real browser testing, use Vitest Browser Mode: + +```ts +defineConfig({ + test: { + browser: { + enabled: true, + name: "chromium", // or 'firefox', 'webkit' + provider: "playwright", + }, + }, +}); +``` + +## CSS and Assets + +In jsdom/happy-dom, configure CSS handling: + +```ts +defineConfig({ + test: { + css: true, // Process CSS + + // Or with options + css: { + include: /\.module\.css$/, + modules: { + classNameStrategy: "non-scoped", + }, + }, + }, +}); +``` + +## Fixing External Dependencies + +If external deps fail with CSS/asset errors: + +```ts +defineConfig({ + test: { + server: { + deps: { + inline: ["problematic-package"], + }, + }, + }, +}); +``` + +## Key Points + +- Default is `node` - no browser APIs +- Use `jsdom` for full browser simulation +- Use `happy-dom` for faster tests with basic DOM +- Per-file environment via `// @vitest-environment` comment +- Use projects for multiple environment configurations +- Browser Mode is for real browser testing, not environment + + diff --git a/.agents/skills/vitest/references/advanced-projects.md b/.agents/skills/vitest/references/advanced-projects.md new file mode 100644 index 0000000..2b4d37f --- /dev/null +++ b/.agents/skills/vitest/references/advanced-projects.md @@ -0,0 +1,300 @@ +--- +name: projects-workspaces +description: Multi-project configuration for monorepos and different test types +--- + +# Projects + +Run different test configurations in the same Vitest process. + +## Basic Projects Setup + +```ts +// vitest.config.ts +defineConfig({ + test: { + projects: [ + // Glob patterns for config files + "packages/*", + + // Inline config + { + test: { + name: "unit", + include: ["tests/unit/**/*.test.ts"], + environment: "node", + }, + }, + { + test: { + name: "integration", + include: ["tests/integration/**/*.test.ts"], + environment: "jsdom", + }, + }, + ], + }, +}); +``` + +## Monorepo Pattern + +```ts +defineConfig({ + test: { + projects: [ + // Each package has its own vitest.config.ts + "packages/core", + "packages/cli", + "packages/utils", + ], + }, +}); +``` + +Package config: + +```ts +// packages/core/vitest.config.ts +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + name: "core", + include: ["src/**/*.test.ts"], + environment: "node", + }, +}); +``` + +## Different Environments + +Run same tests in different environments: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: "happy-dom", + root: "./shared-tests", + environment: "happy-dom", + setupFiles: ["./setup.happy-dom.ts"], + }, + }, + { + test: { + name: "node", + root: "./shared-tests", + environment: "node", + setupFiles: ["./setup.node.ts"], + }, + }, + ], + }, +}); +``` + +## Browser + Node Projects + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: "unit", + include: ["tests/unit/**/*.test.ts"], + environment: "node", + }, + }, + { + test: { + name: "browser", + include: ["tests/browser/**/*.test.ts"], + browser: { + enabled: true, + name: "chromium", + provider: "playwright", + }, + }, + }, + ], + }, +}); +``` + +## Shared Configuration + +```ts +// vitest.shared.ts +export const sharedConfig = { + testTimeout: 10000, + setupFiles: ["./tests/setup.ts"], +}; + +// vitest.config.ts +import { sharedConfig } from "./vitest.shared"; + +defineConfig({ + test: { + projects: [ + { + test: { + ...sharedConfig, + name: "unit", + include: ["tests/unit/**/*.test.ts"], + }, + }, + { + test: { + ...sharedConfig, + name: "e2e", + include: ["tests/e2e/**/*.test.ts"], + }, + }, + ], + }, +}); +``` + +## Project-Specific Dependencies + +Each project can have different dependencies inlined: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: "project-a", + server: { + deps: { + inline: ["package-a"], + }, + }, + }, + }, + ], + }, +}); +``` + +## Running Specific Projects + +```bash +# Run specific project +vitest --project unit +vitest --project integration + +# Multiple projects +vitest --project unit --project e2e + +# Exclude project +vitest --project.ignore browser +``` + +## Providing Values to Projects + +Share values from config to tests: + +```ts +// vitest.config.ts +defineConfig({ + test: { + projects: [ + { + test: { + name: "staging", + provide: { + apiUrl: "https://staging.api.com", + debug: true, + }, + }, + }, + { + test: { + name: "production", + provide: { + apiUrl: "https://api.com", + debug: false, + }, + }, + }, + ], + }, +}); + +// In tests, use inject +import { inject } from "vitest"; + +test("uses correct api", () => { + const url = inject("apiUrl"); + expect(url).toContain("api.com"); +}); +``` + +## With Fixtures + +```ts +const test = base.extend({ + apiUrl: ["/default", { injected: true }], +}); + +test("uses injected url", ({ apiUrl }) => { + // apiUrl comes from project's provide config +}); +``` + +## Project Isolation + +Each project runs in its own thread pool by default: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: "isolated", + isolate: true, // Full isolation + pool: "forks", + }, + }, + ], + }, +}); +``` + +## Global Setup per Project + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: "with-db", + globalSetup: ["./tests/db-setup.ts"], + }, + }, + ], + }, +}); +``` + +## Key Points + +- Projects run in same Vitest process +- Each project can have different environment, config +- Use glob patterns for monorepo packages +- Run specific projects with `--project` flag +- Use `provide` to inject config values into tests +- Projects inherit from root config unless overridden + + diff --git a/.agents/skills/vitest/references/advanced-type-testing.md b/.agents/skills/vitest/references/advanced-type-testing.md new file mode 100644 index 0000000..52dbd67 --- /dev/null +++ b/.agents/skills/vitest/references/advanced-type-testing.md @@ -0,0 +1,242 @@ +--- +name: type-testing +description: Test TypeScript types with expectTypeOf and assertType +--- + +# Type Testing + +Test TypeScript types without runtime execution. + +## Setup + +Type tests use `.test-d.ts` extension: + +```ts +// math.test-d.ts +import { expectTypeOf } from "vitest"; +import { add } from "./math"; + +test("add returns number", () => { + expectTypeOf(add).returns.toBeNumber(); +}); +``` + +## Configuration + +```ts +defineConfig({ + test: { + typecheck: { + enabled: true, + + // Only type check + only: false, + + // Checker: 'tsc' or 'vue-tsc' + checker: "tsc", + + // Include patterns + include: ["**/*.test-d.ts"], + + // tsconfig to use + tsconfig: "./tsconfig.json", + }, + }, +}); +``` + +## expectTypeOf API + +```ts +import { expectTypeOf } from "vitest"; + +// Basic type checks +expectTypeOf().toBeString(); +expectTypeOf().toBeNumber(); +expectTypeOf().toBeBoolean(); +expectTypeOf().toBeNull(); +expectTypeOf().toBeUndefined(); +expectTypeOf().toBeVoid(); +expectTypeOf().toBeNever(); +expectTypeOf().toBeAny(); +expectTypeOf().toBeUnknown(); +expectTypeOf().toBeObject(); +expectTypeOf().toBeFunction(); +expectTypeOf<[]>().toBeArray(); +expectTypeOf().toBeSymbol(); +``` + +## Value Type Checking + +```ts +const value = "hello"; +expectTypeOf(value).toBeString(); + +const obj = { name: "test", count: 42 }; +expectTypeOf(obj).toMatchTypeOf<{ name: string }>(); +expectTypeOf(obj).toHaveProperty("name"); +``` + +## Function Types + +```ts +function greet(name: string): string { + return `Hello, ${name}`; +} + +expectTypeOf(greet).toBeFunction(); +expectTypeOf(greet).parameters.toEqualTypeOf<[string]>(); +expectTypeOf(greet).returns.toBeString(); + +// Parameter checking +expectTypeOf(greet).parameter(0).toBeString(); +``` + +## Object Types + +```ts +interface User { + id: number; + name: string; + email?: string; +} + +expectTypeOf().toHaveProperty("id"); +expectTypeOf().toHaveProperty("name").toBeString(); + +// Check shape +expectTypeOf({ id: 1, name: "test" }).toMatchTypeOf(); +``` + +## Equality vs Matching + +```ts +interface A { + x: number; +} +interface B { + x: number; + y: string; +} + +// toMatchTypeOf - subset matching +expectTypeOf().toMatchTypeOf(); // B extends A + +// toEqualTypeOf - exact match +expectTypeOf().not.toEqualTypeOf(); // Not exact match +expectTypeOf().toEqualTypeOf<{ x: number }>(); // Exact match +``` + +## Branded Types + +```ts +type UserId = number & { __brand: "UserId" }; +type PostId = number & { __brand: "PostId" }; + +expectTypeOf().not.toEqualTypeOf(); +expectTypeOf().not.toEqualTypeOf(); +``` + +## Generic Types + +```ts +function identity(value: T): T { + return value; +} + +expectTypeOf(identity).returns.toBeString(); +expectTypeOf(identity).returns.toBeNumber(); +``` + +## Nullable Types + +```ts +type MaybeString = string | null | undefined; + +expectTypeOf().toBeNullable(); +expectTypeOf().not.toBeNullable(); +``` + +## assertType + +Assert a value matches a type (no assertion at runtime): + +```ts +import { assertType } from "vitest"; + +function getUser(): User | null { + return { id: 1, name: "test" }; +} + +test("returns user", () => { + const result = getUser(); + + // @ts-expect-error - should fail type check + assertType(result); + + // Correct type + assertType(result); +}); +``` + +## Using @ts-expect-error + +Test that code produces type error: + +```ts +test("rejects wrong types", () => { + function requireString(s: string) {} + + // @ts-expect-error - number not assignable to string + requireString(123); +}); +``` + +## Running Type Tests + +```bash +# Run type tests +vitest typecheck + +# Run alongside unit tests +vitest --typecheck + +# Type tests only +vitest --typecheck.only +``` + +## Mixed Test Files + +Combine runtime and type tests: + +```ts +// user.test.ts +import { describe, expect, expectTypeOf, test } from "vitest"; +import { createUser } from "./user"; + +describe("createUser", () => { + test("runtime: creates user", () => { + const user = createUser("John"); + expect(user.name).toBe("John"); + }); + + test("types: returns User type", () => { + expectTypeOf(createUser).returns.toMatchTypeOf<{ name: string }>(); + }); +}); +``` + +## Key Points + +- Use `.test-d.ts` for type-only tests +- `expectTypeOf` for type assertions +- `toMatchTypeOf` for subset matching +- `toEqualTypeOf` for exact type matching +- Use `@ts-expect-error` to test type errors +- Run with `vitest typecheck` or `--typecheck` + + diff --git a/.agents/skills/vitest/references/advanced-vi.md b/.agents/skills/vitest/references/advanced-vi.md new file mode 100644 index 0000000..51b549d --- /dev/null +++ b/.agents/skills/vitest/references/advanced-vi.md @@ -0,0 +1,251 @@ +--- +name: vi-utilities +description: vi helper for mocking, timers, utilities +--- + +# Vi Utilities + +The `vi` helper provides mocking and utility functions. + +```ts +import { vi } from "vitest"; +``` + +## Mock Functions + +```ts +// Create mock +const fn = vi.fn(); +const fnWithImpl = vi.fn((x) => x * 2); + +// Check if mock +vi.isMockFunction(fn); // true + +// Mock methods +fn.mockReturnValue(42); +fn.mockReturnValueOnce(1); +fn.mockResolvedValue(data); +fn.mockRejectedValue(error); +fn.mockImplementation(() => "result"); +fn.mockImplementationOnce(() => "once"); + +// Clear/reset +fn.mockClear(); // Clear call history +fn.mockReset(); // Clear history + implementation +fn.mockRestore(); // Restore original (for spies) +``` + +## Spying + +```ts +const obj = { method: () => "original" }; + +const spy = vi.spyOn(obj, "method"); +obj.method(); + +expect(spy).toHaveBeenCalled(); + +// Mock implementation +spy.mockReturnValue("mocked"); + +// Spy on getter/setter +vi.spyOn(obj, "prop", "get").mockReturnValue("value"); +``` + +## Module Mocking + +```ts +// Hoisted to top of file +vi.mock("./module", () => ({ + fn: vi.fn(), +})); + +// Partial mock +vi.mock("./module", async (importOriginal) => ({ + ...(await importOriginal()), + specificFn: vi.fn(), +})); + +// Spy mode - keep implementation +vi.mock("./module", { spy: true }); + +// Import actual module inside mock +const actual = await vi.importActual("./module"); + +// Import as mock +const mocked = await vi.importMock("./module"); +``` + +## Dynamic Mocking + +```ts +// Not hoisted - use with dynamic imports +vi.doMock("./config", () => ({ key: "value" })); +const config = await import("./config"); + +// Unmock +vi.doUnmock("./config"); +vi.unmock("./module"); // Hoisted +``` + +## Reset Modules + +```ts +// Clear module cache +vi.resetModules(); + +// Wait for dynamic imports +await vi.dynamicImportSettled(); +``` + +## Fake Timers + +```ts +vi.useFakeTimers(); + +setTimeout(() => console.log("done"), 1000); + +// Advance time +vi.advanceTimersByTime(1000); +vi.advanceTimersByTimeAsync(1000); // For async callbacks +vi.advanceTimersToNextTimer(); +vi.advanceTimersToNextFrame(); // requestAnimationFrame + +// Run all timers +vi.runAllTimers(); +vi.runAllTimersAsync(); +vi.runOnlyPendingTimers(); + +// Clear timers +vi.clearAllTimers(); + +// Check state +vi.getTimerCount(); +vi.isFakeTimers(); + +// Restore +vi.useRealTimers(); +``` + +## Mock Date/Time + +```ts +vi.setSystemTime(new Date("2024-01-01")); +expect(new Date().getFullYear()).toBe(2024); + +vi.getMockedSystemTime(); // Get mocked date +vi.getRealSystemTime(); // Get real time (ms) +``` + +## Global/Env Mocking + +```ts +// Stub global +vi.stubGlobal("fetch", vi.fn()); +vi.unstubAllGlobals(); + +// Stub environment +vi.stubEnv("API_KEY", "test"); +vi.stubEnv("NODE_ENV", "test"); +vi.unstubAllEnvs(); +``` + +## Hoisted Code + +Run code before imports: + +```ts +const mock = vi.hoisted(() => vi.fn()); + +vi.mock("./module", () => ({ + fn: mock, // Can reference hoisted variable +})); +``` + +## Waiting Utilities + +```ts +// Wait for callback to succeed +await vi.waitFor( + async () => { + const el = document.querySelector(".loaded"); + expect(el).toBeTruthy(); + }, + { timeout: 5000, interval: 100 } +); + +// Wait for truthy value +const element = await vi.waitUntil(() => document.querySelector(".loaded"), { + timeout: 5000, +}); +``` + +## Mock Object + +Mock all methods of an object: + +```ts +const original = { + method: () => "real", + nested: { fn: () => "nested" }, +}; + +const mocked = vi.mockObject(original); +mocked.method(); // undefined (mocked) +mocked.method.mockReturnValue("mocked"); + +// Spy mode +const spied = vi.mockObject(original, { spy: true }); +spied.method(); // 'real' +expect(spied.method).toHaveBeenCalled(); +``` + +## Test Configuration + +```ts +vi.setConfig({ + testTimeout: 10_000, + hookTimeout: 10_000, +}); + +vi.resetConfig(); +``` + +## Global Mock Management + +```ts +vi.clearAllMocks(); // Clear all mock call history +vi.resetAllMocks(); // Reset + clear implementation +vi.restoreAllMocks(); // Restore originals (spies) +``` + +## vi.mocked Type Helper + +TypeScript helper for mocked values: + +```ts +import { myFn } from "./module"; +vi.mock("./module"); + +// Type as mock +vi.mocked(myFn).mockReturnValue("typed"); + +// Deep mocking +vi.mocked(myModule, { deep: true }); + +// Partial mock typing +vi.mocked(fn, { partial: true }).mockResolvedValue({ ok: true }); +``` + +## Key Points + +- `vi.mock` is hoisted - use `vi.doMock` for dynamic mocking +- `vi.hoisted` lets you reference variables in mock factories +- Use `vi.spyOn` to spy on existing methods +- Fake timers require explicit setup and teardown +- `vi.waitFor` retries until assertion passes + + diff --git a/.agents/skills/vitest/references/core-cli.md b/.agents/skills/vitest/references/core-cli.md new file mode 100644 index 0000000..9985459 --- /dev/null +++ b/.agents/skills/vitest/references/core-cli.md @@ -0,0 +1,167 @@ +--- +name: vitest-cli +description: Command line interface commands and options +--- + +# Command Line Interface + +## Commands + +### `vitest` + +Start Vitest in watch mode (dev) or run mode (CI): + +```bash +vitest # Watch mode in dev, run mode in CI +vitest foobar # Run tests containing "foobar" in path +vitest basic/foo.test.ts:10 # Run specific test by file and line number +``` + +### `vitest run` + +Run tests once without watch mode: + +```bash +vitest run +vitest run --coverage +``` + +### `vitest watch` + +Explicitly start watch mode: + +```bash +vitest watch +``` + +### `vitest related` + +Run tests that import specific files (useful with lint-staged): + +```bash +vitest related src/index.ts src/utils.ts --run +``` + +### `vitest bench` + +Run only benchmark tests: + +```bash +vitest bench +``` + +### `vitest list` + +List all matching tests without running them: + +```bash +vitest list # List test names +vitest list --json # Output as JSON +vitest list --filesOnly # List only test files +``` + +### `vitest init` + +Initialize project setup: + +```bash +vitest init browser # Set up browser testing +``` + +## Common Options + +```bash +# Configuration +--config # Path to config file +--project # Run specific project + +# Filtering +--testNamePattern, -t # Run tests matching pattern +--changed # Run tests for changed files +--changed HEAD~1 # Tests for last commit changes + +# Reporters +--reporter # default, verbose, dot, json, html +--reporter=html --outputFile=report.html + +# Coverage +--coverage # Enable coverage +--coverage.provider v8 # Use v8 provider +--coverage.reporter text,html + +# Execution +--shard / # Split tests across machines +--bail # Stop after n failures +--retry # Retry failed tests n times +--sequence.shuffle # Randomize test order + +# Watch mode +--no-watch # Disable watch mode +--standalone # Start without running tests + +# Environment +--environment # jsdom, happy-dom, node +--globals # Enable global APIs + +# Debugging +--inspect # Enable Node inspector +--inspect-brk # Break on start + +# Output +--silent # Suppress console output +--no-color # Disable colors +``` + +## Package.json Scripts + +```json +{ + "scripts": { + "test": "vitest", + "test:run": "vitest run", + "test:ui": "vitest --ui", + "coverage": "vitest run --coverage" + } +} +``` + +## Sharding for CI + +Split tests across multiple machines: + +```bash +# Machine 1 +vitest run --shard=1/3 --reporter=blob + +# Machine 2 +vitest run --shard=2/3 --reporter=blob + +# Machine 3 +vitest run --shard=3/3 --reporter=blob + +# Merge reports +vitest --merge-reports --reporter=junit +``` + +## Watch Mode Keyboard Shortcuts + +In watch mode, press: + +- `a` - Run all tests +- `f` - Run only failed tests +- `u` - Update snapshots +- `p` - Filter by filename pattern +- `t` - Filter by test name pattern +- `q` - Quit + +## Key Points + +- Watch mode is default in dev, run mode in CI (when `process.env.CI` is set) +- Use `--run` flag to ensure single run (important for lint-staged) +- Both camelCase (`--testTimeout`) and kebab-case (`--test-timeout`) work +- Boolean options can be negated with `--no-` prefix + + diff --git a/.agents/skills/vitest/references/core-config.md b/.agents/skills/vitest/references/core-config.md new file mode 100644 index 0000000..4eae4c4 --- /dev/null +++ b/.agents/skills/vitest/references/core-config.md @@ -0,0 +1,177 @@ +--- +name: vitest-configuration +description: Configure Vitest with vite.config.ts or vitest.config.ts +--- + +# Configuration + +Vitest reads configuration from `vitest.config.ts` or `vite.config.ts`. It shares the same config format as Vite. + +## Basic Setup + +```ts +// vitest.config.ts +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + // test options + }, +}); +``` + +## Using with Existing Vite Config + +Add Vitest types reference and use the `test` property: + +```ts +// vite.config.ts +/// +import { defineConfig } from "vite"; + +export default defineConfig({ + test: { + globals: true, + environment: "jsdom", + }, +}); +``` + +## Merging Configs + +If you have separate config files, use `mergeConfig`: + +```ts +// vitest.config.ts +import { defineConfig, mergeConfig } from "vitest/config"; +import viteConfig from "./vite.config"; + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + environment: "jsdom", + }, + }) +); +``` + +## Common Options + +```ts +defineConfig({ + test: { + // Enable global APIs (describe, it, expect) without imports + globals: true, + + // Test environment: 'node', 'jsdom', 'happy-dom' + environment: "node", + + // Setup files to run before each test file + setupFiles: ["./tests/setup.ts"], + + // Include patterns for test files + include: ["**/*.{test,spec}.{js,ts,jsx,tsx}"], + + // Exclude patterns + exclude: ["**/node_modules/**", "**/dist/**"], + + // Test timeout in ms + testTimeout: 5000, + + // Hook timeout in ms + hookTimeout: 10000, + + // Enable watch mode by default + watch: true, + + // Coverage configuration + coverage: { + provider: "v8", // or 'istanbul' + reporter: ["text", "html"], + include: ["src/**/*.ts"], + }, + + // Run tests in isolation (each file in separate process) + isolate: true, + + // Pool for running tests: 'threads', 'forks', 'vmThreads' + pool: "threads", + + // Number of threads/processes + poolOptions: { + threads: { + maxThreads: 4, + minThreads: 1, + }, + }, + + // Automatically clear mocks between tests + clearMocks: true, + + // Restore mocks between tests + restoreMocks: true, + + // Retry failed tests + retry: 0, + + // Stop after first failure + bail: 0, + }, +}); +``` + +## Conditional Configuration + +Use `mode` or `process.env.VITEST` for test-specific config: + +```ts +export default defineConfig(({ mode }) => ({ + plugins: mode === "test" ? [] : [myPlugin()], + test: { + // test options + }, +})); +``` + +## Projects (Monorepos) + +Run different configurations in the same Vitest process: + +```ts +defineConfig({ + test: { + projects: [ + "packages/*", + { + test: { + name: "unit", + include: ["tests/unit/**/*.test.ts"], + environment: "node", + }, + }, + { + test: { + name: "integration", + include: ["tests/integration/**/*.test.ts"], + environment: "jsdom", + }, + }, + ], + }, +}); +``` + +## Key Points + +- Vitest uses Vite's transformation pipeline - same `resolve.alias`, plugins work +- `vitest.config.ts` takes priority over `vite.config.ts` +- Use `--config` flag to specify a custom config path +- `process.env.VITEST` is set to `true` when running tests +- Test config uses `test` property, rest is Vite config + + diff --git a/.agents/skills/vitest/references/core-describe.md b/.agents/skills/vitest/references/core-describe.md new file mode 100644 index 0000000..80a1a46 --- /dev/null +++ b/.agents/skills/vitest/references/core-describe.md @@ -0,0 +1,193 @@ +--- +name: describe-api +description: describe/suite for grouping tests into logical blocks +--- + +# Describe API + +Group related tests into suites for organization and shared setup. + +## Basic Usage + +```ts +import { describe, expect, test } from "vitest"; + +describe("Math", () => { + test("adds numbers", () => { + expect(1 + 1).toBe(2); + }); + + test("subtracts numbers", () => { + expect(3 - 1).toBe(2); + }); +}); + +// Alias: suite +import { suite } from "vitest"; +suite("equivalent to describe", () => {}); +``` + +## Nested Suites + +```ts +describe("User", () => { + describe("when logged in", () => { + test("shows dashboard", () => {}); + test("can update profile", () => {}); + }); + + describe("when logged out", () => { + test("shows login page", () => {}); + }); +}); +``` + +## Suite Options + +```ts +// All tests inherit options +describe("slow tests", { timeout: 30_000 }, () => { + test("test 1", () => {}); // 30s timeout + test("test 2", () => {}); // 30s timeout +}); +``` + +## Suite Modifiers + +### Skip Suites + +```ts +describe.skip("skipped suite", () => { + test("wont run", () => {}); +}); + +// Conditional +describe.skipIf(process.env.CI)("not in CI", () => {}); +describe.runIf(!process.env.CI)("only local", () => {}); +``` + +### Focus Suites + +```ts +describe.only("only this suite runs", () => { + test("runs", () => {}); +}); +``` + +### Todo Suites + +```ts +describe.todo("implement later"); +``` + +### Concurrent Suites + +```ts +// All tests run in parallel +describe.concurrent("parallel tests", () => { + test("test 1", async ({ expect }) => {}); + test("test 2", async ({ expect }) => {}); +}); +``` + +### Sequential in Concurrent + +```ts +describe.concurrent("parallel", () => { + test("concurrent 1", async () => {}); + + describe.sequential("must be sequential", () => { + test("step 1", async () => {}); + test("step 2", async () => {}); + }); +}); +``` + +### Shuffle Tests + +```ts +describe.shuffle("random order", () => { + test("test 1", () => {}); + test("test 2", () => {}); + test("test 3", () => {}); +}); + +// Or with option +describe("random", { shuffle: true }, () => {}); +``` + +## Parameterized Suites + +### describe.each + +```ts +describe.each([ + { name: "Chrome", version: 100 }, + { name: "Firefox", version: 90 }, +])("$name browser", ({ name, version }) => { + test("has version", () => { + expect(version).toBeGreaterThan(0); + }); +}); +``` + +### describe.for + +```ts +describe.for([ + ["Chrome", 100], + ["Firefox", 90], +])("%s browser", ([name, version]) => { + test("has version", () => { + expect(version).toBeGreaterThan(0); + }); +}); +``` + +## Hooks in Suites + +```ts +describe("Database", () => { + let db; + + beforeAll(async () => { + db = await createDb(); + }); + + afterAll(async () => { + await db.close(); + }); + + beforeEach(async () => { + await db.clear(); + }); + + test("insert works", async () => { + await db.insert({ name: "test" }); + expect(await db.count()).toBe(1); + }); +}); +``` + +## Modifier Combinations + +All modifiers can be chained: + +```ts +describe.skip.concurrent("skipped concurrent", () => {}); +describe.only.shuffle("only and shuffled", () => {}); +describe.concurrent.skip("equivalent", () => {}); +``` + +## Key Points + +- Top-level tests belong to an implicit file suite +- Nested suites inherit parent's options (timeout, retry, etc.) +- Hooks are scoped to their suite and nested suites +- Use `describe.concurrent` with context's `expect` for snapshots +- Shuffle order depends on `sequence.seed` config + + diff --git a/.agents/skills/vitest/references/core-expect.md b/.agents/skills/vitest/references/core-expect.md new file mode 100644 index 0000000..306e6a7 --- /dev/null +++ b/.agents/skills/vitest/references/core-expect.md @@ -0,0 +1,211 @@ +--- +name: expect-api +description: Assertions with matchers, asymmetric matchers, and custom matchers +--- + +# Expect API + +Vitest uses Chai assertions with Jest-compatible API. + +## Basic Assertions + +```ts +import { expect, test } from "vitest"; + +test("assertions", () => { + // Equality + expect(1 + 1).toBe(2); // Strict equality (===) + expect({ a: 1 }).toEqual({ a: 1 }); // Deep equality + + // Truthiness + expect(true).toBeTruthy(); + expect(false).toBeFalsy(); + expect(null).toBeNull(); + expect(undefined).toBeUndefined(); + expect("value").toBeDefined(); + + // Numbers + expect(10).toBeGreaterThan(5); + expect(10).toBeGreaterThanOrEqual(10); + expect(5).toBeLessThan(10); + expect(0.1 + 0.2).toBeCloseTo(0.3, 5); + + // Strings + expect("hello world").toMatch(/world/); + expect("hello").toContain("ell"); + + // Arrays + expect([1, 2, 3]).toContain(2); + expect([{ a: 1 }]).toContainEqual({ a: 1 }); + expect([1, 2, 3]).toHaveLength(3); + + // Objects + expect({ a: 1, b: 2 }).toHaveProperty("a"); + expect({ a: 1, b: 2 }).toHaveProperty("a", 1); + expect({ a: { b: 1 } }).toHaveProperty("a.b", 1); + expect({ a: 1 }).toMatchObject({ a: 1 }); + + // Types + expect("string").toBeTypeOf("string"); + expect(new Date()).toBeInstanceOf(Date); +}); +``` + +## Negation + +```ts +expect(1).not.toBe(2); +expect({ a: 1 }).not.toEqual({ a: 2 }); +``` + +## Error Assertions + +```ts +// Sync errors - wrap in function +expect(() => throwError()).toThrow(); +expect(() => throwError()).toThrow("message"); +expect(() => throwError()).toThrow(/pattern/); +expect(() => throwError()).toThrow(CustomError); + +// Async errors - use rejects +await expect(asyncThrow()).rejects.toThrow("error"); +``` + +## Promise Assertions + +```ts +// Resolves +await expect(Promise.resolve(1)).resolves.toBe(1); +await expect(fetchData()).resolves.toEqual({ data: true }); + +// Rejects +await expect(Promise.reject("error")).rejects.toBe("error"); +await expect(failingFetch()).rejects.toThrow(); +``` + +## Spy/Mock Assertions + +```ts +const fn = vi.fn(); +fn("arg1", "arg2"); +fn("arg3"); + +expect(fn).toHaveBeenCalled(); +expect(fn).toHaveBeenCalledTimes(2); +expect(fn).toHaveBeenCalledWith("arg1", "arg2"); +expect(fn).toHaveBeenLastCalledWith("arg3"); +expect(fn).toHaveBeenNthCalledWith(1, "arg1", "arg2"); + +expect(fn).toHaveReturned(); +expect(fn).toHaveReturnedWith(value); +``` + +## Asymmetric Matchers + +Use inside `toEqual`, `toHaveBeenCalledWith`, etc: + +```ts +expect({ id: 1, name: "test" }).toEqual({ + id: expect.any(Number), + name: expect.any(String), +}); + +expect({ a: 1, b: 2, c: 3 }).toEqual(expect.objectContaining({ a: 1 })); + +expect([1, 2, 3, 4]).toEqual(expect.arrayContaining([1, 3])); + +expect("hello world").toEqual(expect.stringContaining("world")); + +expect("hello world").toEqual(expect.stringMatching(/world$/)); + +expect({ value: null }).toEqual({ + value: expect.anything(), // Matches anything except null/undefined +}); + +// Negate with expect.not +expect([1, 2]).toEqual(expect.not.arrayContaining([3])); +``` + +## Soft Assertions + +Continue test after failure: + +```ts +expect.soft(1).toBe(2); // Marks test failed but continues +expect.soft(2).toBe(3); // Also runs +// All failures reported at end +``` + +## Poll Assertions + +Retry until passes: + +```ts +await expect.poll(() => fetchStatus()).toBe("ready"); + +await expect + .poll(() => document.querySelector(".element"), { + interval: 100, + timeout: 5000, + }) + .toBeTruthy(); +``` + +## Assertion Count + +```ts +test("async assertions", async () => { + expect.assertions(2); // Exactly 2 assertions must run + + await doAsync((data) => { + expect(data).toBeDefined(); + expect(data.id).toBe(1); + }); +}); + +test("at least one", () => { + expect.hasAssertions(); // At least 1 assertion must run +}); +``` + +## Extending Matchers + +```ts +expect.extend({ + toBeWithinRange(received, floor, ceiling) { + const pass = received >= floor && received <= ceiling; + return { + pass, + message: () => + `expected ${received} to be within range ${floor} - ${ceiling}`, + }; + }, +}); + +test("custom matcher", () => { + expect(100).toBeWithinRange(90, 110); +}); +``` + +## Snapshot Assertions + +```ts +expect(data).toMatchSnapshot() +expect(data).toMatchInlineSnapshot(`{ "id": 1 }`) +await expect(result).toMatchFileSnapshot('./expected.json') + +expect(() => throw new Error('fail')).toThrowErrorMatchingSnapshot() +``` + +## Key Points + +- Use `toBe` for primitives, `toEqual` for objects/arrays +- `toStrictEqual` checks undefined properties and array sparseness +- Always `await` async assertions (`resolves`, `rejects`, `poll`) +- Use context's `expect` in concurrent tests for correct tracking +- `toThrow` requires wrapping sync code in a function + + diff --git a/.agents/skills/vitest/references/core-hooks.md b/.agents/skills/vitest/references/core-hooks.md new file mode 100644 index 0000000..4f34893 --- /dev/null +++ b/.agents/skills/vitest/references/core-hooks.md @@ -0,0 +1,245 @@ +--- +name: lifecycle-hooks +description: beforeEach, afterEach, beforeAll, afterAll, and around hooks +--- + +# Lifecycle Hooks + +## Basic Hooks + +```ts +import { afterAll, afterEach, beforeAll, beforeEach, test } from "vitest"; + +beforeAll(async () => { + // Runs once before all tests in file/suite + await setupDatabase(); +}); + +afterAll(async () => { + // Runs once after all tests in file/suite + await teardownDatabase(); +}); + +beforeEach(async () => { + // Runs before each test + await clearTestData(); +}); + +afterEach(async () => { + // Runs after each test + await cleanupMocks(); +}); +``` + +## Cleanup Return Pattern + +Return cleanup function from `before*` hooks: + +```ts +beforeAll(async () => { + const server = await startServer(); + + // Returned function runs as afterAll + return async () => { + await server.close(); + }; +}); + +beforeEach(async () => { + const connection = await connect(); + + // Runs as afterEach + return () => connection.close(); +}); +``` + +## Scoped Hooks + +Hooks apply to current suite and nested suites: + +```ts +describe("outer", () => { + beforeEach(() => console.log("outer before")); + + test("test 1", () => {}); // outer before → test + + describe("inner", () => { + beforeEach(() => console.log("inner before")); + + test("test 2", () => {}); // outer before → inner before → test + }); +}); +``` + +## Hook Timeout + +```ts +beforeAll(async () => { + await slowSetup(); +}, 30_000); // 30 second timeout +``` + +## Around Hooks + +Wrap tests with setup/teardown context: + +```ts +import { aroundEach, test } from "vitest"; + +// Wrap each test in database transaction +aroundEach(async (runTest) => { + await db.beginTransaction(); + await runTest(); // Must be called! + await db.rollback(); +}); + +test("insert user", async () => { + await db.insert({ name: "Alice" }); + // Automatically rolled back after test +}); +``` + +### aroundAll + +Wrap entire suite: + +```ts +import { aroundAll, test } from "vitest"; + +aroundAll(async (runSuite) => { + console.log("before all tests"); + await runSuite(); // Must be called! + console.log("after all tests"); +}); +``` + +### Multiple Around Hooks + +Nested like onion layers: + +```ts +aroundEach(async (runTest) => { + console.log("outer before"); + await runTest(); + console.log("outer after"); +}); + +aroundEach(async (runTest) => { + console.log("inner before"); + await runTest(); + console.log("inner after"); +}); + +// Order: outer before → inner before → test → inner after → outer after +``` + +## Test Hooks + +Inside test body: + +```ts +import { onTestFailed, onTestFinished, test } from "vitest"; + +test("with cleanup", () => { + const db = connect(); + + // Runs after test finishes (pass or fail) + onTestFinished(() => db.close()); + + // Only runs if test fails + onTestFailed(({ task }) => { + console.log("Failed:", task.result?.errors); + }); + + db.query("SELECT * FROM users"); +}); +``` + +### Reusable Cleanup Pattern + +```ts +function useTestDb() { + const db = connect(); + onTestFinished(() => db.close()); + return db; +} + +test("query users", () => { + const db = useTestDb(); + expect(db.query("SELECT * FROM users")).toBeDefined(); +}); + +test("query orders", () => { + const db = useTestDb(); // Fresh connection, auto-closed + expect(db.query("SELECT * FROM orders")).toBeDefined(); +}); +``` + +## Concurrent Test Hooks + +For concurrent tests, use context's hooks: + +```ts +test.concurrent("concurrent", ({ onTestFinished }) => { + const resource = allocate(); + onTestFinished(() => resource.release()); +}); +``` + +## Extended Test Hooks + +With `test.extend`, hooks are type-aware: + +```ts +const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { + const db = await createDb(); + await use(db); + await db.close(); + }, +}); + +// These hooks know about `db` fixture +test.beforeEach(({ db }) => { + db.seed(); +}); + +test.afterEach(({ db }) => { + db.clear(); +}); +``` + +## Hook Execution Order + +Default order (stack): + +1. `beforeAll` (in order) +2. `beforeEach` (in order) +3. Test +4. `afterEach` (reverse order) +5. `afterAll` (reverse order) + +Configure with `sequence.hooks`: + +```ts +defineConfig({ + test: { + sequence: { + hooks: "list", // 'stack' (default), 'list', 'parallel' + }, + }, +}); +``` + +## Key Points + +- Hooks are not called during type checking +- Return cleanup function from `before*` to avoid `after*` duplication +- `aroundEach`/`aroundAll` must call `runTest()`/`runSuite()` +- `onTestFinished` always runs, even if test fails +- Use context hooks for concurrent tests + + diff --git a/.agents/skills/vitest/references/core-test-api.md b/.agents/skills/vitest/references/core-test-api.md new file mode 100644 index 0000000..bdf65b0 --- /dev/null +++ b/.agents/skills/vitest/references/core-test-api.md @@ -0,0 +1,237 @@ +--- +name: test-api +description: test/it function for defining tests with modifiers +--- + +# Test API + +## Basic Test + +```ts +import { expect, test } from "vitest"; + +test("adds numbers", () => { + expect(1 + 1).toBe(2); +}); + +// Alias: it +import { it } from "vitest"; + +it("works the same", () => { + expect(true).toBe(true); +}); +``` + +## Async Tests + +```ts +test("async test", async () => { + const result = await fetchData(); + expect(result).toBeDefined(); +}); + +// Promises are automatically awaited +test("returns promise", () => { + return fetchData().then((result) => { + expect(result).toBeDefined(); + }); +}); +``` + +## Test Options + +```ts +// Timeout (default: 5000ms) +test("slow test", async () => { + // ... +}, 10_000); + +// Or with options object +test("with options", { timeout: 10_000, retry: 2 }, async () => { + // ... +}); +``` + +## Test Modifiers + +### Skip Tests + +```ts +test.skip("skipped test", () => { + // Won't run +}); + +// Conditional skip +test.skipIf(process.env.CI)("not in CI", () => {}); +test.runIf(process.env.CI)("only in CI", () => {}); + +// Dynamic skip via context +test("dynamic skip", ({ skip }) => { + skip(someCondition, "reason"); + // ... +}); +``` + +### Focus Tests + +```ts +test.only("only this runs", () => { + // Other tests in file are skipped +}); +``` + +### Todo Tests + +```ts +test.todo("implement later"); + +test.todo("with body", () => { + // Not run, shows in report +}); +``` + +### Failing Tests + +```ts +test.fails("expected to fail", () => { + expect(1).toBe(2); // Test passes because assertion fails +}); +``` + +### Concurrent Tests + +```ts +// Run tests in parallel +test.concurrent("test 1", async ({ expect }) => { + // Use context.expect for concurrent tests + expect(await fetch1()).toBe("result"); +}); + +test.concurrent("test 2", async ({ expect }) => { + expect(await fetch2()).toBe("result"); +}); +``` + +### Sequential Tests + +```ts +// Force sequential in concurrent context +test.sequential("must run alone", async () => {}); +``` + +## Parameterized Tests + +### test.each + +```ts +test.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])("add(%i, %i) = %i", (a, b, expected) => { + expect(a + b).toBe(expected); +}); + +// With objects +test.each([ + { a: 1, b: 1, expected: 2 }, + { a: 1, b: 2, expected: 3 }, +])("add($a, $b) = $expected", ({ a, b, expected }) => { + expect(a + b).toBe(expected); +}); + +// Template literal +test.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} +`("add($a, $b) = $expected", ({ a, b, expected }) => { + expect(a + b).toBe(expected); +}); +``` + +### test.for + +Preferred over `.each` - doesn't spread arrays: + +```ts +test.for([ + [1, 1, 2], + [1, 2, 3], +])("add(%i, %i) = %i", ([a, b, expected], { expect }) => { + // Second arg is TestContext + expect(a + b).toBe(expected); +}); +``` + +## Test Context + +First argument provides context utilities: + +```ts +test("with context", ({ expect, skip, task }) => { + console.log(task.name); // Test name + skip(someCondition); // Skip dynamically + expect(1).toBe(1); // Context-bound expect +}); +``` + +## Custom Test with Fixtures + +```ts +import { test as base } from "vitest"; + +const test = base.extend({ + db: async ({}, use) => { + const db = await createDb(); + await use(db); + await db.close(); + }, +}); + +test("query", async ({ db }) => { + const users = await db.query("SELECT * FROM users"); + expect(users).toBeDefined(); +}); +``` + +## Retry Configuration + +```ts +test("flaky test", { retry: 3 }, async () => { + // Retries up to 3 times on failure +}); + +// Advanced retry options +test( + "with delay", + { + retry: { + count: 3, + delay: 1000, + condition: /timeout/i, // Only retry on timeout errors + }, + }, + async () => {} +); +``` + +## Tags + +```ts +test("database test", { tags: ["db", "slow"] }, async () => {}); + +// Run with: vitest --tags db +``` + +## Key Points + +- Tests with no body are marked as `todo` +- `test.only` throws in CI unless `allowOnly: true` +- Use context's `expect` for concurrent tests and snapshots +- Function name is used as test name if passed as first arg + + diff --git a/.agents/skills/vitest/references/features-concurrency.md b/.agents/skills/vitest/references/features-concurrency.md new file mode 100644 index 0000000..956a709 --- /dev/null +++ b/.agents/skills/vitest/references/features-concurrency.md @@ -0,0 +1,250 @@ +--- +name: concurrency-parallelism +description: Concurrent tests, parallel execution, and sharding +--- + +# Concurrency & Parallelism + +## File Parallelism + +By default, Vitest runs test files in parallel across workers: + +```ts +defineConfig({ + test: { + // Run files in parallel (default: true) + fileParallelism: true, + + // Number of worker threads + maxWorkers: 4, + minWorkers: 1, + + // Pool type: 'threads', 'forks', 'vmThreads' + pool: "threads", + }, +}); +``` + +## Concurrent Tests + +Run tests within a file in parallel: + +```ts +// Individual concurrent tests +test.concurrent("test 1", async ({ expect }) => { + expect(await fetch1()).toBe("result"); +}); + +test.concurrent("test 2", async ({ expect }) => { + expect(await fetch2()).toBe("result"); +}); + +// All tests in suite concurrent +describe.concurrent("parallel suite", () => { + test("test 1", async ({ expect }) => {}); + test("test 2", async ({ expect }) => {}); +}); +``` + +**Important:** Use `{ expect }` from context for concurrent tests. + +## Sequential in Concurrent Context + +Force sequential execution: + +```ts +describe.concurrent("mostly parallel", () => { + test("parallel 1", async () => {}); + test("parallel 2", async () => {}); + + test.sequential("must run alone 1", async () => {}); + test.sequential("must run alone 2", async () => {}); +}); + +// Or entire suite +describe.sequential("sequential suite", () => { + test("first", () => {}); + test("second", () => {}); +}); +``` + +## Max Concurrency + +Limit concurrent tests: + +```ts +defineConfig({ + test: { + maxConcurrency: 5, // Max concurrent tests per file + }, +}); +``` + +## Isolation + +Each file runs in isolated environment by default: + +```ts +defineConfig({ + test: { + // Disable isolation for faster runs (less safe) + isolate: false, + }, +}); +``` + +## Sharding + +Split tests across machines: + +```bash +# Machine 1 +vitest run --shard=1/3 + +# Machine 2 +vitest run --shard=2/3 + +# Machine 3 +vitest run --shard=3/3 +``` + +### CI Example (GitHub Actions) + +```yaml +jobs: + test: + strategy: + matrix: + shard: [1, 2, 3] + steps: + - run: vitest run --shard=${{ matrix.shard }}/3 --reporter=blob + + merge: + needs: test + steps: + - run: vitest --merge-reports --reporter=junit +``` + +### Merge Reports + +```bash +# Each shard outputs blob +vitest run --shard=1/3 --reporter=blob --coverage +vitest run --shard=2/3 --reporter=blob --coverage + +# Merge all blobs +vitest --merge-reports --reporter=json --coverage +``` + +## Test Sequence + +Control test order: + +```ts +defineConfig({ + test: { + sequence: { + // Run tests in random order + shuffle: true, + + // Seed for reproducible shuffle + seed: 12345, + + // Hook execution order + hooks: "stack", // 'stack', 'list', 'parallel' + + // All tests concurrent by default + concurrent: true, + }, + }, +}); +``` + +## Shuffle Tests + +Randomize to catch hidden dependencies: + +```ts +// Via CLI +vitest --sequence.shuffle + +// Per suite +describe.shuffle('random order', () => { + test('test 1', () => {}) + test('test 2', () => {}) + test('test 3', () => {}) +}) +``` + +## Pool Options + +### Threads (Default) + +```ts +defineConfig({ + test: { + pool: "threads", + poolOptions: { + threads: { + maxThreads: 8, + minThreads: 2, + isolate: true, + }, + }, + }, +}); +``` + +### Forks + +Better isolation, slower: + +```ts +defineConfig({ + test: { + pool: "forks", + poolOptions: { + forks: { + maxForks: 4, + isolate: true, + }, + }, + }, +}); +``` + +### VM Threads + +Full VM isolation per file: + +```ts +defineConfig({ + test: { + pool: "vmThreads", + }, +}); +``` + +## Bail on Failure + +Stop after first failure: + +```bash +vitest --bail 1 # Stop after 1 failure +vitest --bail # Stop on first failure (same as --bail 1) +``` + +## Key Points + +- Files run in parallel by default +- Use `.concurrent` for parallel tests within file +- Always use context's `expect` in concurrent tests +- Sharding splits tests across CI machines +- Use `--merge-reports` to combine sharded results +- Shuffle tests to find hidden dependencies + + diff --git a/.agents/skills/vitest/references/features-context.md b/.agents/skills/vitest/references/features-context.md new file mode 100644 index 0000000..73732ce --- /dev/null +++ b/.agents/skills/vitest/references/features-context.md @@ -0,0 +1,240 @@ +--- +name: test-context-fixtures +description: Test context, custom fixtures with test.extend +--- + +# Test Context & Fixtures + +## Built-in Context + +Every test receives context as first argument: + +```ts +test("context", ({ task, expect, skip }) => { + console.log(task.name); // Test name + expect(1).toBe(1); // Context-bound expect + skip(); // Skip test dynamically +}); +``` + +### Context Properties + +- `task` - Test metadata (name, file, etc.) +- `expect` - Expect bound to this test (important for concurrent tests) +- `skip(condition?, message?)` - Skip the test +- `onTestFinished(fn)` - Cleanup after test +- `onTestFailed(fn)` - Run on failure only + +## Custom Fixtures with test.extend + +Create reusable test utilities: + +```ts +import { test as base } from "vitest"; + +// Define fixture types +interface Fixtures { + db: Database; + user: User; +} + +// Create extended test +export const test = base.extend({ + // Fixture with setup/teardown + db: async ({}, use) => { + const db = await createDatabase(); + await use(db); // Provide to test + await db.close(); // Cleanup + }, + + // Fixture depending on another fixture + user: async ({ db }, use) => { + const user = await db.createUser({ name: "Test" }); + await use(user); + await db.deleteUser(user.id); + }, +}); +``` + +Using fixtures: + +```ts +test("query user", async ({ db, user }) => { + const found = await db.findUser(user.id); + expect(found).toEqual(user); +}); +``` + +## Fixture Initialization + +Fixtures only initialize when accessed: + +```ts +const test = base.extend({ + expensive: async ({}, use) => { + console.log("initializing"); // Only runs if test uses it + await use("value"); + }, +}); + +test("no fixture", () => {}); // expensive not called +test("uses fixture", ({ expensive }) => {}); // expensive called +``` + +## Auto Fixtures + +Run fixture for every test: + +```ts +const test = base.extend({ + setup: [ + async ({}, use) => { + await globalSetup(); + await use(); + await globalTeardown(); + }, + { auto: true }, // Always run + ], +}); +``` + +## Scoped Fixtures + +### File Scope + +Initialize once per file: + +```ts +const test = base.extend({ + connection: [ + async ({}, use) => { + const conn = await connect(); + await use(conn); + await conn.close(); + }, + { scope: "file" }, + ], +}); +``` + +### Worker Scope + +Initialize once per worker: + +```ts +const test = base.extend({ + sharedResource: [ + async ({}, use) => { + await use(globalResource); + }, + { scope: "worker" }, + ], +}); +``` + +## Injected Fixtures (from Config) + +Override fixtures per project: + +```ts +// test file +const test = base.extend({ + apiUrl: ["/default", { injected: true }], +}); + +// vitest.config.ts +defineConfig({ + test: { + projects: [ + { + test: { + name: "prod", + provide: { apiUrl: "https://api.prod.com" }, + }, + }, + ], + }, +}); +``` + +## Scoped Values per Suite + +Override fixture for specific suite: + +```ts +const test = base.extend({ + environment: "development", +}); + +describe("production tests", () => { + test.scoped({ environment: "production" }); + + test("uses production", ({ environment }) => { + expect(environment).toBe("production"); + }); +}); + +test("uses default", ({ environment }) => { + expect(environment).toBe("development"); +}); +``` + +## Extended Test Hooks + +Type-aware hooks with fixtures: + +```ts +const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { + const db = await createDb(); + await use(db); + await db.close(); + }, +}); + +// Hooks know about fixtures +test.beforeEach(({ db }) => { + db.seed(); +}); + +test.afterEach(({ db }) => { + db.clear(); +}); +``` + +## Composing Fixtures + +Extend from another extended test: + +```ts +// base-test.ts +export const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { + /* ... */ + }, +}); + +// admin-test.ts +import { test as dbTest } from "./base-test"; + +export const test = dbTest.extend<{ admin: User }>({ + admin: async ({ db }, use) => { + const admin = await db.createAdmin(); + await use(admin); + }, +}); +``` + +## Key Points + +- Use `{ }` destructuring to access fixtures +- Fixtures are lazy - only initialize when accessed +- Return cleanup function from fixtures +- Use `{ auto: true }` for setup fixtures +- Use `{ scope: 'file' }` for expensive shared resources +- Fixtures compose - extend from extended tests + + diff --git a/.agents/skills/vitest/references/features-coverage.md b/.agents/skills/vitest/references/features-coverage.md new file mode 100644 index 0000000..6167c2d --- /dev/null +++ b/.agents/skills/vitest/references/features-coverage.md @@ -0,0 +1,202 @@ +--- +name: code-coverage +description: Code coverage with V8 or Istanbul providers +--- + +# Code Coverage + +## Setup + +```bash +# Run tests with coverage +vitest run --coverage +``` + +## Configuration + +```ts +// vitest.config.ts +defineConfig({ + test: { + coverage: { + // Provider: 'v8' (default, faster) or 'istanbul' (more compatible) + provider: "v8", + + // Enable coverage + enabled: true, + + // Reporters + reporter: ["text", "json", "html"], + + // Files to include + include: ["src/**/*.{ts,tsx}"], + + // Files to exclude + exclude: ["node_modules/", "tests/", "**/*.d.ts", "**/*.test.ts"], + + // Report uncovered files + all: true, + + // Thresholds + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + }, + }, + }, +}); +``` + +## Providers + +### V8 (Default) + +```bash +npm i -D @vitest/coverage-v8 +``` + +- Faster, no pre-instrumentation +- Uses V8's native coverage +- Recommended for most projects + +### Istanbul + +```bash +npm i -D @vitest/coverage-istanbul +``` + +- Pre-instruments code +- Works in any JS runtime +- More overhead but widely compatible + +## Reporters + +```ts +coverage: { + reporter: [ + 'text', // Terminal output + 'text-summary', // Summary only + 'json', // JSON file + 'html', // HTML report + 'lcov', // For CI tools + 'cobertura', // XML format + ], + reportsDirectory: './coverage', +} +``` + +## Thresholds + +Fail tests if coverage is below threshold: + +```ts +coverage: { + thresholds: { + // Global thresholds + lines: 80, + functions: 75, + branches: 70, + statements: 80, + + // Per-file thresholds + perFile: true, + + // Auto-update thresholds (for gradual improvement) + autoUpdate: true, + }, +} +``` + +## Ignoring Code + +### V8 + +```ts +/* v8 ignore next -- @preserve */ +function ignored() { + return "not covered"; +} + +/* v8 ignore start -- @preserve */ +// All code here ignored +/* v8 ignore stop -- @preserve */ +``` + +### Istanbul + +```ts +/* istanbul ignore next -- @preserve */ +function ignored() {} + +/* istanbul ignore if -- @preserve */ +if (condition) { + // ignored +} +``` + +Note: `@preserve` keeps comments through esbuild. + +## Package.json Scripts + +```json +{ + "scripts": { + "test": "vitest", + "test:coverage": "vitest run --coverage", + "test:coverage:watch": "vitest --coverage" + } +} +``` + +## Vitest UI Coverage + +Enable HTML coverage in Vitest UI: + +```ts +coverage: { + enabled: true, + reporter: ['text', 'html'], +} +``` + +Run with `vitest --ui` to view coverage visually. + +## CI Integration + +```yaml +# GitHub Actions +- name: Run tests with coverage + run: npm run test:coverage + +- name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage/lcov.info +``` + +## Coverage with Sharding + +Merge coverage from sharded runs: + +```bash +vitest run --shard=1/3 --coverage --reporter=blob +vitest run --shard=2/3 --coverage --reporter=blob +vitest run --shard=3/3 --coverage --reporter=blob + +vitest --merge-reports --coverage --reporter=json +``` + +## Key Points + +- V8 is faster, Istanbul is more compatible +- Use `--coverage` flag or `coverage.enabled: true` +- Include `all: true` to see uncovered files +- Set thresholds to enforce minimum coverage +- Use `@preserve` comment to keep ignore hints + + diff --git a/.agents/skills/vitest/references/features-filtering.md b/.agents/skills/vitest/references/features-filtering.md new file mode 100644 index 0000000..ad79909 --- /dev/null +++ b/.agents/skills/vitest/references/features-filtering.md @@ -0,0 +1,208 @@ +--- +name: test-filtering +description: Filter tests by name, file patterns, and tags +--- + +# Test Filtering + +## CLI Filtering + +### By File Path + +```bash +# Run files containing "user" +vitest user + +# Multiple patterns +vitest user auth + +# Specific file +vitest src/user.test.ts + +# By line number +vitest src/user.test.ts:25 +``` + +### By Test Name + +```bash +# Tests matching pattern +vitest -t "login" +vitest --testNamePattern "should.*work" + +# Regex patterns +vitest -t "/user|auth/" +``` + +## Changed Files + +```bash +# Uncommitted changes +vitest --changed + +# Since specific commit +vitest --changed HEAD~1 +vitest --changed abc123 + +# Since branch +vitest --changed origin/main +``` + +## Related Files + +Run tests that import specific files: + +```bash +vitest related src/utils.ts src/api.ts --run +``` + +Useful with lint-staged: + +```js +// .lintstagedrc.js +export default { + "*.{ts,tsx}": "vitest related --run", +}; +``` + +## Focus Tests (.only) + +```ts +test.only("only this runs", () => {}); + +describe.only("only this suite", () => { + test("runs", () => {}); +}); +``` + +In CI, `.only` throws error unless configured: + +```ts +defineConfig({ + test: { + allowOnly: true, // Allow .only in CI + }, +}); +``` + +## Skip Tests + +```ts +test.skip("skipped", () => {}); + +// Conditional +test.skipIf(process.env.CI)("not in CI", () => {}); +test.runIf(!process.env.CI)("local only", () => {}); + +// Dynamic skip +test("dynamic", ({ skip }) => { + skip(someCondition, "reason"); +}); +``` + +## Tags + +Filter by custom tags: + +```ts +test("database test", { tags: ["db"] }, () => {}); +test("slow test", { tags: ["slow", "integration"] }, () => {}); +``` + +Run tagged tests: + +```bash +vitest --tags db +vitest --tags "db,slow" # OR +vitest --tags db --tags slow # OR +``` + +Configure allowed tags: + +```ts +defineConfig({ + test: { + tags: ["db", "slow", "integration"], + strictTags: true, // Fail on unknown tags + }, +}); +``` + +## Include/Exclude Patterns + +```ts +defineConfig({ + test: { + // Test file patterns + include: ["**/*.{test,spec}.{ts,tsx}"], + + // Exclude patterns + exclude: ["**/node_modules/**", "**/e2e/**", "**/*.skip.test.ts"], + + // Include source for in-source testing + includeSource: ["src/**/*.ts"], + }, +}); +``` + +## Watch Mode Filtering + +In watch mode, press: + +- `p` - Filter by filename pattern +- `t` - Filter by test name pattern +- `a` - Run all tests +- `f` - Run only failed tests + +## Projects Filtering + +Run specific project: + +```bash +vitest --project unit +vitest --project integration --project e2e +``` + +## Environment-based Filtering + +```ts +const isDev = process.env.NODE_ENV === "development"; +const isCI = process.env.CI; + +describe.skipIf(isCI)("local only tests", () => {}); +describe.runIf(isDev)("dev tests", () => {}); +``` + +## Combining Filters + +```bash +# File pattern + test name + changed +vitest user -t "login" --changed + +# Related files + run mode +vitest related src/auth.ts --run +``` + +## List Tests Without Running + +```bash +vitest list # Show all test names +vitest list -t "user" # Filter by name +vitest list --filesOnly # Show only file paths +vitest list --json # JSON output +``` + +## Key Points + +- Use `-t` for test name pattern filtering +- `--changed` runs only tests affected by changes +- `--related` runs tests importing specific files +- Tags provide semantic test grouping +- Use `.only` for debugging, but configure CI to reject it +- Watch mode has interactive filtering + + diff --git a/.agents/skills/vitest/references/features-mocking.md b/.agents/skills/vitest/references/features-mocking.md new file mode 100644 index 0000000..4c9e4c7 --- /dev/null +++ b/.agents/skills/vitest/references/features-mocking.md @@ -0,0 +1,272 @@ +--- +name: mocking +description: Mock functions, modules, timers, and dates with vi utilities +--- + +# Mocking + +## Mock Functions + +```ts +import { expect, vi } from "vitest"; + +// Create mock function +const fn = vi.fn(); +fn("hello"); + +expect(fn).toHaveBeenCalled(); +expect(fn).toHaveBeenCalledWith("hello"); + +// With implementation +const add = vi.fn((a, b) => a + b); +expect(add(1, 2)).toBe(3); + +// Mock return values +fn.mockReturnValue(42); +fn.mockReturnValueOnce(1).mockReturnValueOnce(2); +fn.mockResolvedValue({ data: true }); +fn.mockRejectedValue(new Error("fail")); + +// Mock implementation +fn.mockImplementation((x) => x * 2); +fn.mockImplementationOnce(() => "first call"); +``` + +## Spying on Objects + +```ts +const cart = { + getTotal: () => 100, +}; + +const spy = vi.spyOn(cart, "getTotal"); +cart.getTotal(); + +expect(spy).toHaveBeenCalled(); + +// Mock implementation +spy.mockReturnValue(200); +expect(cart.getTotal()).toBe(200); + +// Restore original +spy.mockRestore(); +``` + +## Module Mocking + +```ts +// vi.mock is hoisted to top of file +vi.mock("./api", () => ({ + fetchUser: vi.fn(() => ({ id: 1, name: "Mock" })), +})); + +import { fetchUser } from "./api"; + +test("mocked module", () => { + expect(fetchUser()).toEqual({ id: 1, name: "Mock" }); +}); +``` + +### Partial Mock + +```ts +vi.mock("./utils", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + specificFunction: vi.fn(), + }; +}); +``` + +### Auto-mock with Spy + +```ts +// Keep implementation but spy on calls +vi.mock("./calculator", { spy: true }); + +import { add } from "./calculator"; + +test("spy on module", () => { + const result = add(1, 2); // Real implementation + expect(result).toBe(3); + expect(add).toHaveBeenCalledWith(1, 2); +}); +``` + +### Manual Mocks (**mocks**) + +``` +src/ + __mocks__/ + axios.ts # Mocks 'axios' + api/ + __mocks__/ + client.ts # Mocks './client' + client.ts +``` + +```ts +// Just call vi.mock with no factory +vi.mock("axios"); +vi.mock("./api/client"); +``` + +## Dynamic Mocking (vi.doMock) + +Not hoisted - use for dynamic imports: + +```ts +test("dynamic mock", async () => { + vi.doMock("./config", () => ({ + apiUrl: "http://test.local", + })); + + const { apiUrl } = await import("./config"); + expect(apiUrl).toBe("http://test.local"); + + vi.doUnmock("./config"); +}); +``` + +## Mock Timers + +```ts +import { afterEach, beforeEach, vi } from "vitest"; + +beforeEach(() => { + vi.useFakeTimers(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +test("timers", () => { + const fn = vi.fn(); + setTimeout(fn, 1000); + + expect(fn).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(1000); + expect(fn).toHaveBeenCalled(); +}); + +// Other timer methods +vi.runAllTimers(); // Run all pending timers +vi.runOnlyPendingTimers(); // Run only currently pending +vi.advanceTimersToNextTimer(); // Advance to next timer +``` + +### Async Timer Methods + +```ts +test("async timers", async () => { + vi.useFakeTimers(); + + let resolved = false; + setTimeout( + () => + Promise.resolve().then(() => { + resolved = true; + }), + 100 + ); + + await vi.advanceTimersByTimeAsync(100); + expect(resolved).toBe(true); +}); +``` + +## Mock Dates + +```ts +vi.setSystemTime(new Date("2024-01-01")); +expect(new Date().getFullYear()).toBe(2024); + +vi.useRealTimers(); // Restore +``` + +## Mock Globals + +```ts +vi.stubGlobal( + "fetch", + vi.fn(() => Promise.resolve({ json: () => ({ data: "mock" }) })) +); + +// Restore +vi.unstubAllGlobals(); +``` + +## Mock Environment Variables + +```ts +vi.stubEnv("API_KEY", "test-key"); +expect(import.meta.env.API_KEY).toBe("test-key"); + +// Restore +vi.unstubAllEnvs(); +``` + +## Clearing Mocks + +```ts +const fn = vi.fn(); +fn(); + +fn.mockClear(); // Clear call history +fn.mockReset(); // Clear history + implementation +fn.mockRestore(); // Restore original (for spies) + +// Global +vi.clearAllMocks(); +vi.resetAllMocks(); +vi.restoreAllMocks(); +``` + +## Config Auto-Reset + +```ts +// vitest.config.ts +defineConfig({ + test: { + clearMocks: true, // Clear before each test + mockReset: true, // Reset before each test + restoreMocks: true, // Restore after each test + unstubEnvs: true, // Restore env vars + unstubGlobals: true, // Restore globals + }, +}); +``` + +## Hoisted Variables for Mocks + +```ts +const mockFn = vi.hoisted(() => vi.fn()); + +vi.mock("./module", () => ({ + getData: mockFn, +})); + +import { getData } from "./module"; + +test("hoisted mock", () => { + mockFn.mockReturnValue("test"); + expect(getData()).toBe("test"); +}); +``` + +## Key Points + +- `vi.mock` is hoisted - called before imports +- Use `vi.doMock` for dynamic, non-hoisted mocking +- Always restore mocks to avoid test pollution +- Use `{ spy: true }` to keep implementation but track calls +- `vi.hoisted` lets you reference variables in mock factories + + diff --git a/.agents/skills/vitest/references/features-snapshots.md b/.agents/skills/vitest/references/features-snapshots.md new file mode 100644 index 0000000..2e51223 --- /dev/null +++ b/.agents/skills/vitest/references/features-snapshots.md @@ -0,0 +1,207 @@ +--- +name: snapshot-testing +description: Snapshot testing with file, inline, and file snapshots +--- + +# Snapshot Testing + +Snapshot tests capture output and compare against stored references. + +## Basic Snapshot + +```ts +import { expect, test } from "vitest"; + +test("snapshot", () => { + const result = generateOutput(); + expect(result).toMatchSnapshot(); +}); +``` + +First run creates `.snap` file: + +```js +// __snapshots__/test.spec.ts.snap +exports["snapshot 1"] = ` +{ + "id": 1, + "name": "test" +} +`; +``` + +## Inline Snapshots + +Stored directly in test file: + +```ts +test("inline snapshot", () => { + const data = { foo: "bar" }; + expect(data).toMatchInlineSnapshot(); +}); +``` + +Vitest updates the test file: + +```ts +test("inline snapshot", () => { + const data = { foo: "bar" }; + expect(data).toMatchInlineSnapshot(` + { + "foo": "bar", + } + `); +}); +``` + +## File Snapshots + +Compare against explicit file: + +```ts +test("render html", async () => { + const html = renderComponent(); + await expect(html).toMatchFileSnapshot("./expected/component.html"); +}); +``` + +## Snapshot Hints + +Add descriptive hints: + +```ts +test("multiple snapshots", () => { + expect(header).toMatchSnapshot("header"); + expect(body).toMatchSnapshot("body content"); + expect(footer).toMatchSnapshot("footer"); +}); +``` + +## Object Shape Matching + +Match partial structure: + +```ts +test("shape snapshot", () => { + const data = { + id: Math.random(), + created: new Date(), + name: "test", + }; + + expect(data).toMatchSnapshot({ + id: expect.any(Number), + created: expect.any(Date), + }); +}); +``` + +## Error Snapshots + +```ts +test("error message", () => { + expect(() => { + throw new Error("Something went wrong"); + }).toThrowErrorMatchingSnapshot(); +}); + +test("inline error", () => { + expect(() => { + throw new Error("Bad input"); + }).toThrowErrorMatchingInlineSnapshot(`[Error: Bad input]`); +}); +``` + +## Updating Snapshots + +```bash +# Update all snapshots +vitest -u +vitest --update + +# In watch mode, press 'u' to update failed snapshots +``` + +## Custom Serializers + +Add custom snapshot formatting: + +```ts +expect.addSnapshotSerializer({ + test(val) { + return val && typeof val.toJSON === "function"; + }, + serialize(val, config, indentation, depth, refs, printer) { + return printer(val.toJSON(), config, indentation, depth, refs); + }, +}); +``` + +Or via config: + +```ts +// vitest.config.ts +defineConfig({ + test: { + snapshotSerializers: ["./my-serializer.ts"], + }, +}); +``` + +## Snapshot Format Options + +```ts +defineConfig({ + test: { + snapshotFormat: { + printBasicPrototype: false, // Don't print Array/Object prototypes + escapeString: false, + }, + }, +}); +``` + +## Concurrent Test Snapshots + +Use context's expect: + +```ts +test.concurrent("concurrent 1", async ({ expect }) => { + expect(await getData()).toMatchSnapshot(); +}); + +test.concurrent("concurrent 2", async ({ expect }) => { + expect(await getOther()).toMatchSnapshot(); +}); +``` + +## Snapshot File Location + +Default: `__snapshots__/.snap` + +Customize: + +```ts +defineConfig({ + test: { + resolveSnapshotPath: (testPath, snapExtension) => { + return testPath.replace("__tests__", "__snapshots__") + snapExtension; + }, + }, +}); +``` + +## Key Points + +- Commit snapshot files to version control +- Review snapshot changes in code review +- Use hints for multiple snapshots in one test +- Use `toMatchFileSnapshot` for large outputs (HTML, JSON) +- Inline snapshots auto-update in test file +- Use context's `expect` for concurrent tests + + diff --git a/.changeset/config.json b/.changeset/config.json index 3fb9a10..3698b62 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,11 +1,11 @@ { - "$schema": "https://unpkg.com/@changesets/config@latest/schema.json", - "changelog": "@changesets/cli/changelog", - "commit": false, - "fixed": [], - "linked": [], - "ignore": [], - "access": "public", - "baseBranch": "main", - "updateInternalDependencies": "patch" + "$schema": "https://unpkg.com/@changesets/config@latest/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "ignore": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch" } diff --git a/.claude/skills/tsdoc-jsdoc-authoring b/.claude/skills/tsdoc-jsdoc-authoring new file mode 120000 index 0000000..74d9f11 --- /dev/null +++ b/.claude/skills/tsdoc-jsdoc-authoring @@ -0,0 +1 @@ +../../.agents/skills/tsdoc-jsdoc-authoring \ No newline at end of file diff --git a/.claude/skills/vitest b/.claude/skills/vitest new file mode 120000 index 0000000..7661536 --- /dev/null +++ b/.claude/skills/vitest @@ -0,0 +1 @@ +../../.agents/skills/vitest \ No newline at end of file diff --git a/.cursor/hooks.json b/.cursor/hooks.json index 486fd75..0a92b6a 100644 --- a/.cursor/hooks.json +++ b/.cursor/hooks.json @@ -7,4 +7,4 @@ } ] } -} \ No newline at end of file +} diff --git a/.cursor/skills/tsdoc-jsdoc-authoring b/.cursor/skills/tsdoc-jsdoc-authoring new file mode 120000 index 0000000..74d9f11 --- /dev/null +++ b/.cursor/skills/tsdoc-jsdoc-authoring @@ -0,0 +1 @@ +../../.agents/skills/tsdoc-jsdoc-authoring \ No newline at end of file diff --git a/.cursor/skills/vitest b/.cursor/skills/vitest new file mode 120000 index 0000000..7661536 --- /dev/null +++ b/.cursor/skills/vitest @@ -0,0 +1 @@ +../../.agents/skills/vitest \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3af9d82..edccfb4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - registry-url: 'https://registry.npmjs.org' + registry-url: "https://registry.npmjs.org" - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc index f52b3c1..b1bb9b1 100644 --- a/.oxfmtrc.jsonc +++ b/.oxfmtrc.jsonc @@ -18,6 +18,6 @@ "experimentalSortImports": { "ignoreCase": true, "newlinesBetween": true, - "order": "asc" - } + "order": "asc", + }, } diff --git a/.oxlintrc.json b/.oxlintrc.json index c435a02..ef7d70e 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,6 +1,19 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", - "extends": [ - "./node_modules/ultracite/config/oxlint/core/.oxlintrc.json" - ] -} \ No newline at end of file + "extends": ["./node_modules/ultracite/config/oxlint/core/.oxlintrc.json"], + "rules": { + "complexity": "off", + "func-style": "off", + "import/no-nodejs-modules": "off", + "import/no-relative-parent-imports": "off", + "jest/max-expects": "off", + "jest/require-hook": "off", + "jest/valid-title": "off", + "max-statements": "off", + "no-nested-ternary": "off", + "no-shadow": "off", + "no-useless-return": "off", + "promise/prefer-await-to-callbacks": "off", + "unicorn/no-useless-fallback-in-spread": "off" + } +} diff --git a/examples/basic/README.md b/examples/basic/README.md index 60e3b0e..8dba017 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -28,9 +28,9 @@ npm install acme-toolkit 1. Import the toolkit and initialize your client. ```ts -import { createClient } from 'acme-toolkit' +import { createClient } from "acme-toolkit"; -const client = createClient() +const client = createClient(); ``` ## Support diff --git a/examples/basic/readie.json b/examples/basic/readie.json index ea0cf3a..7c76ce4 100644 --- a/examples/basic/readie.json +++ b/examples/basic/readie.json @@ -11,19 +11,11 @@ "link": "https://www.npmjs.com/package/acme-toolkit" } ], - "features": [ - "Typed APIs", - "Fast local setup", - "Works in Node.js and Bun" - ], - "installation": [ - "```bash\nnpm install acme-toolkit\n```" - ], + "features": ["Typed APIs", "Fast local setup", "Works in Node.js and Bun"], + "installation": ["```bash\nnpm install acme-toolkit\n```"], "usage": [ "Import the toolkit and initialize your client.", "```ts\nimport { createClient } from 'acme-toolkit'\n\nconst client = createClient()\n```" ], - "support": [ - "Open an issue in your repository tracker." - ] + "support": ["Open an issue in your repository tracker."] } diff --git a/examples/c15t/README.md b/examples/c15t/README.md index cd2ec38..a0ee31a 100644 --- a/examples/c15t/README.md +++ b/examples/c15t/README.md @@ -74,7 +74,7 @@ To manually install, follow the guide in our [docs – manual setup](https://c15 ```tsx // App.tsx -import { ConsentManagerProvider, CookieBanner } from '@c15t/react' +import { ConsentManagerProvider, CookieBanner } from "@c15t/react"; function App() { return ( @@ -82,7 +82,7 @@ function App() { - ) + ); } ``` diff --git a/examples/c15t/old.md b/examples/c15t/old.md index cd2ec38..a0ee31a 100644 --- a/examples/c15t/old.md +++ b/examples/c15t/old.md @@ -74,7 +74,7 @@ To manually install, follow the guide in our [docs – manual setup](https://c15 ```tsx // App.tsx -import { ConsentManagerProvider, CookieBanner } from '@c15t/react' +import { ConsentManagerProvider, CookieBanner } from "@c15t/react"; function App() { return ( @@ -82,7 +82,7 @@ function App() { - ) + ); } ``` diff --git a/examples/c15t/readie.global.json b/examples/c15t/readie.global.json index 929e860..289d8e3 100644 --- a/examples/c15t/readie.global.json +++ b/examples/c15t/readie.global.json @@ -1,67 +1,67 @@ { - "$schema": "../../schemas/readie.global.schema.json", - "banner": "

\n \n \n \n \"c15t\n \n \n
\n

{{title}}

\n

", - "badges": [ - { - "label": "GitHub stars", - "image": "https://img.shields.io/github/stars/c15t/c15t?style=flat-square", - "link": "https://github.com/c15t/c15t" - }, - { - "label": "CI", - "image": "https://img.shields.io/github/actions/workflow/status/c15t/c15t/ci.yml?style=flat-square", - "link": "https://github.com/c15t/c15t/actions/workflows/ci.yml" - }, - { - "label": "License", - "image": "https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square", - "link": "https://github.com/c15t/c15t/blob/main/LICENSE.md" - }, - { - "label": "Discord", - "image": "https://img.shields.io/discord/1312171102268690493?style=flat-square", - "link": "https://c15t.link/discord" - }, - { - "label": "npm version", - "image": "https://img.shields.io/npm/v/%40c15t%2Freact?style=flat-square", - "link": "https://www.npmjs.com/package/@c15t/react" - }, - { - "label": "Top Language", - "image": "https://img.shields.io/github/languages/top/c15t/c15t?style=flat-square", - "link": "https://github.com/c15t/c15t" - }, - { - "label": "Last Commit", - "image": "https://img.shields.io/github/last-commit/c15t/c15t?style=flat-square", - "link": "https://github.com/c15t/c15t/commits/main" - }, - { - "label": "Open Issues", - "image": "https://img.shields.io/github/issues/c15t/c15t?style=flat-square", - "link": "https://github.com/c15t/c15t/issues" - } - ], - "support": [ - "Join our [Discord community](https://c15t.link/discord)", - "Open an issue on our [GitHub repository](https://github.com/c15t/c15t/issues)", - "Visit [consent.io](https://consent.io) and use the chat widget", - "Contact our support team via email [support@consent.io](mailto:support@consent.io)" - ], - "contributing": [ - "We're open to all community contributions!", - "Read our [Contribution Guidelines](https://c15t.com/docs/oss/contributing)", - "Review our [Code of Conduct](https://c15t.com/docs/oss/code-of-conduct)", - "Fork the repository", - "Create a new branch for your feature", - "Submit a pull request", - "**All contributions, big or small, are welcome and appreciated!**" - ], - "security": "If you believe you have found a security vulnerability in c15t, we encourage you to **_responsibly disclose this and NOT open a public issue_**. We will investigate all legitimate reports.\n\nOur preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software. To do this, please visit [https://github.com/c15t/c15t/security](https://github.com/c15t/c15t/security) and click the \"Report a vulnerability\" button.\n\n### Security Policy\n\n- Please do not share security vulnerabilities in public forums, issues, or pull requests\n- Provide detailed information about the potential vulnerability\n- Allow reasonable time for us to address the issue before any public disclosure\n- We are committed to addressing security concerns promptly and transparently", - "license": { - "name": "GNU General Public License v3.0", - "url": "https://github.com/c15t/c15t/blob/main/LICENSE.md" - }, - "footer": "---\n\n**Built with ❤️ by the [consent.io](https://www.consent.io?utm_source=github&utm_medium=repopage_{{packageNameEncoded}}) team**" + "$schema": "../../schemas/readie.global.schema.json", + "banner": "

\n \n \n \n \"c15t\n \n \n
\n

{{title}}

\n

", + "badges": [ + { + "label": "GitHub stars", + "image": "https://img.shields.io/github/stars/c15t/c15t?style=flat-square", + "link": "https://github.com/c15t/c15t" + }, + { + "label": "CI", + "image": "https://img.shields.io/github/actions/workflow/status/c15t/c15t/ci.yml?style=flat-square", + "link": "https://github.com/c15t/c15t/actions/workflows/ci.yml" + }, + { + "label": "License", + "image": "https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square", + "link": "https://github.com/c15t/c15t/blob/main/LICENSE.md" + }, + { + "label": "Discord", + "image": "https://img.shields.io/discord/1312171102268690493?style=flat-square", + "link": "https://c15t.link/discord" + }, + { + "label": "npm version", + "image": "https://img.shields.io/npm/v/%40c15t%2Freact?style=flat-square", + "link": "https://www.npmjs.com/package/@c15t/react" + }, + { + "label": "Top Language", + "image": "https://img.shields.io/github/languages/top/c15t/c15t?style=flat-square", + "link": "https://github.com/c15t/c15t" + }, + { + "label": "Last Commit", + "image": "https://img.shields.io/github/last-commit/c15t/c15t?style=flat-square", + "link": "https://github.com/c15t/c15t/commits/main" + }, + { + "label": "Open Issues", + "image": "https://img.shields.io/github/issues/c15t/c15t?style=flat-square", + "link": "https://github.com/c15t/c15t/issues" + } + ], + "support": [ + "Join our [Discord community](https://c15t.link/discord)", + "Open an issue on our [GitHub repository](https://github.com/c15t/c15t/issues)", + "Visit [consent.io](https://consent.io) and use the chat widget", + "Contact our support team via email [support@consent.io](mailto:support@consent.io)" + ], + "contributing": [ + "We're open to all community contributions!", + "Read our [Contribution Guidelines](https://c15t.com/docs/oss/contributing)", + "Review our [Code of Conduct](https://c15t.com/docs/oss/code-of-conduct)", + "Fork the repository", + "Create a new branch for your feature", + "Submit a pull request", + "**All contributions, big or small, are welcome and appreciated!**" + ], + "security": "If you believe you have found a security vulnerability in c15t, we encourage you to **_responsibly disclose this and NOT open a public issue_**. We will investigate all legitimate reports.\n\nOur preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software. To do this, please visit [https://github.com/c15t/c15t/security](https://github.com/c15t/c15t/security) and click the \"Report a vulnerability\" button.\n\n### Security Policy\n\n- Please do not share security vulnerabilities in public forums, issues, or pull requests\n- Provide detailed information about the potential vulnerability\n- Allow reasonable time for us to address the issue before any public disclosure\n- We are committed to addressing security concerns promptly and transparently", + "license": { + "name": "GNU General Public License v3.0", + "url": "https://github.com/c15t/c15t/blob/main/LICENSE.md" + }, + "footer": "---\n\n**Built with ❤️ by the [consent.io](https://www.consent.io?utm_source=github&utm_medium=repopage_{{packageNameEncoded}}) team**" } diff --git a/examples/c15t/readie.json b/examples/c15t/readie.json index aa4a948..d2b1c47 100644 --- a/examples/c15t/readie.json +++ b/examples/c15t/readie.json @@ -1,38 +1,38 @@ { - "$schema": "../../schemas/readie.schema.json", - "title": "@c15t/react: React Consent Components", - "description": "Developer-first CMP for React: cookie banner, consent manager, preferences centre. GDPR ready with minimal setup and rich customization", - "includeTableOfContents": false, - "features": [ - "Works with React 19, 18, 17, and 16.8", - "Full 'use client' support for React Server Components", - "Headless and fully customizable UI components", - "Automatic GDPR compliance", - "Minimal configuration with TypeScript-first design", - "Comprehensive Consent Management Platform (CMP)", - "Flexible Cookie Banner and Preference Center components", - "Built-in internationalization support", - "Seamless consent storage and tracking" - ], - "prerequisites": [ - "React 16.8 or later", - "Node.js 18.17.0 or later", - "A hosted [c15t instance](https://consent.io) (free sign-up) or [self-hosted deployment](https://c15t.com/docs/self-host/v2)" - ], - "manualInstallation": [ - "", - "```bash\npnpm add @c15t/react\n```", - "", - "To manually install, follow the guide in our [docs – manual setup](https://c15t.com/docs/frameworks/react/quickstart#manual-setup)." - ], - "quickStart": "Easiest setup with @c15t/cli:\n\n```bash\n# Generate schema and code\npnpm dlx @c15t/cli generate\n# Alternatives:\n# npx @c15t/cli generate\n# bunx --bun @c15t/cli generate\n```\n\nThe CLI will:\n\n- Install necessary packages\n- Configure your c15t instance\n- Set up environment variables\n- Add consent management components to your app", - "usage": [ - "Import `ConsentManagerProvider` in your app's root component", - "Add `CookieBanner` and `PreferenceCenter` components", - "Customise styling and behaviour to fit your app", - "For full implementation details, see the [React quickstart docs](https://c15t.com/docs/frameworks/react/quickstart)", + "$schema": "../../schemas/readie.schema.json", + "title": "@c15t/react: React Consent Components", + "description": "Developer-first CMP for React: cookie banner, consent manager, preferences centre. GDPR ready with minimal setup and rich customization", + "includeTableOfContents": false, + "features": [ + "Works with React 19, 18, 17, and 16.8", + "Full 'use client' support for React Server Components", + "Headless and fully customizable UI components", + "Automatic GDPR compliance", + "Minimal configuration with TypeScript-first design", + "Comprehensive Consent Management Platform (CMP)", + "Flexible Cookie Banner and Preference Center components", + "Built-in internationalization support", + "Seamless consent storage and tracking" + ], + "prerequisites": [ + "React 16.8 or later", + "Node.js 18.17.0 or later", + "A hosted [c15t instance](https://consent.io) (free sign-up) or [self-hosted deployment](https://c15t.com/docs/self-host/v2)" + ], + "manualInstallation": [ + "", + "```bash\npnpm add @c15t/react\n```", + "", + "To manually install, follow the guide in our [docs – manual setup](https://c15t.com/docs/frameworks/react/quickstart#manual-setup)." + ], + "quickStart": "Easiest setup with @c15t/cli:\n\n```bash\n# Generate schema and code\npnpm dlx @c15t/cli generate\n# Alternatives:\n# npx @c15t/cli generate\n# bunx --bun @c15t/cli generate\n```\n\nThe CLI will:\n\n- Install necessary packages\n- Configure your c15t instance\n- Set up environment variables\n- Add consent management components to your app", + "usage": [ + "Import `ConsentManagerProvider` in your app's root component", + "Add `CookieBanner` and `PreferenceCenter` components", + "Customise styling and behaviour to fit your app", + "For full implementation details, see the [React quickstart docs](https://c15t.com/docs/frameworks/react/quickstart)", - "```tsx\n// App.tsx\nimport { ConsentManagerProvider, CookieBanner } from '@c15t/react'\n\nfunction App() {\n return (\n \n \n \n \n )\n}\n```" - ], - "docsLink": "https://c15t.com/docs/frameworks/react/quickstart" + "```tsx\n// App.tsx\nimport { ConsentManagerProvider, CookieBanner } from '@c15t/react'\n\nfunction App() {\n return (\n \n \n \n \n )\n}\n```" + ], + "docsLink": "https://c15t.com/docs/frameworks/react/quickstart" } diff --git a/package.json b/package.json index 49d4029..e54b105 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,23 @@ "name": "readie", "version": "0.0.1", "description": "Generate high-quality README files from a validated JSON config.", - "type": "module", - "main": "dist/index.js", + "keywords": [ + "cli", + "documentation", + "generator", + "markdown", + "readme" + ], + "homepage": "https://github.com/consentdotio/readie", + "bugs": { + "url": "https://github.com/consentdotio/readie/issues" + }, + "license": "MIT", + "author": "Christopher Burns", + "repository": { + "type": "git", + "url": "git+https://github.com/consentdotio/readie.git" + }, "bin": { "readie": "dist/index.js" }, @@ -17,6 +32,8 @@ "CODE_OF_CONDUCT.md", "SECURITY.md" ], + "type": "module", + "main": "dist/index.js", "scripts": { "build": "bun build src/index.ts --outdir dist --target node --minify", "clean": "bun --eval \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true });\"", @@ -29,23 +46,6 @@ "check": "ultracite check", "fix": "ultracite fix" }, - "keywords": [ - "readme", - "documentation", - "cli", - "generator", - "markdown" - ], - "author": "Christopher Burns", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/consentdotio/readie.git" - }, - "homepage": "https://github.com/consentdotio/readie", - "bugs": { - "url": "https://github.com/consentdotio/readie/issues" - }, "dependencies": { "@effect/cli": "^0.73.2", "@effect/platform": "^0.94.5", @@ -66,4 +66,4 @@ "ultracite": "7.2.3", "vitest": "^3.2.4" } -} \ No newline at end of file +} diff --git a/schemas/readie.schema.json b/schemas/readie.schema.json index bc9ba15..66b75f5 100644 --- a/schemas/readie.schema.json +++ b/schemas/readie.schema.json @@ -149,7 +149,9 @@ "features": ["Fast", "Typed", "Zero-config"], "installation": ["```bash\\nnpm i my-library\\n```"], "usage": ["Import and use the library in your project."], - "commands": [{ "name": "my-cli init", "description": "Initialize project files." }] + "commands": [ + { "name": "my-cli init", "description": "Initialize project files." } + ] } ] } diff --git a/src/cli/commands/generate-workspace.ts b/src/cli/commands/generate-workspace.ts index b0125ef..d186547 100644 --- a/src/cli/commands/generate-workspace.ts +++ b/src/cli/commands/generate-workspace.ts @@ -1,7 +1,12 @@ -import path from 'node:path'; -import { Command, Options } from '@effect/cli'; -import { Effect } from 'effect'; -import { generateWorkspaceReadmes, parsePackageList } from '../../readme-generator/generator.js'; +import path from "node:path"; + +import { Command, Options } from "@effect/cli"; +import { Effect } from "effect"; + +import { + generateWorkspaceReadmes, + parsePackageList, +} from "../../readme-generator/generator.js"; interface GenerateWorkspaceCommandArgs { root: string; @@ -13,46 +18,61 @@ interface GenerateWorkspaceCommandArgs { } export const generateWorkspaceCommand = Command.make( - 'generate:workspace', + "generate:workspace", { - root: Options.directory('root').pipe( - Options.withAlias('r'), - Options.withDescription('Workspace root directory'), - Options.withDefault(path.resolve('./packages')), + configName: Options.text("config-name").pipe( + Options.withDescription("Config filename to search for in each project"), + Options.withDefault("readie.json") + ), + dryRun: Options.boolean("dry-run").pipe( + Options.withDescription("Show changes without writing files") ), - configName: Options.text('config-name').pipe( - Options.withDescription('Config filename to search for in each project'), - Options.withDefault('readie.json'), + noGlobal: Options.boolean("no-global").pipe( + Options.withDescription("Disable readie.global.json discovery and merge") ), - packageValues: Options.text('package').pipe( - Options.withAlias('p'), - Options.withDescription('Project name filter (repeatable, comma-separated supported)'), - Options.repeated, + packageValues: Options.text("package").pipe( + Options.withAlias("p"), + Options.withDescription( + "Project name filter (repeatable, comma-separated supported)" + ), + Options.repeated ), - dryRun: Options.boolean('dry-run').pipe(Options.withDescription('Show changes without writing files')), - strict: Options.boolean('strict').pipe(Options.withDescription('Exit with code 1 if any project fails')), - noGlobal: Options.boolean('no-global').pipe( - Options.withDescription('Disable readie.global.json discovery and merge'), + root: Options.directory("root").pipe( + Options.withAlias("r"), + Options.withDescription("Workspace root directory"), + Options.withDefault(path.resolve("./packages")) + ), + strict: Options.boolean("strict").pipe( + Options.withDescription("Exit with code 1 if any project fails") ), }, - ({ root, configName, packageValues, dryRun, strict, noGlobal }: GenerateWorkspaceCommandArgs) => - Effect.gen(function* () { + ({ + root, + configName, + packageValues, + dryRun, + strict, + noGlobal, + }: GenerateWorkspaceCommandArgs) => + Effect.gen(function* generateWorkspaceCommand() { const result = yield* Effect.tryPromise({ + catch: (error: unknown) => + error instanceof Error + ? error + : new Error(`Workspace generation failed: ${String(error)}`), try: () => generateWorkspaceReadmes({ - rootDir: root, configName, - packageFilter: parsePackageList(packageValues), dryRun, + packageFilter: parsePackageList(packageValues), + rootDir: root, useGlobalConfig: !noGlobal, }), - catch: (error: unknown) => - error instanceof Error ? error : new Error(`Workspace generation failed: ${String(error)}`), }); yield* Effect.sync(() => { - console.log(''); - console.log('Summary'); + console.log(""); + console.log("Summary"); console.log(`- Updated: ${result.updated.length}`); console.log(`- Unchanged: ${result.unchanged.length}`); console.log(`- Failed: ${result.failed.length}`); @@ -63,7 +83,9 @@ export const generateWorkspaceCommand = Command.make( process.exitCode = 1; } }); - }), + }) ).pipe( - Command.withDescription('Generate READMEs for projects inside a workspace root.'), + Command.withDescription( + "Generate READMEs for projects inside a workspace root." + ) ); diff --git a/src/cli/commands/generate.ts b/src/cli/commands/generate.ts index ec79ecd..713117d 100644 --- a/src/cli/commands/generate.ts +++ b/src/cli/commands/generate.ts @@ -1,6 +1,7 @@ -import { Command, Options } from '@effect/cli'; -import { Effect } from 'effect'; -import { generateReadmeFromConfig } from '../../readme-generator/generator.js'; +import { Command, Options } from "@effect/cli"; +import { Effect } from "effect"; + +import { generateReadmeFromConfig } from "../../readme-generator/generator.js"; interface GenerateCommandArgs { config: string; @@ -10,42 +11,52 @@ interface GenerateCommandArgs { } export const generateCommand = Command.make( - 'generate', + "generate", { - config: Options.text('config').pipe( - Options.withAlias('c'), - Options.withDescription('Path to readie config file'), - Options.withDefault('./readie.json'), + config: Options.text("config").pipe( + Options.withAlias("c"), + Options.withDescription("Path to readie config file"), + Options.withDefault("./readie.json") + ), + dryRun: Options.boolean("dry-run").pipe( + Options.withDescription("Show changes without writing files") ), - output: Options.text('output').pipe( - Options.withAlias('o'), - Options.withDescription('Optional output path for README'), - Options.withDefault(''), + noGlobal: Options.boolean("no-global").pipe( + Options.withDescription("Disable readie.global.json discovery and merge") ), - dryRun: Options.boolean('dry-run').pipe(Options.withDescription('Show changes without writing files')), - noGlobal: Options.boolean('no-global').pipe( - Options.withDescription('Disable readie.global.json discovery and merge'), + output: Options.text("output").pipe( + Options.withAlias("o"), + Options.withDescription("Optional output path for README"), + Options.withDefault("") ), }, ({ config, output, dryRun, noGlobal }: GenerateCommandArgs) => - Effect.gen(function* () { + Effect.gen(function* generateCommand() { const result = yield* Effect.tryPromise({ + catch: (error: unknown) => + error instanceof Error + ? error + : new Error(`Generation failed: ${String(error)}`), try: () => generateReadmeFromConfig({ configPath: config, - outputPath: output.trim().length > 0 ? output : undefined, dryRun, + outputPath: output.trim().length > 0 ? output : undefined, useGlobalConfig: !noGlobal, }), - catch: (error: unknown) => - error instanceof Error ? error : new Error(`Generation failed: ${String(error)}`), }); yield* Effect.sync(() => { - const status = result.updated ? (dryRun ? 'Would update' : 'Generated') : 'No changes'; + const status = result.updated + ? (dryRun + ? "Would update" + : "Generated") + : "No changes"; console.log(`${status}: ${result.outputPath}`); }); - }), + }) ).pipe( - Command.withDescription('Generate a README from a single readie.json config file.'), + Command.withDescription( + "Generate a README from a single readie.json config file." + ) ); diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 3eef85d..8faabc1 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -1,9 +1,11 @@ -import * as fssync from 'node:fs'; -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { Command, Options } from '@effect/cli'; -import { Effect } from 'effect'; -import { starterConfigText } from '../../config/starter-config.js'; +import * as fssync from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; + +import { Command, Options } from "@effect/cli"; +import { Effect } from "effect"; + +import { starterConfigText } from "../../config/starter-config.js"; interface InitCommandArgs { config: string; @@ -11,37 +13,45 @@ interface InitCommandArgs { } export const initCommand = Command.make( - 'init', + "init", { - config: Options.text('config').pipe( - Options.withAlias('c'), - Options.withDescription('Path for generated starter config'), - Options.withDefault('./readie.json'), + config: Options.text("config").pipe( + Options.withAlias("c"), + Options.withDescription("Path for generated starter config"), + Options.withDefault("./readie.json") ), - force: Options.boolean('force').pipe( - Options.withAlias('f'), - Options.withDescription('Overwrite existing config file if it exists'), + force: Options.boolean("force").pipe( + Options.withAlias("f"), + Options.withDescription("Overwrite existing config file if it exists") ), }, ({ config, force }: InitCommandArgs) => - Effect.gen(function* () { + Effect.gen(function* initCommand() { const configPath = path.resolve(config); const exists = fssync.existsSync(configPath); if (exists && !force) { - yield* Effect.fail(new Error(`Config already exists at ${configPath}. Use --force to overwrite.`)); + yield* Effect.fail( + new Error( + `Config already exists at ${configPath}. Use --force to overwrite.` + ) + ); } yield* Effect.tryPromise({ - try: () => fs.writeFile(configPath, starterConfigText, 'utf8'), catch: (error: unknown) => - error instanceof Error ? error : new Error(`Failed to write config: ${String(error)}`), + error instanceof Error + ? error + : new Error(`Failed to write config: ${String(error)}`), + try: () => fs.writeFile(configPath, starterConfigText, "utf8"), }); yield* Effect.sync(() => { console.log(`Created starter config: ${configPath}`); }); - }), + }) ).pipe( - Command.withDescription('Create a starter readie.json file in the current directory.'), + Command.withDescription( + "Create a starter readie.json file in the current directory." + ) ); diff --git a/src/cli/resolve-invocation.ts b/src/cli/resolve-invocation.ts index 9b7aa92..c0a4e55 100644 --- a/src/cli/resolve-invocation.ts +++ b/src/cli/resolve-invocation.ts @@ -1,4 +1,9 @@ -export type InvocationMode = 'generate' | 'generate:workspace' | 'init' | 'help' | 'unknown'; +export type InvocationMode = + | "generate" + | "generate:workspace" + | "init" + | "help" + | "unknown"; export interface ResolvedInvocation { mode: InvocationMode; @@ -6,62 +11,63 @@ export interface ResolvedInvocation { originalArgs: string[]; } -const isHelpFlag = (value: string | undefined) => value === '--help' || value === '-h'; +const isHelpFlag = (value: string | undefined) => + value === "--help" || value === "-h"; export const resolveInvocation = (args: string[]): ResolvedInvocation => { const [first, ...rest] = args; if (!first) { return { - mode: 'generate', commandArgs: [], + mode: "generate", originalArgs: args, }; } if (isHelpFlag(first)) { return { - mode: 'help', commandArgs: rest, + mode: "help", originalArgs: args, }; } - if (first === 'generate') { + if (first === "generate") { return { - mode: 'generate', commandArgs: rest, + mode: "generate", originalArgs: args, }; } - if (first === 'generate:workspace') { + if (first === "generate:workspace") { return { - mode: 'generate:workspace', commandArgs: rest, + mode: "generate:workspace", originalArgs: args, }; } - if (first === 'init') { + if (first === "init") { return { - mode: 'init', commandArgs: rest, + mode: "init", originalArgs: args, }; } - if (first === 'help') { + if (first === "help") { return { - mode: 'help', commandArgs: rest, + mode: "help", originalArgs: args, }; } return { - mode: 'unknown', commandArgs: args, + mode: "unknown", originalArgs: args, }; }; diff --git a/src/config/load-config.ts b/src/config/load-config.ts index 73e0445..99043da 100644 --- a/src/config/load-config.ts +++ b/src/config/load-config.ts @@ -1,21 +1,23 @@ -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { Schema } from 'effect'; -import type { ReadieConfig, ReadieGlobalConfig } from './types.js'; +import fs from "node:fs/promises"; +import path from "node:path"; + +import { Schema } from "effect"; + +import type { ReadieConfig, ReadieGlobalConfig } from "./types.js"; const commandSchema = Schema.Struct({ - name: Schema.NonEmptyString, description: Schema.NonEmptyString, + name: Schema.NonEmptyString, }); const globalFlagSchema = Schema.Struct({ - flag: Schema.NonEmptyString, description: Schema.NonEmptyString, + flag: Schema.NonEmptyString, }); const badgeSchema = Schema.Struct({ - label: Schema.NonEmptyString, image: Schema.NonEmptyString, + label: Schema.NonEmptyString, link: Schema.optional(Schema.NonEmptyString), }); @@ -24,89 +26,95 @@ const licenseSchema = Schema.Union( Schema.Struct({ name: Schema.NonEmptyString, url: Schema.NonEmptyString, - }), + }) ); const readieConfigSchema = Schema.Struct({ $schema: Schema.optional(Schema.String), - version: Schema.optional(Schema.Literal('1')), - title: Schema.NonEmptyString, - description: Schema.NonEmptyString, - output: Schema.optional(Schema.String), - includeTableOfContents: Schema.optional(Schema.Boolean), - features: Schema.optional(Schema.Array(Schema.String)), - prerequisites: Schema.optional(Schema.Array(Schema.String)), - installation: Schema.optional(Schema.Array(Schema.String)), - manualInstallation: Schema.optional(Schema.Array(Schema.String)), - usage: Schema.optional(Schema.Array(Schema.String)), - commands: Schema.optional(Schema.Array(commandSchema)), - globalFlags: Schema.optional(Schema.Array(globalFlagSchema)), badges: Schema.optional(Schema.Array(badgeSchema)), banner: Schema.optional(Schema.String), - quickStart: Schema.optional(Schema.String), - support: Schema.optional(Schema.Array(Schema.String)), + commands: Schema.optional(Schema.Array(commandSchema)), contributing: Schema.optional(Schema.Array(Schema.String)), - security: Schema.optional(Schema.String), - license: Schema.optional(licenseSchema), - footer: Schema.optional(Schema.String), - docsLink: Schema.optional(Schema.String), - quickStartLink: Schema.optional(Schema.String), customSections: Schema.optional( Schema.Record({ key: Schema.String, value: Schema.String, - }), + }) ), -}); - -const readieGlobalConfigSchema = Schema.Struct({ - $schema: Schema.optional(Schema.String), - version: Schema.optional(Schema.Literal('1')), - title: Schema.optional(Schema.NonEmptyString), - description: Schema.optional(Schema.NonEmptyString), - output: Schema.optional(Schema.String), - includeTableOfContents: Schema.optional(Schema.Boolean), + description: Schema.NonEmptyString, + docsLink: Schema.optional(Schema.String), features: Schema.optional(Schema.Array(Schema.String)), - prerequisites: Schema.optional(Schema.Array(Schema.String)), + footer: Schema.optional(Schema.String), + globalFlags: Schema.optional(Schema.Array(globalFlagSchema)), + includeTableOfContents: Schema.optional(Schema.Boolean), installation: Schema.optional(Schema.Array(Schema.String)), + license: Schema.optional(licenseSchema), manualInstallation: Schema.optional(Schema.Array(Schema.String)), + output: Schema.optional(Schema.String), + prerequisites: Schema.optional(Schema.Array(Schema.String)), + quickStart: Schema.optional(Schema.String), + quickStartLink: Schema.optional(Schema.String), + security: Schema.optional(Schema.String), + support: Schema.optional(Schema.Array(Schema.String)), + title: Schema.NonEmptyString, usage: Schema.optional(Schema.Array(Schema.String)), - commands: Schema.optional(Schema.Array(commandSchema)), - globalFlags: Schema.optional(Schema.Array(globalFlagSchema)), + version: Schema.optional(Schema.Literal("1")), +}); + +const readieGlobalConfigSchema = Schema.Struct({ + $schema: Schema.optional(Schema.String), badges: Schema.optional(Schema.Array(badgeSchema)), banner: Schema.optional(Schema.String), - quickStart: Schema.optional(Schema.String), - support: Schema.optional(Schema.Array(Schema.String)), + commands: Schema.optional(Schema.Array(commandSchema)), contributing: Schema.optional(Schema.Array(Schema.String)), - security: Schema.optional(Schema.String), - license: Schema.optional(licenseSchema), - footer: Schema.optional(Schema.String), - docsLink: Schema.optional(Schema.String), - quickStartLink: Schema.optional(Schema.String), customSections: Schema.optional( Schema.Record({ key: Schema.String, value: Schema.String, - }), + }) ), + description: Schema.optional(Schema.NonEmptyString), + docsLink: Schema.optional(Schema.String), + features: Schema.optional(Schema.Array(Schema.String)), + footer: Schema.optional(Schema.String), + globalFlags: Schema.optional(Schema.Array(globalFlagSchema)), + includeTableOfContents: Schema.optional(Schema.Boolean), + installation: Schema.optional(Schema.Array(Schema.String)), + license: Schema.optional(licenseSchema), + manualInstallation: Schema.optional(Schema.Array(Schema.String)), + output: Schema.optional(Schema.String), + prerequisites: Schema.optional(Schema.Array(Schema.String)), + quickStart: Schema.optional(Schema.String), + quickStartLink: Schema.optional(Schema.String), + security: Schema.optional(Schema.String), + support: Schema.optional(Schema.Array(Schema.String)), + title: Schema.optional(Schema.NonEmptyString), + usage: Schema.optional(Schema.Array(Schema.String)), + version: Schema.optional(Schema.Literal("1")), }); const decodeReadieConfig = Schema.decodeUnknownSync(readieConfigSchema); -const decodeReadieGlobalConfig = Schema.decodeUnknownSync(readieGlobalConfigSchema); +const decodeReadieGlobalConfig = Schema.decodeUnknownSync( + readieGlobalConfigSchema +); -const GLOBAL_CONFIG_NAME = 'readie.global.json'; +const GLOBAL_CONFIG_NAME = "readie.global.json"; const parseJsonFile = async (absolutePath: string): Promise => { - const raw = await fs.readFile(absolutePath, 'utf8'); + const raw = await fs.readFile(absolutePath, "utf8"); try { return JSON.parse(raw); } catch (error) { const message = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to parse JSON in ${absolutePath}: ${message}`); + throw new Error(`Failed to parse JSON in ${absolutePath}: ${message}`, { + cause: error, + }); } }; -export const loadReadieConfig = async (configPath: string): Promise => { +export const loadReadieConfig = async ( + configPath: string +): Promise => { const absolutePath = path.resolve(configPath); const parsed = await parseJsonFile(absolutePath); @@ -114,11 +122,16 @@ export const loadReadieConfig = async (configPath: string): Promise => { +const loadGlobalReadieConfig = async ( + configPath: string +): Promise => { const absolutePath = path.resolve(configPath); const parsed = await parseJsonFile(absolutePath); @@ -126,11 +139,16 @@ const loadGlobalReadieConfig = async (configPath: string): Promise => { +export const loadGlobalConfig = async ( + startDir: string +): Promise => { let current = path.resolve(startDir); while (true) { @@ -150,34 +168,41 @@ export const loadGlobalConfig = async (startDir: string): Promise Object.prototype.hasOwnProperty.call(obj, key); +const hasOwn = (obj: object, key: string): boolean => Object.hasOwn(obj, key); interface InterpolationContext { packageName?: string; } -const interpolatePlaceholders = (value: string, placeholders: Record): string => - value.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (match, key: string) => placeholders[key] ?? match); +const interpolatePlaceholders = ( + value: string, + placeholders: Record +): string => + value.replaceAll( + /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, + (match, key: string) => placeholders[key] ?? match + ); const interpolateTopLevelStrings = ( config: ReadieConfig, - interpolationContext: InterpolationContext, + interpolationContext: InterpolationContext ): ReadieConfig => { - const resolvedPackageName = interpolationContext.packageName?.trim() || config.title; + const resolvedPackageName = + interpolationContext.packageName?.trim() || config.title; const placeholders: Record = { - title: config.title, packageName: resolvedPackageName, packageNameEncoded: encodeURIComponent(resolvedPackageName), + title: config.title, }; const interpolated = { ...config } as Record; for (const [key, value] of Object.entries(interpolated)) { - if (typeof value === 'string') { + if (typeof value === "string") { interpolated[key] = interpolatePlaceholders(value, placeholders); } } - // Interpolate customSections values + // Interpolate customSections values if (config.customSections) { const interpolatedSections: Record = {}; for (const [key, value] of Object.entries(config.customSections)) { @@ -186,14 +211,13 @@ const interpolateTopLevelStrings = ( interpolated.customSections = interpolatedSections; } - return interpolated as unknown as ReadieConfig; }; const resolveMergedValue = ( key: keyof ReadieConfig, projectConfig: ReadieConfig, - globalConfig: ReadieGlobalConfig | null, + globalConfig: ReadieGlobalConfig | null ): T | undefined => { const project = projectConfig as unknown as Record; const global = (globalConfig ?? {}) as Record; @@ -210,58 +234,89 @@ const resolveMergedValue = ( export const mergeConfigs = ( globalConfig: ReadieGlobalConfig | null, projectConfig: ReadieConfig, - interpolationContext: InterpolationContext = {}, + interpolationContext: InterpolationContext = {} ): ReadieConfig => { const mergedCustomSections = (() => { const project = projectConfig as unknown as Record; const global = (globalConfig ?? {}) as Record; - if (hasOwn(project, 'customSections')) { + if (hasOwn(project, "customSections")) { const projectCustomSections = project.customSections; if (projectCustomSections === null) { - return undefined; + return; } - if (typeof projectCustomSections === 'object' && projectCustomSections !== null) { + if ( + typeof projectCustomSections === "object" && + projectCustomSections !== null + ) { return { - ...((global.customSections as Record | undefined) ?? {}), + ...((global.customSections as Record | undefined) ?? + {}), ...(projectCustomSections as Record), }; } - return undefined; + return; } - if (typeof global.customSections === 'object' && global.customSections !== null) { + if ( + typeof global.customSections === "object" && + global.customSections !== null + ) { return global.customSections as Record; } - return undefined; + return; })(); const merged: ReadieConfig = { - title: projectConfig.title, - description: projectConfig.description, - $schema: resolveMergedValue('$schema', projectConfig, globalConfig), - version: resolveMergedValue('version', projectConfig, globalConfig), - output: resolveMergedValue('output', projectConfig, globalConfig), - includeTableOfContents: resolveMergedValue('includeTableOfContents', projectConfig, globalConfig), - features: resolveMergedValue('features', projectConfig, globalConfig), - prerequisites: resolveMergedValue('prerequisites', projectConfig, globalConfig), - installation: resolveMergedValue('installation', projectConfig, globalConfig), - manualInstallation: resolveMergedValue('manualInstallation', projectConfig, globalConfig), - usage: resolveMergedValue('usage', projectConfig, globalConfig), - commands: resolveMergedValue('commands', projectConfig, globalConfig), - globalFlags: resolveMergedValue('globalFlags', projectConfig, globalConfig), - badges: resolveMergedValue('badges', projectConfig, globalConfig), - banner: resolveMergedValue('banner', projectConfig, globalConfig), - quickStart: resolveMergedValue('quickStart', projectConfig, globalConfig), - support: resolveMergedValue('support', projectConfig, globalConfig), - contributing: resolveMergedValue('contributing', projectConfig, globalConfig), - security: resolveMergedValue('security', projectConfig, globalConfig), - license: resolveMergedValue('license', projectConfig, globalConfig), - footer: resolveMergedValue('footer', projectConfig, globalConfig), - docsLink: resolveMergedValue('docsLink', projectConfig, globalConfig), - quickStartLink: resolveMergedValue('quickStartLink', projectConfig, globalConfig), + $schema: resolveMergedValue("$schema", projectConfig, globalConfig), + badges: resolveMergedValue("badges", projectConfig, globalConfig), + banner: resolveMergedValue("banner", projectConfig, globalConfig), + commands: resolveMergedValue("commands", projectConfig, globalConfig), + contributing: resolveMergedValue( + "contributing", + projectConfig, + globalConfig + ), customSections: mergedCustomSections, + description: projectConfig.description, + docsLink: resolveMergedValue("docsLink", projectConfig, globalConfig), + features: resolveMergedValue("features", projectConfig, globalConfig), + footer: resolveMergedValue("footer", projectConfig, globalConfig), + globalFlags: resolveMergedValue("globalFlags", projectConfig, globalConfig), + includeTableOfContents: resolveMergedValue( + "includeTableOfContents", + projectConfig, + globalConfig + ), + installation: resolveMergedValue( + "installation", + projectConfig, + globalConfig + ), + license: resolveMergedValue("license", projectConfig, globalConfig), + manualInstallation: resolveMergedValue( + "manualInstallation", + projectConfig, + globalConfig + ), + output: resolveMergedValue("output", projectConfig, globalConfig), + prerequisites: resolveMergedValue( + "prerequisites", + projectConfig, + globalConfig + ), + quickStart: resolveMergedValue("quickStart", projectConfig, globalConfig), + quickStartLink: resolveMergedValue( + "quickStartLink", + projectConfig, + globalConfig + ), + security: resolveMergedValue("security", projectConfig, globalConfig), + support: resolveMergedValue("support", projectConfig, globalConfig), + title: projectConfig.title, + usage: resolveMergedValue("usage", projectConfig, globalConfig), + version: resolveMergedValue("version", projectConfig, globalConfig), }; return interpolateTopLevelStrings(merged, interpolationContext); diff --git a/src/config/starter-config.ts b/src/config/starter-config.ts index adde44b..065c7ea 100644 --- a/src/config/starter-config.ts +++ b/src/config/starter-config.ts @@ -1,17 +1,18 @@ -import type { ReadieConfig } from './types.js'; +import type { ReadieConfig } from "./types.js"; -export const DEFAULT_SCHEMA_URL = 'https://unpkg.com/readie/schemas/readie.schema.json'; +export const DEFAULT_SCHEMA_URL = + "https://unpkg.com/readie/schemas/readie.schema.json"; export const starterConfig: ReadieConfig = { $schema: DEFAULT_SCHEMA_URL, - version: '1', - title: 'My Project', - description: 'A short description of what this project does.', + description: "A short description of what this project does.", + docsLink: "https://example.com/docs", + features: ["Fast setup", "Clear docs", "Simple CLI usage"], includeTableOfContents: true, - features: ['Fast setup', 'Clear docs', 'Simple CLI usage'], - installation: ['```bash\nnpm install my-project\n```'], - usage: ['Explain basic usage in a few steps.', '```bash\nnpm run start\n```'], - docsLink: 'https://example.com/docs', + installation: ["```bash\nnpm install my-project\n```"], + title: "My Project", + usage: ["Explain basic usage in a few steps.", "```bash\nnpm run start\n```"], + version: "1", }; export const starterConfigText = `${JSON.stringify(starterConfig, null, 2)}\n`; diff --git a/src/config/types.ts b/src/config/types.ts index 6e6493a..4eaec67 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -23,7 +23,7 @@ export type ReadieLicense = string | ReadieLicenseObject; export interface ReadieConfig { $schema?: string; - version?: '1'; + version?: "1"; title: string; description: string; output?: string; @@ -73,6 +73,6 @@ export interface GenerateWorkspaceOptions { export interface GenerateWorkspaceResult { updated: string[]; unchanged: string[]; - failed: Array<{ projectDir: string; error: unknown }>; + failed: { projectDir: string; error: unknown }[]; skippedByFilter: string[]; } diff --git a/src/index.ts b/src/index.ts index 50b0b68..178ce85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,69 +1,71 @@ #!/usr/bin/env node -import { Command, ValidationError } from '@effect/cli'; -import * as NodeContext from '@effect/platform-node/NodeContext'; -import * as NodeRuntime from '@effect/platform-node/NodeRuntime'; -import { Effect } from 'effect'; -import { generateCommand } from './cli/commands/generate.js'; -import { generateWorkspaceCommand } from './cli/commands/generate-workspace.js'; -import { initCommand } from './cli/commands/init.js'; -import { printRootHelp } from './cli/help.js'; -import { resolveInvocation } from './cli/resolve-invocation.js'; +import { Command, ValidationError } from "@effect/cli"; +import * as NodeContext from "@effect/platform-node/NodeContext"; +import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; +import { Effect } from "effect"; -const version = '0.1.0'; +import { generateWorkspaceCommand } from "./cli/commands/generate-workspace.js"; +import { generateCommand } from "./cli/commands/generate.js"; +import { initCommand } from "./cli/commands/init.js"; +import { printRootHelp } from "./cli/help.js"; +import { resolveInvocation } from "./cli/resolve-invocation.js"; + +const version = "0.1.0"; const runGenerate = (args: string[]) => Command.run(generateCommand, { - name: 'readie', + name: "readie", version, })(args).pipe(Effect.provide(NodeContext.layer)); const runGenerateWorkspace = (args: string[]) => Command.run(generateWorkspaceCommand, { - name: 'readie', + name: "readie", version, })(args).pipe(Effect.provide(NodeContext.layer)); const runInit = (args: string[]) => Command.run(initCommand, { - name: 'readie', + name: "readie", version, })(args).pipe(Effect.provide(NodeContext.layer)); const resolved = resolveInvocation(process.argv.slice(2)); -if (resolved.mode === 'help') { +if (resolved.mode === "help") { printRootHelp(); process.exit(0); } -if (resolved.mode === 'unknown') { +if (resolved.mode === "unknown") { printRootHelp(); process.exit(1); } const commandEffect = - resolved.mode === 'generate' + resolved.mode === "generate" ? runGenerate(resolved.commandArgs) - : resolved.mode === 'generate:workspace' + : (resolved.mode === "generate:workspace" ? runGenerateWorkspace(resolved.commandArgs) - : runInit(resolved.commandArgs); + : runInit(resolved.commandArgs)); const program = commandEffect.pipe( Effect.catchIf( - (error): error is ValidationError.ValidationError => ValidationError.isValidationError(error), + (error): error is ValidationError.ValidationError => + ValidationError.isValidationError(error), (error) => Effect.sync(() => { console.error(String(error)); process.exitCode = 1; - }), + }) ), Effect.catchAll((error) => Effect.sync(() => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; - }), - ), + }) + ) ); NodeRuntime.runMain(program); diff --git a/src/readme-generator/generator.ts b/src/readme-generator/generator.ts index 2417f11..46d6e8d 100644 --- a/src/readme-generator/generator.ts +++ b/src/readme-generator/generator.ts @@ -1,19 +1,24 @@ -import * as fssync from 'node:fs'; -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { loadGlobalConfig, loadReadieConfig, mergeConfigs } from '../config/load-config.js'; +import * as fssync from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; + +import { + loadGlobalConfig, + loadReadieConfig, + mergeConfigs, +} from "../config/load-config.js"; import type { GenerateSingleOptions, GenerateSingleResult, GenerateWorkspaceOptions, GenerateWorkspaceResult, -} from '../config/types.js'; -import { baseReadmeTemplate } from './template.js'; +} from "../config/types.js"; +import { baseReadmeTemplate } from "./template.js"; export const parsePackageList = (values: string[]): Set => { const packages = new Set(); for (const value of values) { - for (const part of value.split(',')) { + for (const part of value.split(",")) { const name = part.trim(); if (name.length > 0) { packages.add(name); @@ -26,7 +31,7 @@ export const parsePackageList = (values: string[]): Set => { const resolveOutputPath = ( configPath: string, configOutputPath: string | undefined, - cliOutputPath: string | undefined, + cliOutputPath: string | undefined ) => { if (cliOutputPath) { return path.resolve(cliOutputPath); @@ -34,16 +39,20 @@ const resolveOutputPath = ( if (configOutputPath) { return path.resolve(path.dirname(configPath), configOutputPath); } - return path.resolve(path.dirname(configPath), 'README.md'); + return path.resolve(path.dirname(configPath), "README.md"); }; -const resolvePackageName = async (configPath: string): Promise => { - const packageJsonPath = path.join(path.dirname(configPath), 'package.json'); +const resolvePackageName = async ( + configPath: string +): Promise => { + const packageJsonPath = path.join(path.dirname(configPath), "package.json"); try { - const rawPackageJson = await fs.readFile(packageJsonPath, 'utf8'); + const rawPackageJson = await fs.readFile(packageJsonPath, "utf8"); const parsed = JSON.parse(rawPackageJson) as { name?: unknown }; - return typeof parsed.name === 'string' && parsed.name.trim().length > 0 ? parsed.name : undefined; + return typeof parsed.name === "string" && parsed.name.trim().length > 0 + ? parsed.name + : undefined; } catch (error) { console.warn(`Package.json not found at ${packageJsonPath}:`, error); return undefined; @@ -58,14 +67,20 @@ export const generateReadmeFromConfig = async ({ }: GenerateSingleOptions): Promise => { const absoluteConfigPath = path.resolve(configPath); const projectConfig = await loadReadieConfig(absoluteConfigPath); - const globalConfig = useGlobalConfig ? await loadGlobalConfig(path.dirname(absoluteConfigPath)) : null; + const globalConfig = useGlobalConfig + ? await loadGlobalConfig(path.dirname(absoluteConfigPath)) + : null; const packageName = await resolvePackageName(absoluteConfigPath); const config = mergeConfigs(globalConfig, projectConfig, { packageName }); - const resolvedOutputPath = resolveOutputPath(absoluteConfigPath, config.output, outputPath); + const resolvedOutputPath = resolveOutputPath( + absoluteConfigPath, + config.output, + outputPath + ); const content = baseReadmeTemplate(config); const existingContent = fssync.existsSync(resolvedOutputPath) - ? await fs.readFile(resolvedOutputPath, 'utf8') + ? await fs.readFile(resolvedOutputPath, "utf8") : null; if (existingContent === content) { @@ -76,7 +91,7 @@ export const generateReadmeFromConfig = async ({ } if (!dryRun) { - await fs.writeFile(resolvedOutputPath, content, 'utf8'); + await fs.writeFile(resolvedOutputPath, content, "utf8"); } return { @@ -116,10 +131,10 @@ export async function generateWorkspaceReadmes({ : []; const result: GenerateWorkspaceResult = { - updated: [], - unchanged: [], failed: [], skippedByFilter, + unchanged: [], + updated: [], }; for (const projectDir of selectedProjectDirs) { @@ -133,13 +148,15 @@ export async function generateWorkspaceReadmes({ }); if (singleResult.updated) { result.updated.push(projectName); - console.log(`${dryRun ? 'Would update' : 'Generated'} README for ${projectName}`); + console.log( + `${dryRun ? "Would update" : "Generated"} README for ${projectName}` + ); } else { result.unchanged.push(projectName); console.log(`No changes for ${projectName}`); } } catch (error) { - result.failed.push({ projectDir: projectName, error }); + result.failed.push({ error, projectDir: projectName }); console.error(`Error generating README for ${projectName}:`, error); } } diff --git a/src/readme-generator/template.ts b/src/readme-generator/template.ts index 0924ae2..822651c 100644 --- a/src/readme-generator/template.ts +++ b/src/readme-generator/template.ts @@ -1,7 +1,11 @@ -import type { ReadieBadge, ReadieConfig, ReadieLicenseObject } from '../config/types.js'; +import type { + ReadieBadge, + ReadieConfig, + ReadieLicenseObject, +} from "../config/types.js"; const isNonEmpty = (value?: string | null): value is string => - typeof value === 'string' && value.trim().length > 0; + typeof value === "string" && value.trim().length > 0; const renderNumberedWithCodeBlocks = (items: string[]) => { const lines: string[] = []; @@ -9,18 +13,20 @@ const renderNumberedWithCodeBlocks = (items: string[]) => { for (const rawItem of items) { const item = rawItem.trim(); - if (!item) continue; + if (!item) { + continue; + } - if (item.startsWith('```')) { - if (lines.length > 0 && lines[lines.length - 1] !== '') { - lines.push(''); + if (item.startsWith("```")) { + if (lines.length > 0 && lines.at(-1) !== "") { + lines.push(""); } lines.push(item); - lines.push(''); + lines.push(""); continue; } - if (item.startsWith('- ')) { + if (item.startsWith("- ")) { lines.push(`${i}. ${item.slice(2)}`); i += 1; continue; @@ -30,104 +36,117 @@ const renderNumberedWithCodeBlocks = (items: string[]) => { i += 1; } - return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim(); + return lines + .join("\n") + .replaceAll(/\n{3,}/g, "\n\n") + .trim(); }; const addSection = ( header: string, content: string[] | undefined, - formatter: (item: string, index?: number) => string = (item) => `- ${item}`, + formatter: (item: string, index?: number) => string = (item) => `- ${item}` ) => { - if (!content || content.length === 0) return ''; - const body = content.map(formatter).join('\n'); + if (!content || content.length === 0) { + return ""; + } + const body = content.map(formatter).join("\n"); return `${header}\n\n${body}`.trim(); }; export const baseReadmeTemplate = (rawConfig: ReadieConfig) => { const config: ReadieConfig = { ...rawConfig }; - const bannerBlock = isNonEmpty(config.banner) ? config.banner : ''; + const bannerBlock = isNonEmpty(config.banner) ? config.banner : ""; const titleBlock = - isNonEmpty(bannerBlock) && bannerBlock.toLowerCase().includes(' 0 - ? config.badges.map((badge: ReadieBadge) => { - const image = `![${badge.label}](${badge.image})`; - return isNonEmpty(badge.link) ? `[${image}](${badge.link})` : image; - }).join('\n') - : ''; + ? config.badges + .map((badge: ReadieBadge) => { + const image = `![${badge.label}](${badge.image})`; + return isNonEmpty(badge.link) ? `[${image}](${badge.link})` : image; + }) + .join("\n") + : ""; const featuresBlock = config.features && config.features.length > 0 - ? `## Key Features\n\n${config.features.map((feature) => `- ${feature}`).join('\n')}` - : ''; + ? `## Key Features\n\n${config.features.map((feature) => `- ${feature}`).join("\n")}` + : ""; - const prerequisitesBlock = addSection('## Prerequisites', config.prerequisites); + const prerequisitesBlock = addSection( + "## Prerequisites", + config.prerequisites + ); const quickStartBlock = isNonEmpty(config.quickStart) - ? config.quickStart.trimStart().startsWith('## ') + ? (config.quickStart.trimStart().startsWith("## ") ? config.quickStart - : `## Quick Start\n\n${config.quickStart}` - : ''; + : `## Quick Start\n\n${config.quickStart}`) + : ""; const manualInstallationBlock = config.manualInstallation && config.manualInstallation.length > 0 - ? `## Manual Installation\n\n${config.manualInstallation.join('\n')}` - : ''; + ? `## Manual Installation\n\n${config.manualInstallation.join("\n")}` + : ""; const installationBlock = config.installation && config.installation.length > 0 - ? `## Installation\n\n${config.installation.join('\n')}` - : ''; + ? `## Installation\n\n${config.installation.join("\n")}` + : ""; const usageBlock = config.usage && config.usage.length > 0 ? `## Usage\n\n${renderNumberedWithCodeBlocks(config.usage)}` - : ''; + : ""; const commandsBlock = config.commands && config.commands.length > 0 - ? `## Available Commands\n\n${config.commands.map((cmd) => `- \`${cmd.name}\`: ${cmd.description}`).join('\n')}` - : ''; + ? `## Available Commands\n\n${config.commands.map((cmd) => `- \`${cmd.name}\`: ${cmd.description}`).join("\n")}` + : ""; const globalFlagsBlock = config.globalFlags && config.globalFlags.length > 0 - ? `## Global Flags\n\n${config.globalFlags.map((flag) => `- \`${flag.flag}\`: ${flag.description}`).join('\n')}` - : ''; + ? `## Global Flags\n\n${config.globalFlags.map((flag) => `- \`${flag.flag}\`: ${flag.description}`).join("\n")}` + : ""; const docsBlock = config.docsLink ? `## Documentation For further information, guides, and examples visit the [reference documentation](${config.docsLink}).` - : ''; + : ""; const quickStartLinkBlock = config.quickStartLink ? `## Additional Quick Start See the full quick start guide [here](${config.quickStartLink}).` - : ''; + : ""; const customSectionsBlock = config.customSections ? Object.entries(config.customSections) .map(([heading, content]) => `## ${heading}\n\n${content}`) - .join('\n\n') - : ''; + .join("\n\n") + : ""; - const supportBlock = addSection('## Support', config.support); - const contributingBlock = addSection('## Contributing', config.contributing); + const supportBlock = addSection("## Support", config.support); + const contributingBlock = addSection("## Contributing", config.contributing); const securityBlock = isNonEmpty(config.security) - ? isNonEmpty(config.security) && config.security.trimStart().startsWith('## ') + ? (isNonEmpty(config.security) && + config.security.trimStart().startsWith("## ") ? config.security - : `## Security\n\n${config.security}` - : ''; + : `## Security\n\n${config.security}`) + : ""; const licenseBlock = (() => { - if (!config.license) return ''; - if (typeof config.license === 'string') { - return config.license.trimStart().startsWith('## ') + if (!config.license) { + return ""; + } + if (typeof config.license === "string") { + return config.license.trimStart().startsWith("## ") ? config.license : `## License\n\n${config.license}`; } @@ -135,23 +154,23 @@ See the full quick start guide [here](${config.quickStartLink}).` return `## License\n\n[${name}](${url})`; })(); - const footerBlock = isNonEmpty(config.footer) ? config.footer : ''; + const footerBlock = isNonEmpty(config.footer) ? config.footer : ""; const tocSectionTitles = [ - ['Key Features', featuresBlock], - ['Prerequisites', prerequisitesBlock], - ['Quick Start', quickStartBlock], - ['Installation', installationBlock], - ['Manual Installation', manualInstallationBlock], - ['Usage', usageBlock], - ['Available Commands', commandsBlock], - ['Global Flags', globalFlagsBlock], - ['Documentation', docsBlock], - ['Additional Quick Start', quickStartLinkBlock], - ['Support', supportBlock], - ['Contributing', contributingBlock], - ['Security', securityBlock], - ['License', licenseBlock], + ["Key Features", featuresBlock], + ["Prerequisites", prerequisitesBlock], + ["Quick Start", quickStartBlock], + ["Installation", installationBlock], + ["Manual Installation", manualInstallationBlock], + ["Usage", usageBlock], + ["Available Commands", commandsBlock], + ["Global Flags", globalFlagsBlock], + ["Documentation", docsBlock], + ["Additional Quick Start", quickStartLinkBlock], + ["Support", supportBlock], + ["Contributing", contributingBlock], + ["Security", securityBlock], + ["License", licenseBlock], ].filter(([, section]) => isNonEmpty(section)); if (isNonEmpty(customSectionsBlock)) { @@ -166,13 +185,13 @@ See the full quick start guide [here](${config.quickStartLink}).` .map(([title]) => { const slug = title .toLowerCase() - .replace(/[^a-z0-9 -]/g, '') + .replaceAll(/[^a-z0-9 -]/g, "") .trim() - .replace(/\s+/g, '-'); + .replaceAll(/\s+/g, "-"); return `- [${title}](#${slug})`; }) - .join('\n')}` - : ''; + .join("\n")}` + : ""; const readmeContent = [ bannerBlock, @@ -198,9 +217,9 @@ See the full quick start guide [here](${config.quickStartLink}).` footerBlock, ] .filter((section) => isNonEmpty(section)) - .join('\n\n') - .replace(/\n{3,}/g, '\n\n') - .replace(/\n{2,}$/, '\n'); + .join("\n\n") + .replaceAll(/\n{3,}/g, "\n\n") + .replace(/\n{2,}$/, "\n"); return `${readmeContent.trim()}\n`; }; diff --git a/test/generator-global-interpolation.test.ts b/test/generator-global-interpolation.test.ts index 66c738e..0e2f771 100644 --- a/test/generator-global-interpolation.test.ts +++ b/test/generator-global-interpolation.test.ts @@ -1,34 +1,34 @@ -import fs from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { generateReadmeFromConfig } from '../src/readme-generator/generator'; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { generateReadmeFromConfig } from "../src/readme-generator/generator"; const writeJson = async (filePath: string, value: unknown) => { - await fs.writeFile(filePath, JSON.stringify(value, null, 2), 'utf8'); + await fs.writeFile(filePath, JSON.stringify(value, null, 2), "utf8"); }; -describe('generateReadmeFromConfig with global interpolation', () => { - it('injects title and package placeholders in global config', async () => { - const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'readie-global-')); +describe("generateReadmeFromConfig with global interpolation", () => { + it("injects title and package placeholders in global config", async () => { + const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "readie-global-")); try { - const packageDir = path.join(rootDir, 'packages', 'react'); + const packageDir = path.join(rootDir, "packages", "react"); await fs.mkdir(packageDir, { recursive: true }); - await writeJson(path.join(rootDir, 'readie.global.json'), { + await writeJson(path.join(rootDir, "readie.global.json"), { banner: '

{{title}}

', - footer: 'Built for {{ title }} ({{packageNameEncoded}})', + footer: "Built for {{ title }} ({{packageNameEncoded}})", }); - await writeJson(path.join(packageDir, 'package.json'), { - name: '@c15t/react', - version: '1.0.0', + await writeJson(path.join(packageDir, "package.json"), { + name: "@c15t/react", + version: "1.0.0", }); - const configPath = path.join(packageDir, 'readie.json'); + const configPath = path.join(packageDir, "readie.json"); await writeJson(configPath, { - title: '@c15t/react: React Consent Components', - description: 'CMP for React', + description: "CMP for React", + title: "@c15t/react: React Consent Components", }); const result = await generateReadmeFromConfig({ @@ -36,12 +36,16 @@ describe('generateReadmeFromConfig with global interpolation', () => { dryRun: false, }); - const generated = await fs.readFile(result.outputPath, 'utf8'); + const generated = await fs.readFile(result.outputPath, "utf8"); - expect(generated).toContain('

@c15t/react: React Consent Components

'); - expect(generated).toContain('Built for @c15t/react: React Consent Components (%40c15t%2Freact)'); + expect(generated).toContain( + '

@c15t/react: React Consent Components

' + ); + expect(generated).toContain( + "Built for @c15t/react: React Consent Components (%40c15t%2Freact)" + ); } finally { - await fs.rm(rootDir, { recursive: true, force: true }); + await fs.rm(rootDir, { force: true, recursive: true }); } }); }); diff --git a/test/merge-config.test.ts b/test/merge-config.test.ts index 24d16a6..f1a5af9 100644 --- a/test/merge-config.test.ts +++ b/test/merge-config.test.ts @@ -1,55 +1,60 @@ -import { describe, expect, it } from 'vitest'; -import { mergeConfigs } from '../src/config/load-config'; -import type { ReadieConfig, ReadieGlobalConfig } from '../src/config/types'; - -const createProjectConfig = (overrides: Partial = {}): ReadieConfig => ({ - title: 'My Package', - description: 'Project level description.', +import { mergeConfigs } from "../src/config/load-config"; +import type { ReadieConfig, ReadieGlobalConfig } from "../src/config/types"; + +const createProjectConfig = ( + overrides: Partial = {} +): ReadieConfig => ({ + description: "Project level description.", + title: "My Package", ...overrides, }); -describe('mergeConfigs', () => { - it('interpolates top-level global string placeholders', () => { +describe(mergeConfigs, () => { + it("interpolates top-level global string placeholders", () => { const globalConfig: ReadieGlobalConfig = { banner: '

{{title}}

', - footer: 'Built by {{ title }} - {{packageName}} - {{packageNameEncoded}}', - features: ['Feature A'], customSections: { - Notes: 'Package: {{title}}', + Notes: "Package: {{title}}", }, + features: ["Feature A"], + footer: "Built by {{ title }} - {{packageName}} - {{packageNameEncoded}}", }; - const merged = mergeConfigs(globalConfig, createProjectConfig(), { packageName: '@c15t/react' }); + const merged = mergeConfigs(globalConfig, createProjectConfig(), { + packageName: "@c15t/react", + }); expect(merged.banner).toBe('

My Package

'); - expect(merged.footer).toBe('Built by My Package - @c15t/react - %40c15t%2Freact'); - expect(merged.features).toEqual(['Feature A']); - expect(merged.customSections?.Notes).toBe('Package: {{title}}'); + expect(merged.footer).toBe( + "Built by My Package - @c15t/react - %40c15t%2Freact" + ); + expect(merged.features).toStrictEqual(["Feature A"]); + expect(merged.customSections?.Notes).toBe("Package: {{title}}"); }); - it('preserves project-over-global precedence before interpolation', () => { + it("preserves project-over-global precedence before interpolation", () => { const globalConfig: ReadieGlobalConfig = { - banner: 'Global banner {{title}}', - quickStart: 'Global quick start', + banner: "Global banner {{title}}", + quickStart: "Global quick start", }; const projectConfig = createProjectConfig({ - banner: 'Project banner', - quickStart: 'Project quick start for {{title}}', + banner: "Project banner", + quickStart: "Project quick start for {{title}}", }); const merged = mergeConfigs(globalConfig, projectConfig); - expect(merged.banner).toBe('Project banner'); - expect(merged.quickStart).toBe('Project quick start for My Package'); + expect(merged.banner).toBe("Project banner"); + expect(merged.quickStart).toBe("Project quick start for My Package"); }); - it('falls back to title when packageName is unavailable', () => { + it("falls back to title when packageName is unavailable", () => { const globalConfig: ReadieGlobalConfig = { - footer: 'Encoded: {{packageNameEncoded}}', + footer: "Encoded: {{packageNameEncoded}}", }; const merged = mergeConfigs(globalConfig, createProjectConfig()); - expect(merged.footer).toBe('Encoded: My%20Package'); + expect(merged.footer).toBe("Encoded: My%20Package"); }); }); diff --git a/test/resolve-invocation.test.ts b/test/resolve-invocation.test.ts index 6c4e648..1d2516f 100644 --- a/test/resolve-invocation.test.ts +++ b/test/resolve-invocation.test.ts @@ -1,22 +1,25 @@ -import { describe, expect, it } from 'vitest'; -import { resolveInvocation } from '../src/cli/resolve-invocation'; +import { resolveInvocation } from "../src/cli/resolve-invocation"; -describe('resolveInvocation', () => { - it('defaults to generate when no args are passed', () => { +describe(resolveInvocation, () => { + it("defaults to generate when no args are passed", () => { const resolved = resolveInvocation([]); - expect(resolved.mode).toBe('generate'); - expect(resolved.commandArgs).toEqual([]); + expect(resolved.mode).toBe("generate"); + expect(resolved.commandArgs).toStrictEqual([]); }); - it('routes workspace subcommand', () => { - const resolved = resolveInvocation(['generate:workspace', '--root', './packages']); - expect(resolved.mode).toBe('generate:workspace'); - expect(resolved.commandArgs).toEqual(['--root', './packages']); + it("routes workspace subcommand", () => { + const resolved = resolveInvocation([ + "generate:workspace", + "--root", + "./packages", + ]); + expect(resolved.mode).toBe("generate:workspace"); + expect(resolved.commandArgs).toStrictEqual(["--root", "./packages"]); }); - it('routes init subcommand', () => { - const resolved = resolveInvocation(['init', '--force']); - expect(resolved.mode).toBe('init'); - expect(resolved.commandArgs).toEqual(['--force']); + it("routes init subcommand", () => { + const resolved = resolveInvocation(["init", "--force"]); + expect(resolved.mode).toBe("init"); + expect(resolved.commandArgs).toStrictEqual(["--force"]); }); }); diff --git a/test/template.test.ts b/test/template.test.ts index a11c3c0..3363bf9 100644 --- a/test/template.test.ts +++ b/test/template.test.ts @@ -1,23 +1,22 @@ -import { describe, expect, it } from 'vitest'; -import { baseReadmeTemplate } from '../src/readme-generator/template'; +import { baseReadmeTemplate } from "../src/readme-generator/template"; -describe('baseReadmeTemplate', () => { - it('renders neutral markdown without c15t defaults', () => { +describe(baseReadmeTemplate, () => { + it("renders neutral markdown without c15t defaults", () => { const markdown = baseReadmeTemplate({ - title: 'Readie Demo', - description: 'A neutral README.', + description: "A neutral README.", + features: ["Fast", "Simple"], includeTableOfContents: true, - features: ['Fast', 'Simple'], - installation: ['```bash\nnpm install readie-demo\n```'], - usage: ['Run the command', '```bash\nnpx readie\n```'], - security: 'Please report issues privately.', + installation: ["```bash\nnpm install readie-demo\n```"], + security: "Please report issues privately.", + title: "Readie Demo", + usage: ["Run the command", "```bash\nnpx readie\n```"], }); - expect(markdown).toContain('# Readie Demo'); - expect(markdown).toContain('## Table of Contents'); - expect(markdown).toContain('## Key Features'); - expect(markdown).toContain('## Security'); - expect(markdown).not.toContain('c15t'); - expect(markdown).not.toContain('consent.io'); + expect(markdown).toContain("# Readie Demo"); + expect(markdown).toContain("## Table of Contents"); + expect(markdown).toContain("## Key Features"); + expect(markdown).toContain("## Security"); + expect(markdown).not.toContain("c15t"); + expect(markdown).not.toContain("consent.io"); }); }); diff --git a/test/validate-config.test.ts b/test/validate-config.test.ts index 4ba5f24..aa109e3 100644 --- a/test/validate-config.test.ts +++ b/test/validate-config.test.ts @@ -1,36 +1,38 @@ -import fs from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { loadReadieConfig } from '../src/config/load-config'; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { loadReadieConfig } from "../src/config/load-config"; const createTempFile = async (contents: string) => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'readie-test-')); - const filePath = path.join(dir, 'readie.json'); - await fs.writeFile(filePath, contents, 'utf8'); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "readie-test-")); + const filePath = path.join(dir, "readie.json"); + await fs.writeFile(filePath, contents, "utf8"); return filePath; }; -describe('loadReadieConfig', () => { - it('loads a valid config', async () => { +describe(loadReadieConfig, () => { + it("loads a valid config", async () => { const configPath = await createTempFile( JSON.stringify({ - title: 'Test Project', - description: 'Config validation test.', - }), + description: "Config validation test.", + title: "Test Project", + }) ); const config = await loadReadieConfig(configPath); - expect(config.title).toBe('Test Project'); + expect(config.title).toBe("Test Project"); }); - it('throws for invalid config', async () => { + it("throws for invalid config", async () => { const configPath = await createTempFile( JSON.stringify({ - description: 'Missing title should fail.', - }), + description: "Missing title should fail.", + }) ); - await expect(loadReadieConfig(configPath)).rejects.toThrow('Configuration validation failed'); + await expect(loadReadieConfig(configPath)).rejects.toThrow( + "Configuration validation failed" + ); }); }); From 3da1fb4cef414933e47d624cc37b5e89e04df06d Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 21:11:41 +0000 Subject: [PATCH 03/12] Refactor .oxlintrc.json by removing unnecessary rules for a cleaner configuration --- .oxlintrc.json | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index ef7d70e..076354e 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,19 +1,4 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", - "extends": ["./node_modules/ultracite/config/oxlint/core/.oxlintrc.json"], - "rules": { - "complexity": "off", - "func-style": "off", - "import/no-nodejs-modules": "off", - "import/no-relative-parent-imports": "off", - "jest/max-expects": "off", - "jest/require-hook": "off", - "jest/valid-title": "off", - "max-statements": "off", - "no-nested-ternary": "off", - "no-shadow": "off", - "no-useless-return": "off", - "promise/prefer-await-to-callbacks": "off", - "unicorn/no-useless-fallback-in-spread": "off" - } + "extends": ["./node_modules/ultracite/config/oxlint/core/.oxlintrc.json"] } From 97c231461eb5d8d4dfc17dcd1919b288a1894115 Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 21:18:09 +0000 Subject: [PATCH 04/12] Update dependencies and enhance configuration for Readie project - Updated bun.lock to use "latest" versions for all dependencies, ensuring compatibility with the latest features and fixes. - Modified package.json to reflect updated versions for several packages, including @changesets and typescript. - Enhanced tsconfig.json with new compiler options for better module resolution and stricter type checking. - Added import paths in package.json and tsconfig.json for improved module management. - Refactored import statements in source files to utilize the new path aliases, improving code readability and maintainability. - Updated README and test files to align with the new configuration and ensure proper functionality. --- bun.lock | 180 +++++++--- package.json | 19 +- src/cli/commands/generate-workspace.ts | 9 +- src/cli/commands/generate.ts | 17 +- src/cli/commands/init.ts | 16 +- src/cli/resolve-invocation.ts | 64 +--- src/config/load-config.ts | 144 +++++--- src/config/starter-config.ts | 2 +- src/index.ts | 82 +++-- src/readme-generator/generator.ts | 206 +++++++---- src/readme-generator/template.ts | 375 +++++++++++--------- test/generator-global-interpolation.test.ts | 85 ++--- test/merge-config.test.ts | 8 +- test/resolve-invocation.test.ts | 4 +- test/template.test.ts | 20 +- test/validate-config.test.ts | 16 +- tsconfig.json | 38 +- 17 files changed, 754 insertions(+), 531 deletions(-) diff --git a/bun.lock b/bun.lock index 1c22ca2..241dc61 100644 --- a/bun.lock +++ b/bun.lock @@ -5,24 +5,28 @@ "": { "name": "readie", "dependencies": { - "@effect/cli": "^0.73.2", - "@effect/platform": "^0.94.5", - "@effect/platform-node": "^0.104.1", - "@effect/printer": "^0.47.0", - "@effect/printer-ansi": "^0.47.0", - "effect": "^3.19.16", + "@effect/cli": "latest", + "@effect/platform": "latest", + "@effect/platform-node": "latest", + "@effect/printer": "latest", + "@effect/printer-ansi": "latest", + "effect": "latest", }, "devDependencies": { - "@changesets/changelog-github": "^0.5.1", - "@changesets/cli": "^2.29.7", - "@types/node": "^24.3.0", - "changeset-conventional-commits": "^0.2.5", - "lefthook": "^2.1.1", - "oxfmt": "^0.34.0", - "oxlint": "^1.49.0", - "typescript": "^5.9.2", - "ultracite": "7.2.3", - "vitest": "^3.2.4", + "@changesets/changelog-github": "latest", + "@changesets/cli": "latest", + "@types/fs-extra": "latest", + "@types/node": "latest", + "changeset-conventional-commits": "latest", + "fs-extra": "latest", + "lefthook": "latest", + "oxfmt": "latest", + "oxlint": "latest", + "pathe": "latest", + "tempy": "latest", + "typescript": "latest", + "ultracite": "latest", + "vitest": "latest", }, }, }, @@ -337,21 +341,25 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], + "@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + "@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="], - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], + + "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + + "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -373,9 +381,7 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -383,8 +389,6 @@ "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], - "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], - "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], "citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], @@ -397,11 +401,9 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "crypto-random-string": ["crypto-random-string@4.0.0", "", { "dependencies": { "type-fest": "^1.0.1" } }, "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA=="], - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], @@ -447,7 +449,7 @@ "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -477,6 +479,8 @@ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], @@ -485,13 +489,11 @@ "jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], - "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], "lefthook": ["lefthook@2.1.1", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.1.1", "lefthook-darwin-x64": "2.1.1", "lefthook-freebsd-arm64": "2.1.1", "lefthook-freebsd-x64": "2.1.1", "lefthook-linux-arm64": "2.1.1", "lefthook-linux-x64": "2.1.1", "lefthook-openbsd-arm64": "2.1.1", "lefthook-openbsd-x64": "2.1.1", "lefthook-windows-arm64": "2.1.1", "lefthook-windows-x64": "2.1.1" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-Tl9h9c+sG3ShzTHKuR3LAIblnnh+Mgxnm2Ul7yu9cu260Z27LEbO3V6Zw4YZFP59/2rlD42pt/llYsQCkkCFzw=="], @@ -519,8 +521,6 @@ "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], - "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -537,8 +537,6 @@ "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="], "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], @@ -555,6 +553,8 @@ "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], "oxfmt": ["oxfmt@0.34.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.34.0", "@oxfmt/binding-android-arm64": "0.34.0", "@oxfmt/binding-darwin-arm64": "0.34.0", "@oxfmt/binding-darwin-x64": "0.34.0", "@oxfmt/binding-freebsd-x64": "0.34.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.34.0", "@oxfmt/binding-linux-arm-musleabihf": "0.34.0", "@oxfmt/binding-linux-arm64-gnu": "0.34.0", "@oxfmt/binding-linux-arm64-musl": "0.34.0", "@oxfmt/binding-linux-ppc64-gnu": "0.34.0", "@oxfmt/binding-linux-riscv64-gnu": "0.34.0", "@oxfmt/binding-linux-riscv64-musl": "0.34.0", "@oxfmt/binding-linux-s390x-gnu": "0.34.0", "@oxfmt/binding-linux-x64-gnu": "0.34.0", "@oxfmt/binding-linux-x64-musl": "0.34.0", "@oxfmt/binding-openharmony-arm64": "0.34.0", "@oxfmt/binding-win32-arm64-msvc": "0.34.0", "@oxfmt/binding-win32-ia32-msvc": "0.34.0", "@oxfmt/binding-win32-x64-msvc": "0.34.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-t+zTE4XGpzPTK+Zk9gSwcJcFi4pqjl6PwO/ZxPBJiJQ2XCKMucwjPlHxvPHyVKJtkMSyrDGfQ7Ntg/hUr4OgHQ=="], @@ -583,8 +583,6 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -645,23 +643,23 @@ "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], - "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + "temp-dir": ["temp-dir@3.0.0", "", {}, "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw=="], + + "tempy": ["tempy@3.2.0", "", { "dependencies": { "is-stream": "^3.0.0", "temp-dir": "^3.0.0", "type-fest": "^2.12.2", "unique-string": "^3.0.0" } }, "sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ=="], + "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -671,23 +669,25 @@ "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "ultracite": ["ultracite@7.2.3", "", { "dependencies": { "@clack/prompts": "^1.0.1", "commander": "^14.0.3", "deepmerge": "^4.3.1", "glob": "^13.0.3", "jsonc-parser": "^3.3.1", "nypm": "^0.6.5" }, "peerDependencies": { "oxlint": "^1.0.0" }, "optionalPeers": ["oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-WKNS2sKAZe4BHu+JGbZebXvy/A1QagDaBnndrK/zwOJAze/mQ8jeHfdG2bPlv3qcJ5fdS3w2Kd7c/eIcH78HvA=="], "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "unique-string": ["unique-string@3.0.0", "", { "dependencies": { "crypto-random-string": "^4.0.0" } }, "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -703,6 +703,18 @@ "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "@changesets/apply-release-plan/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "@changesets/cli/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "@changesets/config/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "@changesets/pre/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "@changesets/read/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "@changesets/write/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -711,6 +723,10 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "@types/fs-extra/@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], + + "@types/jsonfile/@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], + "changeset-conventional-commits/@changesets/read": ["@changesets/read@0.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.1", "@changesets/git": "^2.0.0", "@changesets/logger": "^0.0.5", "@changesets/parse": "^0.3.16", "@changesets/types": "^5.2.1", "chalk": "^2.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0" } }, "sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ=="], "changeset-conventional-commits/@changesets/types": ["@changesets/types@5.2.1", "", {}, "sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg=="], @@ -719,13 +735,47 @@ "changeset-conventional-commits/@manypkg/get-packages": ["@manypkg/get-packages@2.2.2", "", { "dependencies": { "@manypkg/find-root": "^2.2.2", "@manypkg/tools": "^1.1.1" } }, "sha512-3+Zd8kLZmsyJFmWTBtY0MAuCErI7yKB2cjMBlujvSVKZ2R/BMXi0kjCXu2dtRlSq/ML86t1FkumT0yreQ3n8OQ=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], - "nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], - "vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + "@changesets/apply-release-plan/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@changesets/apply-release-plan/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@changesets/cli/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@changesets/cli/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@changesets/config/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@changesets/config/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@changesets/pre/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@changesets/pre/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@changesets/read/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@changesets/read/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@changesets/write/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@changesets/write/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@manypkg/find-root/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@manypkg/find-root/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@manypkg/get-packages/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@manypkg/get-packages/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@types/fs-extra/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/jsonfile/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "changeset-conventional-commits/@changesets/read/@changesets/git": ["@changesets/git@2.0.0", "", { "dependencies": { "@babel/runtime": "^7.20.1", "@changesets/errors": "^0.1.4", "@changesets/types": "^5.2.1", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.2", "spawndamnit": "^2.0.0" } }, "sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A=="], @@ -735,6 +785,10 @@ "changeset-conventional-commits/@changesets/read/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + "changeset-conventional-commits/@changesets/read/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "changeset-conventional-commits/@changesets/write/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + "changeset-conventional-commits/@changesets/write/human-id": ["human-id@1.0.2", "", {}, "sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw=="], "changeset-conventional-commits/@manypkg/get-packages/@manypkg/find-root": ["@manypkg/find-root@2.2.3", "", { "dependencies": { "@manypkg/tools": "^1.1.2" } }, "sha512-jtEZKczWTueJYHjGpxU3KJQ08Gsrf4r6Q2GjmPp/RGk5leeYAA1eyDADSAF+KVCsQ6EwZd/FMcOFCoMhtqdCtQ=="], @@ -749,6 +803,14 @@ "changeset-conventional-commits/@changesets/read/@changesets/parse/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "changeset-conventional-commits/@changesets/read/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "changeset-conventional-commits/@changesets/read/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "changeset-conventional-commits/@changesets/write/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "changeset-conventional-commits/@changesets/write/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "changeset-conventional-commits/@changesets/read/@changesets/git/@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], "changeset-conventional-commits/@changesets/read/@changesets/git/@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -759,6 +821,10 @@ "changeset-conventional-commits/@changesets/read/@changesets/parse/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "changeset-conventional-commits/@changesets/read/@changesets/git/@manypkg/get-packages/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "changeset-conventional-commits/@changesets/read/@changesets/git/@manypkg/get-packages/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "changeset-conventional-commits/@changesets/read/@changesets/git/spawndamnit/cross-spawn/lru-cache": ["lru-cache@4.1.5", "", { "dependencies": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" } }, "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g=="], "changeset-conventional-commits/@changesets/read/@changesets/git/spawndamnit/cross-spawn/shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], diff --git a/package.json b/package.json index e54b105..f7fd572 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,9 @@ ], "type": "module", "main": "dist/index.js", + "imports": { + "#src/*": "./src/*" + }, "scripts": { "build": "bun build src/index.ts --outdir dist --target node --minify", "clean": "bun --eval \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true });\"", @@ -52,18 +55,22 @@ "@effect/platform-node": "^0.104.1", "@effect/printer": "^0.47.0", "@effect/printer-ansi": "^0.47.0", - "effect": "^3.19.16" + "effect": "^3.19.18" }, "devDependencies": { - "@changesets/changelog-github": "^0.5.1", - "@changesets/cli": "^2.29.7", - "@types/node": "^24.3.0", + "@changesets/changelog-github": "^0.5.2", + "@changesets/cli": "^2.29.8", + "@types/fs-extra": "^11.0.4", + "@types/node": "^25.3.0", "changeset-conventional-commits": "^0.2.5", + "fs-extra": "^11.3.3", "lefthook": "^2.1.1", "oxfmt": "^0.34.0", "oxlint": "^1.49.0", - "typescript": "^5.9.2", + "pathe": "^2.0.3", + "tempy": "^3.2.0", + "typescript": "^5.9.3", "ultracite": "7.2.3", - "vitest": "^3.2.4" + "vitest": "^4.0.18" } } diff --git a/src/cli/commands/generate-workspace.ts b/src/cli/commands/generate-workspace.ts index d186547..5211d0a 100644 --- a/src/cli/commands/generate-workspace.ts +++ b/src/cli/commands/generate-workspace.ts @@ -1,12 +1,11 @@ -import path from "node:path"; - import { Command, Options } from "@effect/cli"; import { Effect } from "effect"; +import { resolve } from "pathe"; import { generateWorkspaceReadmes, parsePackageList, -} from "../../readme-generator/generator.js"; +} from "#src/readme-generator/generator"; interface GenerateWorkspaceCommandArgs { root: string; @@ -40,7 +39,7 @@ export const generateWorkspaceCommand = Command.make( root: Options.directory("root").pipe( Options.withAlias("r"), Options.withDescription("Workspace root directory"), - Options.withDefault(path.resolve("./packages")) + Options.withDefault(resolve("./packages")) ), strict: Options.boolean("strict").pipe( Options.withDescription("Exit with code 1 if any project fails") @@ -54,7 +53,7 @@ export const generateWorkspaceCommand = Command.make( strict, noGlobal, }: GenerateWorkspaceCommandArgs) => - Effect.gen(function* generateWorkspaceCommand() { + Effect.gen(function* runGenerateWorkspaceCommand() { const result = yield* Effect.tryPromise({ catch: (error: unknown) => error instanceof Error diff --git a/src/cli/commands/generate.ts b/src/cli/commands/generate.ts index 713117d..033c95c 100644 --- a/src/cli/commands/generate.ts +++ b/src/cli/commands/generate.ts @@ -1,7 +1,7 @@ import { Command, Options } from "@effect/cli"; import { Effect } from "effect"; -import { generateReadmeFromConfig } from "../../readme-generator/generator.js"; +import { generateReadmeFromConfig } from "#src/readme-generator/generator"; interface GenerateCommandArgs { config: string; @@ -10,6 +10,13 @@ interface GenerateCommandArgs { noGlobal: boolean; } +const resultStatus = (updated: boolean, dryRun: boolean) => { + if (!updated) { + return "No changes"; + } + return dryRun ? "Would update" : "Generated"; +}; + export const generateCommand = Command.make( "generate", { @@ -31,7 +38,7 @@ export const generateCommand = Command.make( ), }, ({ config, output, dryRun, noGlobal }: GenerateCommandArgs) => - Effect.gen(function* generateCommand() { + Effect.gen(function* runGenerateCommand() { const result = yield* Effect.tryPromise({ catch: (error: unknown) => error instanceof Error @@ -47,11 +54,7 @@ export const generateCommand = Command.make( }); yield* Effect.sync(() => { - const status = result.updated - ? (dryRun - ? "Would update" - : "Generated") - : "No changes"; + const status = resultStatus(result.updated, dryRun); console.log(`${status}: ${result.outputPath}`); }); }) diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 8faabc1..bb31313 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -1,11 +1,9 @@ -import * as fssync from "node:fs"; -import fs from "node:fs/promises"; -import path from "node:path"; - import { Command, Options } from "@effect/cli"; import { Effect } from "effect"; +import { existsSync, writeFile } from "fs-extra"; +import { resolve } from "pathe"; -import { starterConfigText } from "../../config/starter-config.js"; +import { starterConfigText } from "#src/config/starter-config"; interface InitCommandArgs { config: string; @@ -26,9 +24,9 @@ export const initCommand = Command.make( ), }, ({ config, force }: InitCommandArgs) => - Effect.gen(function* initCommand() { - const configPath = path.resolve(config); - const exists = fssync.existsSync(configPath); + Effect.gen(function* runInitCommand() { + const configPath = resolve(config); + const exists = existsSync(configPath); if (exists && !force) { yield* Effect.fail( @@ -43,7 +41,7 @@ export const initCommand = Command.make( error instanceof Error ? error : new Error(`Failed to write config: ${String(error)}`), - try: () => fs.writeFile(configPath, starterConfigText, "utf8"), + try: () => writeFile(configPath, starterConfigText, "utf8"), }); yield* Effect.sync(() => { diff --git a/src/cli/resolve-invocation.ts b/src/cli/resolve-invocation.ts index c0a4e55..5e33c32 100644 --- a/src/cli/resolve-invocation.ts +++ b/src/cli/resolve-invocation.ts @@ -14,60 +14,30 @@ export interface ResolvedInvocation { const isHelpFlag = (value: string | undefined) => value === "--help" || value === "-h"; -export const resolveInvocation = (args: string[]): ResolvedInvocation => { - const [first, ...rest] = args; - - if (!first) { - return { - commandArgs: [], - mode: "generate", - originalArgs: args, - }; +const modeFromToken = (token: string | undefined): InvocationMode => { + if (!token) { + return "generate"; } - - if (isHelpFlag(first)) { - return { - commandArgs: rest, - mode: "help", - originalArgs: args, - }; + if (isHelpFlag(token) || token === "help") { + return "help"; } - if (first === "generate") { - return { - commandArgs: rest, - mode: "generate", - originalArgs: args, - }; - } - - if (first === "generate:workspace") { - return { - commandArgs: rest, - mode: "generate:workspace", - originalArgs: args, - }; - } + const commandModes: Record = { + generate: "generate", + "generate:workspace": "generate:workspace", + init: "init", + }; - if (first === "init") { - return { - commandArgs: rest, - mode: "init", - originalArgs: args, - }; - } + return commandModes[token] ?? "unknown"; +}; - if (first === "help") { - return { - commandArgs: rest, - mode: "help", - originalArgs: args, - }; - } +export const resolveInvocation = (args: string[]): ResolvedInvocation => { + const [first, ...rest] = args; + const mode = modeFromToken(first); return { - commandArgs: args, - mode: "unknown", + commandArgs: mode === "unknown" ? args : rest, + mode, originalArgs: args, }; }; diff --git a/src/config/load-config.ts b/src/config/load-config.ts index 99043da..30093c7 100644 --- a/src/config/load-config.ts +++ b/src/config/load-config.ts @@ -1,9 +1,8 @@ -import fs from "node:fs/promises"; -import path from "node:path"; - import { Schema } from "effect"; +import { pathExists, readFile } from "fs-extra"; +import { dirname, join, resolve } from "pathe"; -import type { ReadieConfig, ReadieGlobalConfig } from "./types.js"; +import type { ReadieConfig, ReadieGlobalConfig } from "./types"; const commandSchema = Schema.Struct({ description: Schema.NonEmptyString, @@ -101,7 +100,7 @@ const decodeReadieGlobalConfig = Schema.decodeUnknownSync( const GLOBAL_CONFIG_NAME = "readie.global.json"; const parseJsonFile = async (absolutePath: string): Promise => { - const raw = await fs.readFile(absolutePath, "utf8"); + const raw = await readFile(absolutePath, "utf8"); try { return JSON.parse(raw); } catch (error) { @@ -115,7 +114,7 @@ const parseJsonFile = async (absolutePath: string): Promise => { export const loadReadieConfig = async ( configPath: string ): Promise => { - const absolutePath = path.resolve(configPath); + const absolutePath = resolve(configPath); const parsed = await parseJsonFile(absolutePath); try { @@ -132,7 +131,7 @@ export const loadReadieConfig = async ( const loadGlobalReadieConfig = async ( configPath: string ): Promise => { - const absolutePath = path.resolve(configPath); + const absolutePath = resolve(configPath); const parsed = await parseJsonFile(absolutePath); try { @@ -149,18 +148,15 @@ const loadGlobalReadieConfig = async ( export const loadGlobalConfig = async ( startDir: string ): Promise => { - let current = path.resolve(startDir); + let current = resolve(startDir); while (true) { - const candidate = path.join(current, GLOBAL_CONFIG_NAME); - try { - await fs.access(candidate); + const candidate = join(current, GLOBAL_CONFIG_NAME); + if (await pathExists(candidate)) { return await loadGlobalReadieConfig(candidate); - } catch { - // Keep walking up until filesystem root. } - const parent = path.dirname(current); + const parent = dirname(current); if (parent === current) { return null; } @@ -183,17 +179,43 @@ const interpolatePlaceholders = ( (match, key: string) => placeholders[key] ?? match ); -const interpolateTopLevelStrings = ( +const createInterpolationPlaceholders = ( config: ReadieConfig, interpolationContext: InterpolationContext -): ReadieConfig => { +): Record => { const resolvedPackageName = interpolationContext.packageName?.trim() || config.title; - const placeholders: Record = { + + return { packageName: resolvedPackageName, packageNameEncoded: encodeURIComponent(resolvedPackageName), title: config.title, }; +}; + +const interpolateCustomSections = ( + customSections: Record | undefined, + placeholders: Record +): Record | undefined => { + if (!customSections) { + return; + } + + const interpolatedSections: Record = {}; + for (const [key, value] of Object.entries(customSections)) { + interpolatedSections[key] = interpolatePlaceholders(value, placeholders); + } + return interpolatedSections; +}; + +const interpolateTopLevelStrings = ( + config: ReadieConfig, + interpolationContext: InterpolationContext +): ReadieConfig => { + const placeholders = createInterpolationPlaceholders( + config, + interpolationContext + ); const interpolated = { ...config } as Record; for (const [key, value] of Object.entries(interpolated)) { @@ -202,14 +224,10 @@ const interpolateTopLevelStrings = ( } } - // Interpolate customSections values - if (config.customSections) { - const interpolatedSections: Record = {}; - for (const [key, value] of Object.entries(config.customSections)) { - interpolatedSections[key] = interpolatePlaceholders(value, placeholders); - } - interpolated.customSections = interpolatedSections; - } + interpolated.customSections = interpolateCustomSections( + config.customSections, + placeholders + ); return interpolated as unknown as ReadieConfig; }; @@ -231,42 +249,56 @@ const resolveMergedValue = ( return globalValue === null ? undefined : (globalValue as T | undefined); }; +const readGlobalCustomSections = (global: Record) => { + if (typeof global.customSections !== "object" || !global.customSections) { + return; + } + return global.customSections as Record; +}; + +const readProjectCustomSections = (project: Record) => { + const projectCustomSections = project.customSections; + if (projectCustomSections === null) { + return null; + } + if (typeof projectCustomSections !== "object" || !projectCustomSections) { + return; + } + return projectCustomSections as Record; +}; + +const resolveMergedCustomSections = ( + globalConfig: ReadieGlobalConfig | null, + projectConfig: ReadieConfig +) => { + const project = projectConfig as unknown as Record; + const global = (globalConfig ?? {}) as Record; + const globalCustomSections = readGlobalCustomSections(global); + + if (!hasOwn(project, "customSections")) { + return globalCustomSections; + } + + const projectCustomSections = readProjectCustomSections(project); + if (projectCustomSections === null || !projectCustomSections) { + return; + } + + return { + ...globalCustomSections, + ...projectCustomSections, + }; +}; + export const mergeConfigs = ( globalConfig: ReadieGlobalConfig | null, projectConfig: ReadieConfig, interpolationContext: InterpolationContext = {} ): ReadieConfig => { - const mergedCustomSections = (() => { - const project = projectConfig as unknown as Record; - const global = (globalConfig ?? {}) as Record; - - if (hasOwn(project, "customSections")) { - const projectCustomSections = project.customSections; - if (projectCustomSections === null) { - return; - } - if ( - typeof projectCustomSections === "object" && - projectCustomSections !== null - ) { - return { - ...((global.customSections as Record | undefined) ?? - {}), - ...(projectCustomSections as Record), - }; - } - return; - } - - if ( - typeof global.customSections === "object" && - global.customSections !== null - ) { - return global.customSections as Record; - } - - return; - })(); + const mergedCustomSections = resolveMergedCustomSections( + globalConfig, + projectConfig + ); const merged: ReadieConfig = { $schema: resolveMergedValue("$schema", projectConfig, globalConfig), diff --git a/src/config/starter-config.ts b/src/config/starter-config.ts index 065c7ea..ab5eb46 100644 --- a/src/config/starter-config.ts +++ b/src/config/starter-config.ts @@ -1,4 +1,4 @@ -import type { ReadieConfig } from "./types.js"; +import type { ReadieConfig } from "./types"; export const DEFAULT_SCHEMA_URL = "https://unpkg.com/readie/schemas/readie.schema.json"; diff --git a/src/index.ts b/src/index.ts index 178ce85..007c79f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,14 +2,13 @@ import { Command, ValidationError } from "@effect/cli"; import * as NodeContext from "@effect/platform-node/NodeContext"; -import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import { Effect } from "effect"; -import { generateWorkspaceCommand } from "./cli/commands/generate-workspace.js"; -import { generateCommand } from "./cli/commands/generate.js"; -import { initCommand } from "./cli/commands/init.js"; -import { printRootHelp } from "./cli/help.js"; -import { resolveInvocation } from "./cli/resolve-invocation.js"; +import { generateCommand } from "./cli/commands/generate"; +import { generateWorkspaceCommand } from "./cli/commands/generate-workspace"; +import { initCommand } from "./cli/commands/init"; +import { printRootHelp } from "./cli/help"; +import { resolveInvocation } from "./cli/resolve-invocation"; const version = "0.1.0"; @@ -31,41 +30,46 @@ const runInit = (args: string[]) => version, })(args).pipe(Effect.provide(NodeContext.layer)); -const resolved = resolveInvocation(process.argv.slice(2)); +const selectCommandEffect = ( + resolved: ReturnType +) => { + if (resolved.mode === "generate") { + return runGenerate(resolved.commandArgs); + } + if (resolved.mode === "generate:workspace") { + return runGenerateWorkspace(resolved.commandArgs); + } + return runInit(resolved.commandArgs); +}; -if (resolved.mode === "help") { - printRootHelp(); - process.exit(0); -} +const handleError = (error: unknown) => { + if (ValidationError.isValidationError(error)) { + console.error(String(error)); + process.exitCode = 1; + return; + } -if (resolved.mode === "unknown") { - printRootHelp(); - process.exit(1); -} + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +}; -const commandEffect = - resolved.mode === "generate" - ? runGenerate(resolved.commandArgs) - : (resolved.mode === "generate:workspace" - ? runGenerateWorkspace(resolved.commandArgs) - : runInit(resolved.commandArgs)); +const main = async () => { + const resolved = resolveInvocation(process.argv.slice(2)); -const program = commandEffect.pipe( - Effect.catchIf( - (error): error is ValidationError.ValidationError => - ValidationError.isValidationError(error), - (error) => - Effect.sync(() => { - console.error(String(error)); - process.exitCode = 1; - }) - ), - Effect.catchAll((error) => - Effect.sync(() => { - console.error(error instanceof Error ? error.message : String(error)); - process.exitCode = 1; - }) - ) -); + if (resolved.mode === "help") { + printRootHelp(); + process.exit(0); + } + if (resolved.mode === "unknown") { + printRootHelp(); + process.exit(1); + } -NodeRuntime.runMain(program); + try { + await Effect.runPromise(selectCommandEffect(resolved)); + } catch (error) { + handleError(error); + } +}; + +await main(); diff --git a/src/readme-generator/generator.ts b/src/readme-generator/generator.ts index 46d6e8d..7169b91 100644 --- a/src/readme-generator/generator.ts +++ b/src/readme-generator/generator.ts @@ -1,19 +1,26 @@ -import * as fssync from "node:fs"; -import fs from "node:fs/promises"; -import path from "node:path"; +import { + existsSync, + pathExists, + readdir, + readFile, + readJson, + writeFile, +} from "fs-extra"; +import { basename, dirname, join, resolve } from "pathe"; import { loadGlobalConfig, loadReadieConfig, mergeConfigs, -} from "../config/load-config.js"; +} from "#src/config/load-config"; import type { GenerateSingleOptions, GenerateSingleResult, GenerateWorkspaceOptions, GenerateWorkspaceResult, -} from "../config/types.js"; -import { baseReadmeTemplate } from "./template.js"; +} from "#src/config/types"; + +import { baseReadmeTemplate } from "./template"; export const parsePackageList = (values: string[]): Set => { const packages = new Set(); @@ -34,44 +41,56 @@ const resolveOutputPath = ( cliOutputPath: string | undefined ) => { if (cliOutputPath) { - return path.resolve(cliOutputPath); + return resolve(cliOutputPath); } if (configOutputPath) { - return path.resolve(path.dirname(configPath), configOutputPath); + return resolve(dirname(configPath), configOutputPath); } - return path.resolve(path.dirname(configPath), "README.md"); + return resolve(dirname(configPath), "README.md"); }; const resolvePackageName = async ( configPath: string ): Promise => { - const packageJsonPath = path.join(path.dirname(configPath), "package.json"); + const packageJsonPath = join(dirname(configPath), "package.json"); try { - const rawPackageJson = await fs.readFile(packageJsonPath, "utf8"); - const parsed = JSON.parse(rawPackageJson) as { name?: unknown }; + const parsed = (await readJson(packageJsonPath)) as { name?: unknown }; return typeof parsed.name === "string" && parsed.name.trim().length > 0 ? parsed.name : undefined; - } catch (error) { - console.warn(`Package.json not found at ${packageJsonPath}:`, error); + } catch { return undefined; } }; +const loadMergedConfig = async ( + absoluteConfigPath: string, + useGlobalConfig: boolean +) => { + const projectConfig = await loadReadieConfig(absoluteConfigPath); + const globalConfig = useGlobalConfig + ? await loadGlobalConfig(dirname(absoluteConfigPath)) + : null; + const packageName = await resolvePackageName(absoluteConfigPath); + return mergeConfigs(globalConfig, projectConfig, { packageName }); +}; + +const readExistingContent = async (filePath: string) => { + if (!(await pathExists(filePath))) { + return null; + } + return readFile(filePath, "utf8"); +}; + export const generateReadmeFromConfig = async ({ configPath, outputPath, dryRun, useGlobalConfig = true, }: GenerateSingleOptions): Promise => { - const absoluteConfigPath = path.resolve(configPath); - const projectConfig = await loadReadieConfig(absoluteConfigPath); - const globalConfig = useGlobalConfig - ? await loadGlobalConfig(path.dirname(absoluteConfigPath)) - : null; - const packageName = await resolvePackageName(absoluteConfigPath); - const config = mergeConfigs(globalConfig, projectConfig, { packageName }); + const absoluteConfigPath = resolve(configPath); + const config = await loadMergedConfig(absoluteConfigPath, useGlobalConfig); const resolvedOutputPath = resolveOutputPath( absoluteConfigPath, config.output, @@ -79,9 +98,7 @@ export const generateReadmeFromConfig = async ({ ); const content = baseReadmeTemplate(config); - const existingContent = fssync.existsSync(resolvedOutputPath) - ? await fs.readFile(resolvedOutputPath, "utf8") - : null; + const existingContent = await readExistingContent(resolvedOutputPath); if (existingContent === content) { return { @@ -91,7 +108,7 @@ export const generateReadmeFromConfig = async ({ } if (!dryRun) { - await fs.writeFile(resolvedOutputPath, content, "utf8"); + await writeFile(resolvedOutputPath, content, "utf8"); } return { @@ -100,35 +117,102 @@ export const generateReadmeFromConfig = async ({ }; }; -export async function generateWorkspaceReadmes({ +const listProjectDirsWithConfig = async ( + rootDir: string, + configName: string +) => { + const entries = await readdir(rootDir, { withFileTypes: true }); + const allProjectDirs = entries + .filter((entry) => entry.isDirectory()) + .map((entry) => join(rootDir, entry.name)); + + const projectChecks = await Promise.all( + allProjectDirs.map(async (dir) => ({ + dir, + hasConfig: await pathExists(join(dir, configName)), + })) + ); + + return projectChecks.filter((item) => item.hasConfig).map((item) => item.dir); +}; + +const selectProjectDirs = ( + allProjectDirs: string[], + packageFilter: Set +) => + packageFilter.size > 0 + ? allProjectDirs.filter((dir) => packageFilter.has(basename(dir))) + : allProjectDirs; + +const collectSkippedByFilter = ( + allProjectDirs: string[], + packageFilter: Set +) => { + if (packageFilter.size === 0) { + return []; + } + return allProjectDirs + .map((dir) => basename(dir)) + .filter((dirName) => !packageFilter.has(dirName)); +}; + +const pushProjectResult = ( + result: GenerateWorkspaceResult, + projectName: string, + updated: boolean, + dryRun: boolean +) => { + if (updated) { + result.updated.push(projectName); + const action = dryRun ? "Would update" : "Generated"; + console.log(`${action} README for ${projectName}`); + return; + } + + result.unchanged.push(projectName); + console.log(`No changes for ${projectName}`); +}; + +const processWorkspaceProject = async ( + projectDir: string, + configName: string, + dryRun: boolean, + useGlobalConfig: boolean, + result: GenerateWorkspaceResult +) => { + const projectName = basename(projectDir); + const configPath = join(projectDir, configName); + try { + const singleResult = await generateReadmeFromConfig({ + configPath, + dryRun, + useGlobalConfig, + }); + pushProjectResult(result, projectName, singleResult.updated, dryRun); + } catch (error) { + result.failed.push({ error, projectDir: projectName }); + console.error(`Error generating README for ${projectName}:`, error); + } +}; + +export const generateWorkspaceReadmes = async ({ rootDir, configName, packageFilter, dryRun, useGlobalConfig = true, -}: GenerateWorkspaceOptions): Promise { - const absoluteRootDir = path.resolve(rootDir); - if (!fssync.existsSync(absoluteRootDir)) { +}: GenerateWorkspaceOptions): Promise => { + const absoluteRootDir = resolve(rootDir); + if (!existsSync(absoluteRootDir)) { throw new Error(`Workspace root not found at ${absoluteRootDir}`); } - const entries = await fs.readdir(absoluteRootDir, { withFileTypes: true }); - const allProjectDirs = entries - .filter((entry) => entry.isDirectory()) - .map((entry) => path.join(absoluteRootDir, entry.name)) - .filter((dir) => fssync.existsSync(path.join(dir, configName))); - - const selectedProjectDirs = - packageFilter.size > 0 - ? allProjectDirs.filter((dir) => packageFilter.has(path.basename(dir))) - : allProjectDirs; - - const skippedByFilter = - packageFilter.size > 0 - ? allProjectDirs - .map((dir) => path.basename(dir)) - .filter((dirName) => !packageFilter.has(dirName)) - : []; + const allProjectDirs = await listProjectDirsWithConfig( + absoluteRootDir, + configName + ); + const selectedProjectDirs = selectProjectDirs(allProjectDirs, packageFilter); + const skippedByFilter = collectSkippedByFilter(allProjectDirs, packageFilter); const result: GenerateWorkspaceResult = { failed: [], @@ -138,28 +222,14 @@ export async function generateWorkspaceReadmes({ }; for (const projectDir of selectedProjectDirs) { - const projectName = path.basename(projectDir); - const configPath = path.join(projectDir, configName); - try { - const singleResult = await generateReadmeFromConfig({ - configPath, - dryRun, - useGlobalConfig, - }); - if (singleResult.updated) { - result.updated.push(projectName); - console.log( - `${dryRun ? "Would update" : "Generated"} README for ${projectName}` - ); - } else { - result.unchanged.push(projectName); - console.log(`No changes for ${projectName}`); - } - } catch (error) { - result.failed.push({ error, projectDir: projectName }); - console.error(`Error generating README for ${projectName}:`, error); - } + await processWorkspaceProject( + projectDir, + configName, + dryRun, + useGlobalConfig, + result + ); } return result; -} +}; diff --git a/src/readme-generator/template.ts b/src/readme-generator/template.ts index 822651c..b0b84a6 100644 --- a/src/readme-generator/template.ts +++ b/src/readme-generator/template.ts @@ -2,11 +2,32 @@ import type { ReadieBadge, ReadieConfig, ReadieLicenseObject, -} from "../config/types.js"; +} from "#src/config/types.js"; const isNonEmpty = (value?: string | null): value is string => typeof value === "string" && value.trim().length > 0; +const normalizeSections = (sections: string[]) => + sections + .join("\n") + .replaceAll(/\n{3,}/g, "\n\n") + .trim(); + +const appendUsageItem = (lines: string[], item: string, index: number) => { + if (item.startsWith("```")) { + if (lines.length > 0 && lines.at(-1) !== "") { + lines.push(""); + } + lines.push(item); + lines.push(""); + return index; + } + + const cleaned = item.startsWith("- ") ? item.slice(2) : item; + lines.push(`${index}. ${cleaned}`); + return index + 1; +}; + const renderNumberedWithCodeBlocks = (items: string[]) => { const lines: string[] = []; let i = 1; @@ -16,30 +37,10 @@ const renderNumberedWithCodeBlocks = (items: string[]) => { if (!item) { continue; } - - if (item.startsWith("```")) { - if (lines.length > 0 && lines.at(-1) !== "") { - lines.push(""); - } - lines.push(item); - lines.push(""); - continue; - } - - if (item.startsWith("- ")) { - lines.push(`${i}. ${item.slice(2)}`); - i += 1; - continue; - } - - lines.push(`${i}. ${item}`); - i += 1; + i = appendUsageItem(lines, item, i); } - return lines - .join("\n") - .replaceAll(/\n{3,}/g, "\n\n") - .trim(); + return normalizeSections(lines); }; const addSection = ( @@ -54,167 +55,217 @@ const addSection = ( return `${header}\n\n${body}`.trim(); }; -export const baseReadmeTemplate = (rawConfig: ReadieConfig) => { - const config: ReadieConfig = { ...rawConfig }; - - const bannerBlock = isNonEmpty(config.banner) ? config.banner : ""; - const titleBlock = - isNonEmpty(bannerBlock) && bannerBlock.toLowerCase().includes(" 0 - ? config.badges - .map((badge: ReadieBadge) => { - const image = `![${badge.label}](${badge.image})`; - return isNonEmpty(badge.link) ? `[${image}](${badge.link})` : image; - }) - .join("\n") - : ""; - - const featuresBlock = - config.features && config.features.length > 0 - ? `## Key Features\n\n${config.features.map((feature) => `- ${feature}`).join("\n")}` - : ""; - - const prerequisitesBlock = addSection( - "## Prerequisites", - config.prerequisites - ); +const renderHeadingBlock = ( + heading: string, + content: string | undefined +): string => { + if (!isNonEmpty(content)) { + return ""; + } + if (content.trimStart().startsWith("## ")) { + return content; + } + return `${heading}\n\n${content}`; +}; - const quickStartBlock = isNonEmpty(config.quickStart) - ? (config.quickStart.trimStart().startsWith("## ") - ? config.quickStart - : `## Quick Start\n\n${config.quickStart}`) - : ""; +const renderBadges = (badges: ReadieBadge[] | undefined) => { + if (!badges || badges.length === 0) { + return ""; + } + return badges + .map((badge) => { + const image = `![${badge.label}](${badge.image})`; + return isNonEmpty(badge.link) ? `[${image}](${badge.link})` : image; + }) + .join("\n"); +}; - const manualInstallationBlock = - config.manualInstallation && config.manualInstallation.length > 0 - ? `## Manual Installation\n\n${config.manualInstallation.join("\n")}` - : ""; +const renderSimpleListSection = ( + heading: string, + items: string[] | undefined, + formatter: (value: string) => string = (value) => value +) => { + if (!items || items.length === 0) { + return ""; + } + return `${heading}\n\n${items.map(formatter).join("\n")}`; +}; - const installationBlock = - config.installation && config.installation.length > 0 - ? `## Installation\n\n${config.installation.join("\n")}` - : ""; +const renderCommandsSection = (config: ReadieConfig) => + renderSimpleListSection( + "## Available Commands", + config.commands?.map((cmd) => `- \`${cmd.name}\`: ${cmd.description}`) + ); - const usageBlock = - config.usage && config.usage.length > 0 - ? `## Usage\n\n${renderNumberedWithCodeBlocks(config.usage)}` - : ""; +const renderGlobalFlagsSection = (config: ReadieConfig) => + renderSimpleListSection( + "## Global Flags", + config.globalFlags?.map((flag) => `- \`${flag.flag}\`: ${flag.description}`) + ); - const commandsBlock = - config.commands && config.commands.length > 0 - ? `## Available Commands\n\n${config.commands.map((cmd) => `- \`${cmd.name}\`: ${cmd.description}`).join("\n")}` - : ""; +const renderLicenseBlock = (license: ReadieConfig["license"]) => { + if (!license) { + return ""; + } + if (typeof license === "string") { + return renderHeadingBlock("## License", license); + } + const { name, url } = license as ReadieLicenseObject; + return `## License\n\n[${name}](${url})`; +}; - const globalFlagsBlock = - config.globalFlags && config.globalFlags.length > 0 - ? `## Global Flags\n\n${config.globalFlags.map((flag) => `- \`${flag.flag}\`: ${flag.description}`).join("\n")}` - : ""; +interface ReadmeSections { + bannerBlock: string; + titleBlock: string; + badgesBlock: string; + featuresBlock: string; + prerequisitesBlock: string; + quickStartBlock: string; + installationBlock: string; + manualInstallationBlock: string; + usageBlock: string; + commandsBlock: string; + globalFlagsBlock: string; + docsBlock: string; + quickStartLinkBlock: string; + supportBlock: string; + contributingBlock: string; + securityBlock: string; + licenseBlock: string; + customSectionsBlock: string; + footerBlock: string; +} + +const createReadmeSections = (config: ReadieConfig): ReadmeSections => { + const bannerBlock = isNonEmpty(config.banner) ? config.banner : ""; + const titleBlock = + isNonEmpty(bannerBlock) && bannerBlock.toLowerCase().includes(" `## ${heading}\n\n${content}`) + .join("\n\n") + : "", + docsBlock: config.docsLink + ? `## Documentation For further information, guides, and examples visit the [reference documentation](${config.docsLink}).` - : ""; - - const quickStartLinkBlock = config.quickStartLink - ? `## Additional Quick Start + : "", + featuresBlock: renderSimpleListSection( + "## Key Features", + config.features, + (feature) => `- ${feature}` + ), + footerBlock: isNonEmpty(config.footer) ? config.footer : "", + globalFlagsBlock: renderGlobalFlagsSection(config), + installationBlock: renderSimpleListSection( + "## Installation", + config.installation + ), + licenseBlock: renderLicenseBlock(config.license), + manualInstallationBlock: renderSimpleListSection( + "## Manual Installation", + config.manualInstallation + ), + prerequisitesBlock: addSection("## Prerequisites", config.prerequisites), + quickStartBlock: renderHeadingBlock("## Quick Start", config.quickStart), + quickStartLinkBlock: config.quickStartLink + ? `## Additional Quick Start See the full quick start guide [here](${config.quickStartLink}).` - : ""; - - const customSectionsBlock = config.customSections - ? Object.entries(config.customSections) - .map(([heading, content]) => `## ${heading}\n\n${content}`) - .join("\n\n") - : ""; - - const supportBlock = addSection("## Support", config.support); - const contributingBlock = addSection("## Contributing", config.contributing); - const securityBlock = isNonEmpty(config.security) - ? (isNonEmpty(config.security) && - config.security.trimStart().startsWith("## ") - ? config.security - : `## Security\n\n${config.security}`) - : ""; - - const licenseBlock = (() => { - if (!config.license) { - return ""; - } - if (typeof config.license === "string") { - return config.license.trimStart().startsWith("## ") - ? config.license - : `## License\n\n${config.license}`; - } - const { name, url } = config.license as ReadieLicenseObject; - return `## License\n\n[${name}](${url})`; - })(); + : "", + securityBlock: renderHeadingBlock("## Security", config.security), + supportBlock: addSection("## Support", config.support), + titleBlock, + usageBlock: config.usage + ? `## Usage\n\n${renderNumberedWithCodeBlocks(config.usage)}` + : "", + }; +}; - const footerBlock = isNonEmpty(config.footer) ? config.footer : ""; +const slugifyHeading = (title: string) => + title + .toLowerCase() + .replaceAll(/[^a-z0-9 -]/g, "") + .trim() + .replaceAll(/\s+/g, "-"); +const createTocTitles = (config: ReadieConfig, sections: ReadmeSections) => { const tocSectionTitles = [ - ["Key Features", featuresBlock], - ["Prerequisites", prerequisitesBlock], - ["Quick Start", quickStartBlock], - ["Installation", installationBlock], - ["Manual Installation", manualInstallationBlock], - ["Usage", usageBlock], - ["Available Commands", commandsBlock], - ["Global Flags", globalFlagsBlock], - ["Documentation", docsBlock], - ["Additional Quick Start", quickStartLinkBlock], - ["Support", supportBlock], - ["Contributing", contributingBlock], - ["Security", securityBlock], - ["License", licenseBlock], + ["Key Features", sections.featuresBlock], + ["Prerequisites", sections.prerequisitesBlock], + ["Quick Start", sections.quickStartBlock], + ["Installation", sections.installationBlock], + ["Manual Installation", sections.manualInstallationBlock], + ["Usage", sections.usageBlock], + ["Available Commands", sections.commandsBlock], + ["Global Flags", sections.globalFlagsBlock], + ["Documentation", sections.docsBlock], + ["Additional Quick Start", sections.quickStartLinkBlock], + ["Support", sections.supportBlock], + ["Contributing", sections.contributingBlock], + ["Security", sections.securityBlock], + ["License", sections.licenseBlock], ].filter(([, section]) => isNonEmpty(section)); - if (isNonEmpty(customSectionsBlock)) { - for (const key of Object.keys(config.customSections ?? {})) { - tocSectionTitles.push([key, `## ${key}`]); - } + if (!isNonEmpty(sections.customSectionsBlock)) { + return tocSectionTitles; + } + + for (const key of Object.keys(config.customSections ?? {})) { + tocSectionTitles.push([key, `## ${key}`]); + } + return tocSectionTitles; +}; + +const createTocBlock = ( + includeTableOfContents: boolean | undefined, + titles: string[][] +) => { + if (includeTableOfContents === false || titles.length === 0) { + return ""; } + const links = titles + .map(([title]) => `- [${title}](#${slugifyHeading(title)})`) + .join("\n"); + return `## Table of Contents\n\n${links}`; +}; - const tocBlock = - config.includeTableOfContents !== false && tocSectionTitles.length > 0 - ? `## Table of Contents\n\n${tocSectionTitles - .map(([title]) => { - const slug = title - .toLowerCase() - .replaceAll(/[^a-z0-9 -]/g, "") - .trim() - .replaceAll(/\s+/g, "-"); - return `- [${title}](#${slug})`; - }) - .join("\n")}` - : ""; +export const baseReadmeTemplate = (rawConfig: ReadieConfig) => { + const config: ReadieConfig = { ...rawConfig }; + const sections = createReadmeSections(config); + const tocTitles = createTocTitles(config, sections); + const tocBlock = createTocBlock(config.includeTableOfContents, tocTitles); const readmeContent = [ - bannerBlock, - titleBlock, - badgesBlock, + sections.bannerBlock, + sections.titleBlock, + sections.badgesBlock, config.description, tocBlock, - featuresBlock, - prerequisitesBlock, - quickStartBlock, - installationBlock, - manualInstallationBlock, - usageBlock, - commandsBlock, - globalFlagsBlock, - docsBlock, - quickStartLinkBlock, - supportBlock, - contributingBlock, - securityBlock, - licenseBlock, - customSectionsBlock, - footerBlock, + sections.featuresBlock, + sections.prerequisitesBlock, + sections.quickStartBlock, + sections.installationBlock, + sections.manualInstallationBlock, + sections.usageBlock, + sections.commandsBlock, + sections.globalFlagsBlock, + sections.docsBlock, + sections.quickStartLinkBlock, + sections.supportBlock, + sections.contributingBlock, + sections.securityBlock, + sections.licenseBlock, + sections.customSectionsBlock, + sections.footerBlock, ] .filter((section) => isNonEmpty(section)) .join("\n\n") diff --git a/test/generator-global-interpolation.test.ts b/test/generator-global-interpolation.test.ts index 0e2f771..98b9dfd 100644 --- a/test/generator-global-interpolation.test.ts +++ b/test/generator-global-interpolation.test.ts @@ -1,51 +1,52 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; +import { ensureDir, readFile, remove, writeFile } from "fs-extra"; +import { join } from "pathe"; +import { temporaryDirectory } from "tempy"; -import { generateReadmeFromConfig } from "../src/readme-generator/generator"; +import { generateReadmeFromConfig } from "#src/readme-generator/generator.js"; const writeJson = async (filePath: string, value: unknown) => { - await fs.writeFile(filePath, JSON.stringify(value, null, 2), "utf8"); + await writeFile(filePath, JSON.stringify(value, null, 2), "utf8"); +}; + +const setupFixture = async () => { + const rootDir = temporaryDirectory(); + const packageDir = join(rootDir, "packages", "react"); + await ensureDir(packageDir); + + await writeJson(join(rootDir, "readie.global.json"), { + banner: '

{{title}}

', + footer: "Built for {{ title }} ({{packageNameEncoded}})", + }); + + await writeJson(join(packageDir, "package.json"), { + name: "@c15t/react", + version: "1.0.0", + }); + + const configPath = join(packageDir, "readie.json"); + await writeJson(configPath, { + description: "CMP for React", + title: "@c15t/react: React Consent Components", + }); + + return { configPath, rootDir }; }; describe("generateReadmeFromConfig with global interpolation", () => { it("injects title and package placeholders in global config", async () => { - const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "readie-global-")); - try { - const packageDir = path.join(rootDir, "packages", "react"); - await fs.mkdir(packageDir, { recursive: true }); - - await writeJson(path.join(rootDir, "readie.global.json"), { - banner: '

{{title}}

', - footer: "Built for {{ title }} ({{packageNameEncoded}})", - }); - - await writeJson(path.join(packageDir, "package.json"), { - name: "@c15t/react", - version: "1.0.0", - }); - - const configPath = path.join(packageDir, "readie.json"); - await writeJson(configPath, { - description: "CMP for React", - title: "@c15t/react: React Consent Components", - }); - - const result = await generateReadmeFromConfig({ - configPath, - dryRun: false, - }); - - const generated = await fs.readFile(result.outputPath, "utf8"); - - expect(generated).toContain( - '

@c15t/react: React Consent Components

' - ); - expect(generated).toContain( - "Built for @c15t/react: React Consent Components (%40c15t%2Freact)" - ); - } finally { - await fs.rm(rootDir, { force: true, recursive: true }); - } + const { configPath, rootDir } = await setupFixture(); + const result = await generateReadmeFromConfig({ + configPath, + dryRun: false, + }); + const generated = await readFile(result.outputPath, "utf8"); + + expect(generated).toContain( + '

@c15t/react: React Consent Components

' + ); + expect(generated).toContain( + "Built for @c15t/react: React Consent Components (%40c15t%2Freact)" + ); + await remove(rootDir); }); }); diff --git a/test/merge-config.test.ts b/test/merge-config.test.ts index f1a5af9..c04c629 100644 --- a/test/merge-config.test.ts +++ b/test/merge-config.test.ts @@ -1,5 +1,5 @@ -import { mergeConfigs } from "../src/config/load-config"; -import type { ReadieConfig, ReadieGlobalConfig } from "../src/config/types"; +import { mergeConfigs } from "#src/config/load-config.js"; +import type { ReadieConfig, ReadieGlobalConfig } from "#src/config/types.js"; const createProjectConfig = ( overrides: Partial = {} @@ -9,7 +9,7 @@ const createProjectConfig = ( ...overrides, }); -describe(mergeConfigs, () => { +describe("merge configs", () => { it("interpolates top-level global string placeholders", () => { const globalConfig: ReadieGlobalConfig = { banner: '

{{title}}

', @@ -29,7 +29,7 @@ describe(mergeConfigs, () => { "Built by My Package - @c15t/react - %40c15t%2Freact" ); expect(merged.features).toStrictEqual(["Feature A"]); - expect(merged.customSections?.Notes).toBe("Package: {{title}}"); + expect(merged.customSections?.Notes).toBe("Package: My Package"); }); it("preserves project-over-global precedence before interpolation", () => { diff --git a/test/resolve-invocation.test.ts b/test/resolve-invocation.test.ts index 1d2516f..a39cce7 100644 --- a/test/resolve-invocation.test.ts +++ b/test/resolve-invocation.test.ts @@ -1,6 +1,6 @@ -import { resolveInvocation } from "../src/cli/resolve-invocation"; +import { resolveInvocation } from "#src/cli/resolve-invocation.js"; -describe(resolveInvocation, () => { +describe("resolve invocation routing", () => { it("defaults to generate when no args are passed", () => { const resolved = resolveInvocation([]); expect(resolved.mode).toBe("generate"); diff --git a/test/template.test.ts b/test/template.test.ts index 3363bf9..b370e9f 100644 --- a/test/template.test.ts +++ b/test/template.test.ts @@ -1,6 +1,6 @@ -import { baseReadmeTemplate } from "../src/readme-generator/template"; +import { baseReadmeTemplate } from "#src/readme-generator/template.js"; -describe(baseReadmeTemplate, () => { +describe("base readme template", () => { it("renders neutral markdown without c15t defaults", () => { const markdown = baseReadmeTemplate({ description: "A neutral README.", @@ -12,11 +12,15 @@ describe(baseReadmeTemplate, () => { usage: ["Run the command", "```bash\nnpx readie\n```"], }); - expect(markdown).toContain("# Readie Demo"); - expect(markdown).toContain("## Table of Contents"); - expect(markdown).toContain("## Key Features"); - expect(markdown).toContain("## Security"); - expect(markdown).not.toContain("c15t"); - expect(markdown).not.toContain("consent.io"); + const requiredHeadings = [ + "# Readie Demo", + "## Table of Contents", + "## Key Features", + "## Security", + ]; + expect( + requiredHeadings.every((heading) => markdown.includes(heading)) + ).toBeTruthy(); + expect(markdown).not.toMatch(/c15t|consent\.io/); }); }); diff --git a/test/validate-config.test.ts b/test/validate-config.test.ts index aa109e3..0a5687d 100644 --- a/test/validate-config.test.ts +++ b/test/validate-config.test.ts @@ -1,17 +1,17 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; +import { writeFile } from "fs-extra"; +import { join } from "pathe"; +import { temporaryDirectory } from "tempy"; -import { loadReadieConfig } from "../src/config/load-config"; +import { loadReadieConfig } from "#src/config/load-config.js"; const createTempFile = async (contents: string) => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "readie-test-")); - const filePath = path.join(dir, "readie.json"); - await fs.writeFile(filePath, contents, "utf8"); + const dir = temporaryDirectory(); + const filePath = join(dir, "readie.json"); + await writeFile(filePath, contents, "utf8"); return filePath; }; -describe(loadReadieConfig, () => { +describe("load readie config", () => { it("loads a valid config", async () => { const configPath = await createTempFile( JSON.stringify({ diff --git a/tsconfig.json b/tsconfig.json index 7c22f14..8e55e1d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,34 @@ { "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices "strict": true, - "declaration": true, - "outDir": "dist", - "rootDir": "src", - "esModuleInterop": true, - "resolveJsonModule": true, - "skipLibCheck": true + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "paths": { + "#src/*": ["./src/*"] + }, + "types": ["vitest/globals"] }, "include": ["src/**/*.ts"] } From 766a844dd53ffdac58f5ea0a6478f7f97baba03b Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 21:20:52 +0000 Subject: [PATCH 05/12] Remove outdated TSDoc and JSDoc authoring documentation files - Deleted AGENTS.md, reference.md, SKILL.md, and index files for JSDoc and TSDoc authoring as they are no longer needed. - This cleanup helps streamline the documentation structure and removes redundant content. --- .../skills/tsdoc-jsdoc-authoring/AGENTS.md | 228 ------------- .agents/skills/tsdoc-jsdoc-authoring/SKILL.md | 123 ------- .../indexes/jsdoc-authoring-index.md | 30 -- .../indexes/jsdoc-consistency-index.md | 37 --- .../indexes/tsdoc-index.md | 39 --- .../skills/tsdoc-jsdoc-authoring/reference.md | 194 ----------- .../rules/jsdoc/advanced/jsdoc-async.md | 34 -- .../jsdoc/advanced/jsdoc-class-constructor.md | 34 -- .../advanced/jsdoc-implements-on-classes.md | 43 --- .../rules/jsdoc/advanced/jsdoc-module.md | 32 -- .../jsdoc/advanced/jsdoc-typedef-property.md | 37 --- .../rules/jsdoc/advanced/jsdoc-yields.md | 32 -- .../rules/jsdoc/core/jsdoc-check-access.md | 53 ---- .../rules/jsdoc/core/jsdoc-example.md | 34 -- .../rules/jsdoc/core/jsdoc-no-defaults.md | 46 --- .../jsdoc/core/jsdoc-optional-default.md | 34 -- .../rules/jsdoc/core/jsdoc-param.md | 33 -- .../jsdoc/core/jsdoc-property-namepaths.md | 34 -- .../jsdoc/core/jsdoc-require-param-type.md | 29 -- .../rules/jsdoc/core/jsdoc-require-param.md | 40 --- .../jsdoc-require-property-description.md | 33 -- .../jsdoc/core/jsdoc-require-property-name.md | 33 -- .../jsdoc/core/jsdoc-require-property-type.md | 33 -- .../jsdoc/core/jsdoc-require-property.md | 45 --- .../core/jsdoc-require-returns-description.md | 36 --- .../jsdoc/core/jsdoc-require-returns-type.md | 29 -- .../rules/jsdoc/core/jsdoc-require-returns.md | 53 ---- .../rules/jsdoc/core/jsdoc-require-yields.md | 50 --- .../rules/jsdoc/core/jsdoc-returns.md | 31 -- .../rules/jsdoc/core/jsdoc-summary.md | 31 -- .../rules/jsdoc/core/jsdoc-throws.md | 32 -- .../rules/tsdoc/core/tsdoc-deprecated.md | 31 -- .../rules/tsdoc/core/tsdoc-example.md | 35 -- .../rules/tsdoc/core/tsdoc-param.md | 71 ----- .../rules/tsdoc/core/tsdoc-remarks.md | 36 --- .../rules/tsdoc/core/tsdoc-returns.md | 31 -- .../rules/tsdoc/core/tsdoc-summary.md | 31 -- .../rules/tsdoc/core/tsdoc-throws.md | 32 -- .../rules/tsdoc/core/tsdoc-typeparam.md | 32 -- .../rules/tsdoc/crossref/tsdoc-inheritdoc.md | 39 --- .../rules/tsdoc/crossref/tsdoc-label.md | 33 -- .../rules/tsdoc/crossref/tsdoc-link.md | 31 -- .../rules/tsdoc/crossref/tsdoc-see.md | 33 -- .../rules/tsdoc/policy/tsdoc-default-value.md | 32 -- .../rules/tsdoc/policy/tsdoc-modifier-tags.md | 35 -- .../tsdoc/policy/tsdoc-no-jsdoc-braces.md | 33 -- .../policy/tsdoc-package-documentation.md | 32 -- .../tsdoc/policy/tsdoc-private-remarks.md | 34 -- .../rules/tsdoc/policy/tsdoc-release-tags.md | 36 --- .agents/skills/vitest/GENERATION.md | 5 - .agents/skills/vitest/SKILL.md | 53 ---- .../references/advanced-environments.md | 264 --------------- .../vitest/references/advanced-projects.md | 300 ------------------ .../references/advanced-type-testing.md | 242 -------------- .../skills/vitest/references/advanced-vi.md | 251 --------------- .agents/skills/vitest/references/core-cli.md | 167 ---------- .../skills/vitest/references/core-config.md | 177 ----------- .../skills/vitest/references/core-describe.md | 193 ----------- .../skills/vitest/references/core-expect.md | 211 ------------ .../skills/vitest/references/core-hooks.md | 245 -------------- .../skills/vitest/references/core-test-api.md | 237 -------------- .../vitest/references/features-concurrency.md | 250 --------------- .../vitest/references/features-context.md | 240 -------------- .../vitest/references/features-coverage.md | 202 ------------ .../vitest/references/features-filtering.md | 208 ------------ .../vitest/references/features-mocking.md | 272 ---------------- .../vitest/references/features-snapshots.md | 207 ------------ .claude/skills/tsdoc-jsdoc-authoring | 1 - .claude/skills/vitest | 1 - .cursor/hooks.json | 10 - .cursor/skills/tsdoc-jsdoc-authoring | 1 - .cursor/skills/vitest | 1 - 72 files changed, 5947 deletions(-) delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/AGENTS.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/SKILL.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-authoring-index.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-consistency-index.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/indexes/tsdoc-index.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/reference.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-async.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-class-constructor.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-implements-on-classes.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-module.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-typedef-property.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-yields.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-check-access.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-example.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-no-defaults.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-optional-default.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-param.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-property-namepaths.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param-type.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-description.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-name.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-type.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-description.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-type.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-yields.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-returns.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-summary.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-throws.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-deprecated.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-example.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-param.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-remarks.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-returns.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-summary.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-throws.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-typeparam.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-inheritdoc.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-label.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-link.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-see.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-default-value.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-modifier-tags.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-package-documentation.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-private-remarks.md delete mode 100644 .agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-release-tags.md delete mode 100644 .agents/skills/vitest/GENERATION.md delete mode 100644 .agents/skills/vitest/SKILL.md delete mode 100644 .agents/skills/vitest/references/advanced-environments.md delete mode 100644 .agents/skills/vitest/references/advanced-projects.md delete mode 100644 .agents/skills/vitest/references/advanced-type-testing.md delete mode 100644 .agents/skills/vitest/references/advanced-vi.md delete mode 100644 .agents/skills/vitest/references/core-cli.md delete mode 100644 .agents/skills/vitest/references/core-config.md delete mode 100644 .agents/skills/vitest/references/core-describe.md delete mode 100644 .agents/skills/vitest/references/core-expect.md delete mode 100644 .agents/skills/vitest/references/core-hooks.md delete mode 100644 .agents/skills/vitest/references/core-test-api.md delete mode 100644 .agents/skills/vitest/references/features-concurrency.md delete mode 100644 .agents/skills/vitest/references/features-context.md delete mode 100644 .agents/skills/vitest/references/features-coverage.md delete mode 100644 .agents/skills/vitest/references/features-filtering.md delete mode 100644 .agents/skills/vitest/references/features-mocking.md delete mode 100644 .agents/skills/vitest/references/features-snapshots.md delete mode 120000 .claude/skills/tsdoc-jsdoc-authoring delete mode 120000 .claude/skills/vitest delete mode 100644 .cursor/hooks.json delete mode 120000 .cursor/skills/tsdoc-jsdoc-authoring delete mode 120000 .cursor/skills/vitest diff --git a/.agents/skills/tsdoc-jsdoc-authoring/AGENTS.md b/.agents/skills/tsdoc-jsdoc-authoring/AGENTS.md deleted file mode 100644 index 117d153..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/AGENTS.md +++ /dev/null @@ -1,228 +0,0 @@ -# TSDoc and JSDoc Best Practices - -**Version 1.0.0** -Custom Authoring Guide -February 2026 - -> **Note:** -> This document is designed for agents and LLMs writing or reviewing TypeScript -> and JavaScript documentation comments using TSDoc and JSDoc. It mirrors a -> rule-driven format where each rule has clear "use" and "avoid" guidance with -> incorrect and correct examples. - ---- - -## Abstract - -Comprehensive TSDoc and JSDoc authoring guide for automated documentation generation and review. Contains focused rules across core contract tags, linking/reuse tags, package/internal docs, policy tags, and JavaScript-focused docstring patterns. Each rule explains when to use the tag, when not to use it, and includes concrete incorrect/correct examples. - -## Read Order (Context-Safe) - -Before using this catalog, route through one focused index: - -- `indexes/tsdoc-index.md` -- `indexes/jsdoc-authoring-index.md` -- `indexes/jsdoc-consistency-index.md` - -This file is a reference catalog. Load only the relevant subsection after choosing an index. - ---- - -## Rule Index - -### 1. Core API Contract Rules - -1.1 [Write Clear Summary Sentences](#11-write-clear-summary-sentences) -1.2 [Use `@remarks` for Long-Form Context](#12-use-remarks-for-long-form-context) -1.3 [Document Parameters with `@param`](#13-document-parameters-with-param) -1.4 [Document Generics with `@typeParam`](#14-document-generics-with-typeparam) -1.5 [Document Return Semantics with `@returns`](#15-document-return-semantics-with-returns) -1.6 [Document Expected Failures with `@throws`](#16-document-expected-failures-with-throws) -1.7 [Use `@example` for Non-Obvious Usage](#17-use-example-for-non-obvious-usage) -1.8 [Use `@deprecated` with Migration Guidance](#18-use-deprecated-with-migration-guidance) - -### 2. Cross-Reference and Reuse Rules - -2.1 [Prefer `{@link ...}` for Symbol References](#21-prefer-link--for-symbol-references) -2.2 [Use `@see` for Related APIs](#22-use-see-for-related-apis) -2.3 [Use `@inheritDoc` Only for Equivalent Contracts](#23-use-inheritdoc-only-for-equivalent-contracts) -2.4 [Use `{@label ...}` Only for Structured References](#24-use-label--only-for-structured-references) - -### 3. Package and Internal Notes - -3.1 [Use `@packageDocumentation` for Entrypoint Docs](#31-use-packagedocumentation-for-entrypoint-docs) -3.2 [Use `@privateRemarks` for Maintainer-Only Notes](#32-use-privateremarks-for-maintainer-only-notes) -3.3 [Document Defaults with `@defaultValue`](#33-document-defaults-with-defaultvalue) - -### 4. Stability and Modifier Policy - -4.1 [Apply Release Tags Consistently](#41-apply-release-tags-consistently) -4.2 [Use Modifier Tags Only When Semantically True](#42-use-modifier-tags-only-when-semantically-true) -4.3 [Do Not Mix JSDoc Type Syntax into TSDoc](#43-do-not-mix-jsdoc-type-syntax-into-tsdoc) - -### 5. JSDoc Rules (JavaScript) - -5.1 [Write JSDoc Summaries with Intent](#51-write-jsdoc-summaries-with-intent) -5.2 [Document Parameters with JSDoc Type Expressions](#52-document-parameters-with-jsdoc-type-expressions) -5.3 [Use Optional and Default Parameter Strings Correctly](#53-use-optional-and-default-parameter-strings-correctly) -5.4 [Use Nested Property Namepaths for Object Inputs](#54-use-nested-property-namepaths-for-object-inputs) -5.5 [Document Return Value Semantics with `@returns`](#55-document-return-value-semantics-with-returns) -5.6 [Document Error Contracts with `@throws`](#56-document-error-contracts-with-throws) -5.7 [Add Practical `@example` Snippets](#57-add-practical-example-snippets) -5.8 [Define Reusable Shapes with `@typedef` and `@property`](#58-define-reusable-shapes-with-typedef-and-property) -5.9 [Document Async Behavior with `@async`](#59-document-async-behavior-with-async) -5.10 [Document Generator Output with `@yields`](#510-document-generator-output-with-yields) -5.11 [Document Constructors and Classes Clearly](#511-document-constructors-and-classes-clearly) -5.12 [Use `@module` for Module-Level Docs](#512-use-module-for-module-level-docs) -5.13 [Validate Access Tags with `check-access`](#513-validate-access-tags-with-check-access) -5.14 [Restrict `@implements` to Classes or Constructors](#514-restrict-implements-to-classes-or-constructors) - ---- - -## 1. Core API Contract Rules - -### 1.1 Write Clear Summary Sentences - -See: `rules/tsdoc/core/tsdoc-summary.md` - -### 1.2 Use `@remarks` for Long-Form Context - -See: `rules/tsdoc/core/tsdoc-remarks.md` - -### 1.3 Document Parameters with `@param` - -See: `rules/tsdoc/core/tsdoc-param.md` - -### 1.4 Document Generics with `@typeParam` - -See: `rules/tsdoc/core/tsdoc-typeparam.md` - -### 1.5 Document Return Semantics with `@returns` - -See: `rules/tsdoc/core/tsdoc-returns.md` - -### 1.6 Document Expected Failures with `@throws` - -See: `rules/tsdoc/core/tsdoc-throws.md` - -### 1.7 Use `@example` for Non-Obvious Usage - -See: `rules/tsdoc/core/tsdoc-example.md` - -### 1.8 Use `@deprecated` with Migration Guidance - -See: `rules/tsdoc/core/tsdoc-deprecated.md` - -## 2. Cross-Reference and Reuse Rules - -### 2.1 Prefer `{@link ...}` for Symbol References - -See: `rules/tsdoc/crossref/tsdoc-link.md` - -### 2.2 Use `@see` for Related APIs - -See: `rules/tsdoc/crossref/tsdoc-see.md` - -### 2.3 Use `@inheritDoc` Only for Equivalent Contracts - -See: `rules/tsdoc/crossref/tsdoc-inheritdoc.md` - -### 2.4 Use `{@label ...}` Only for Structured References - -See: `rules/tsdoc/crossref/tsdoc-label.md` - -## 3. Package and Internal Notes - -### 3.1 Use `@packageDocumentation` for Entrypoint Docs - -See: `rules/tsdoc/policy/tsdoc-package-documentation.md` - -### 3.2 Use `@privateRemarks` for Maintainer-Only Notes - -See: `rules/tsdoc/policy/tsdoc-private-remarks.md` - -### 3.3 Document Defaults with `@defaultValue` - -See: `rules/tsdoc/policy/tsdoc-default-value.md` - -## 4. Stability and Modifier Policy - -### 4.1 Apply Release Tags Consistently - -See: `rules/tsdoc/policy/tsdoc-release-tags.md` - -### 4.2 Use Modifier Tags Only When Semantically True - -See: `rules/tsdoc/policy/tsdoc-modifier-tags.md` - -### 4.3 Do Not Mix JSDoc Type Syntax into TSDoc - -See: `rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md` - -## 5. JSDoc Rules (JavaScript) - -These rules are part of the default documentation standard and should be followed even without an active linter. - -### 5.1 Write JSDoc Summaries with Intent - -See: `rules/jsdoc/core/jsdoc-summary.md` - -### 5.2 Document Parameters with JSDoc Type Expressions - -See: `rules/jsdoc/core/jsdoc-param.md` - -### 5.3 Use Optional and Default Parameter Strings Correctly - -See: `rules/jsdoc/core/jsdoc-optional-default.md` - -### 5.4 Use Nested Property Namepaths for Object Inputs - -See: `rules/jsdoc/core/jsdoc-property-namepaths.md` - -### 5.5 Document Return Value Semantics with `@returns` - -See: `rules/jsdoc/core/jsdoc-returns.md` - -### 5.6 Document Error Contracts with `@throws` - -See: `rules/jsdoc/core/jsdoc-throws.md` - -### 5.7 Add Practical `@example` Snippets - -See: `rules/jsdoc/core/jsdoc-example.md` - -### 5.8 Define Reusable Shapes with `@typedef` and `@property` - -See: `rules/jsdoc/advanced/jsdoc-typedef-property.md` - -### 5.9 Document Async Behavior with `@async` - -See: `rules/jsdoc/advanced/jsdoc-async.md` - -### 5.10 Document Generator Output with `@yields` - -See: `rules/jsdoc/advanced/jsdoc-yields.md` - -### 5.11 Document Constructors and Classes Clearly - -See: `rules/jsdoc/advanced/jsdoc-class-constructor.md` - -### 5.12 Use `@module` for Module-Level Docs - -See: `rules/jsdoc/advanced/jsdoc-module.md` - -### 5.13 Validate Access Tags with `check-access` - -See: `rules/jsdoc/core/jsdoc-check-access.md` - -### 5.14 Restrict `@implements` to Classes or Constructors - -See: `rules/jsdoc/advanced/jsdoc-implements-on-classes.md` - ---- - -## References - -1. [https://tsdoc.org](https://tsdoc.org) -2. [https://github.com/microsoft/tsdoc](https://github.com/microsoft/tsdoc) -3. [https://jsdoc.app](https://jsdoc.app) diff --git a/.agents/skills/tsdoc-jsdoc-authoring/SKILL.md b/.agents/skills/tsdoc-jsdoc-authoring/SKILL.md deleted file mode 100644 index c4e6ec1..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/SKILL.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -name: tsdoc-jsdoc-authoring -description: Write and review API documentation comments using TSDoc and JSDoc best practices. Use when the user asks for docs, doc comments, TSDoc, JSDoc, @param/@returns help, or documentation quality improvements in JavaScript or TypeScript code. -metadata: - author: custom - version: "1.0.0" - argument-hint: ---- - -# TSDoc and JSDoc Authoring - -Create high-quality documentation comments for functions, classes, interfaces, types, modules, and exported APIs. - -## When to Apply - -Use this skill when the user asks to: - -- add or improve code comments/docs -- write TSDoc or JSDoc -- document params, return values, thrown errors, or examples -- standardize documentation style across files - -## Routing (Read This First) - -Choose one focused index before loading rule files: - -- TypeScript APIs and typed exports: `indexes/tsdoc-index.md` -- JavaScript authoring and shape docs: `indexes/jsdoc-authoring-index.md` -- Strict consistency/lint-safe output standards: `indexes/jsdoc-consistency-index.md` - -## Rule 1: Choose the Correct Standard - -- Use **TSDoc** for TypeScript code and typed public APIs. -- Use **JSDoc** for JavaScript code or projects using JSDoc tooling. -- Do not mix tag syntaxes from both standards in one comment block. - -## Workflow - -1. Read the target file and identify the symbol's purpose and side effects. -2. Prefer documenting exported/public APIs first. -3. Write a one-line summary in plain language. -4. Add only meaningful tags (no empty or redundant tags). -5. Validate tag names and formatting against the relevant standard. -6. Ensure comments match actual behavior (inputs, outputs, errors, async behavior). - -## Authoring Rules (Both Standards) - -- Describe intent and behavior, not obvious implementation details. -- Keep summaries concise and action-oriented ("Returns...", "Creates...", "Parses..."). -- Document every parameter and explain what each parameter does. -- If a parameter is an object, document each object property and what it does. -- Use imperative, consistent phrasing for `@param` descriptions. -- Document thrown errors with `@throws` when behavior depends on error handling. -- Add `@example` only when it clarifies non-obvious usage. -- Do not restate TypeScript types in prose unless it adds semantic meaning. - -## TSDoc Rules - -Use these tags by default when relevant: - -- `@param` for each function argument -- `@typeParam` for generic parameters -- `@returns` for return semantics -- `@remarks` for longer contextual details -- `@throws` for exceptional behavior -- `@example` for practical usage -- `@deprecated` for migration guidance - -TSDoc formatting reminders: - -- Use `@param name - Description` with a hyphen separator. -- Prefer inline links with `{@link SymbolName}`. -- Keep release tags (`@alpha`, `@beta`, `@public`, `@internal`) aligned with project policy. -- For object parameters in TypeScript, prefer a named `type` or `interface` and add `/** ... */` comments on each property so VS Code hovers/autocomplete show per-property docs. - -## JSDoc Rules - -Use these tags by default when relevant: - -- `@param {Type} name` (with optional/default forms when needed) -- `@returns {Type}` (or `@return`) -- `@throws {Type}` when known -- `@typedef` and `@property` for reusable object shapes -- `@example` for usage snippets -- `@async` and `@yields` for async/generator behavior when needed - -JSDoc formatting reminders: - -- Optional params: `@param {string} [name]` -- Optional with default: `@param {string} [name=John Doe]` -- Nested object props: `@param {Object} options` and `@param {string} options.mode` - -## JSDoc Consistency Standard - -Apply these rules by default in all JavaScript documentation, regardless of lint setup. - -- Keep `@access` usage consistent per block (`@access ` or one shorthand access tag). -- Use `@implements` only on classes/constructors. -- Prefer rule-specific guidance from the `rules/jsdoc/**` files when there is a conflict. - -## Output Expectations - -When updating comments: - -- keep existing project conventions unless user requests a migration -- preserve behavior accuracy over verbosity -- avoid adding comments to private/internal symbols unless requested - -## Additional Reference - -For templates and tag cheat sheets, see [reference.md](reference.md). - -## Rule Files - -Use focused indexes to avoid loading unrelated rules: - -- TSDoc index: `indexes/tsdoc-index.md` -- JSDoc authoring index: `indexes/jsdoc-authoring-index.md` -- JSDoc consistency index: `indexes/jsdoc-consistency-index.md` - -## Full Compiled Document - -For the full rule index and category guide: `AGENTS.md` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-authoring-index.md b/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-authoring-index.md deleted file mode 100644 index 10cba59..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-authoring-index.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: JSDoc Authoring Index -scope: JavaScript documentation patterns ---- - -# JSDoc Authoring Index - -Use this index for normal JavaScript doc authoring tasks. For strict consistency rules, use `indexes/jsdoc-consistency-index.md`. - -## Core Authoring - -- `rules/jsdoc/core/jsdoc-summary.md` -- `rules/jsdoc/core/jsdoc-param.md` -- `rules/jsdoc/core/jsdoc-optional-default.md` -- `rules/jsdoc/core/jsdoc-property-namepaths.md` -- `rules/jsdoc/core/jsdoc-returns.md` -- `rules/jsdoc/core/jsdoc-throws.md` -- `rules/jsdoc/core/jsdoc-example.md` - -## Advanced Authoring - -- `rules/jsdoc/advanced/jsdoc-typedef-property.md` -- `rules/jsdoc/advanced/jsdoc-async.md` -- `rules/jsdoc/advanced/jsdoc-yields.md` -- `rules/jsdoc/advanced/jsdoc-class-constructor.md` -- `rules/jsdoc/advanced/jsdoc-module.md` - -## Deep Reference - -- `reference.md` (templates and syntax cheatsheets) diff --git a/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-consistency-index.md b/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-consistency-index.md deleted file mode 100644 index 0064048..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/indexes/jsdoc-consistency-index.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: JSDoc Consistency Index -scope: Lint-safe and standard-safe JSDoc output ---- - -# JSDoc Consistency Index - -Use this index when requests need strict, consistent, lint-safe JSDoc output. These rules are part of the default standard. - -## Access and Class Semantics - -- `rules/jsdoc/core/jsdoc-check-access.md` -- `rules/jsdoc/advanced/jsdoc-implements-on-classes.md` - -## Parameter Requirements - -- `rules/jsdoc/core/jsdoc-no-defaults.md` -- `rules/jsdoc/core/jsdoc-require-param.md` -- `rules/jsdoc/core/jsdoc-require-param-type.md` - -## Property Requirements - -- `rules/jsdoc/core/jsdoc-require-property.md` -- `rules/jsdoc/core/jsdoc-require-property-name.md` -- `rules/jsdoc/core/jsdoc-require-property-type.md` -- `rules/jsdoc/core/jsdoc-require-property-description.md` - -## Return and Yield Requirements - -- `rules/jsdoc/core/jsdoc-require-returns.md` -- `rules/jsdoc/core/jsdoc-require-returns-type.md` -- `rules/jsdoc/core/jsdoc-require-returns-description.md` -- `rules/jsdoc/core/jsdoc-require-yields.md` - -## Deep Reference - -- `reference.md` (consistency checklist and templates) diff --git a/.agents/skills/tsdoc-jsdoc-authoring/indexes/tsdoc-index.md b/.agents/skills/tsdoc-jsdoc-authoring/indexes/tsdoc-index.md deleted file mode 100644 index 044e6cc..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/indexes/tsdoc-index.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: TSDoc Index -scope: TypeScript API docs ---- - -# TSDoc Index - -Use this index for TypeScript symbols and typed public APIs. Load only the sections you need. - -## Core Contract - -- `rules/tsdoc/core/tsdoc-summary.md` -- `rules/tsdoc/core/tsdoc-remarks.md` -- `rules/tsdoc/core/tsdoc-param.md` -- `rules/tsdoc/core/tsdoc-typeparam.md` -- `rules/tsdoc/core/tsdoc-returns.md` -- `rules/tsdoc/core/tsdoc-throws.md` -- `rules/tsdoc/core/tsdoc-example.md` -- `rules/tsdoc/core/tsdoc-deprecated.md` - -## Cross-Reference and Reuse - -- `rules/tsdoc/crossref/tsdoc-link.md` -- `rules/tsdoc/crossref/tsdoc-see.md` -- `rules/tsdoc/crossref/tsdoc-inheritdoc.md` -- `rules/tsdoc/crossref/tsdoc-label.md` - -## Package and Policy - -- `rules/tsdoc/policy/tsdoc-package-documentation.md` -- `rules/tsdoc/policy/tsdoc-private-remarks.md` -- `rules/tsdoc/policy/tsdoc-default-value.md` -- `rules/tsdoc/policy/tsdoc-release-tags.md` -- `rules/tsdoc/policy/tsdoc-modifier-tags.md` -- `rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md` - -## Deep Reference - -- `reference.md` (templates and review checklist) diff --git a/.agents/skills/tsdoc-jsdoc-authoring/reference.md b/.agents/skills/tsdoc-jsdoc-authoring/reference.md deleted file mode 100644 index a27f54d..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/reference.md +++ /dev/null @@ -1,194 +0,0 @@ -# TSDoc Deep Reference - -This file is the detailed TSDoc authoring spec for this skill. Use it when generating or reviewing TypeScript API documentation comments. - -## 1) TSDoc Comment Structure - -Standard order for a high-quality TSDoc block: - -1. Summary sentence(s) -2. Optional `@remarks` (long-form details) -3. Parameter docs (`@typeParam`, then `@param`) -4. Return docs (`@returns`) when applicable -5. Error docs (`@throws`) when behavior depends on error handling -6. Optional `@example` -7. Optional policy tags (`@deprecated`, release tags, modifiers) -8. Optional `@see` - -Template: - -````ts -/** - * One-sentence summary in plain language. - * - * @remarks - * Long-form context, constraints, caveats, and behavior notes. - * - * @typeParam T - Meaning of generic type. - * @param input - What this value represents and constraints. - * @returns What is returned and key semantics. - * @throws ErrorType when and why this fails. - * @example - * ```ts - * const value = fn(input) - * ``` - * @deprecated Use `newFn()` instead. - */ -```` - -## 2) TSDoc Tag Kinds (Syntax Types) - -TSDoc tags are grouped into three syntax kinds: - -- `InlineTag`: used inside prose (example: `{@link Foo}`) -- `BlockTag`: starts a new section block (example: `@remarks`) -- `ModifierTag`: marker-style tags with no body (example: `@public`) - -Use the right syntax kind for the right purpose. Do not write modifier tags as prose blocks. - -## 3) Routing to Focused Rule Sets - -Use focused indexes instead of loading full rule catalogs into context: - -- TSDoc work: `indexes/tsdoc-index.md` -- JSDoc authoring work: `indexes/jsdoc-authoring-index.md` -- JSDoc consistency and lint-safe output: `indexes/jsdoc-consistency-index.md` - -Load only the minimum rule files needed for the current edit/review task. - -## 4) High-Quality TSDoc Patterns - -### Document async behavior precisely - -```ts -/** - * Fetches the current profile. - * @returns The active user profile. - * @throws Error when the auth token is invalid. - */ -export async function getProfile(): Promise {} -``` - -### Document generics meaningfully - -```ts -/** - * Creates a dictionary from items. - * @typeParam T - Source item type. - * @param items - Source collection. - * @param getKey - Derives a stable key from each item. - * @returns A map from key to item. - */ -export function toMap( - items: T[], - getKey: (item: T) => string -): Map {} -``` - -### Document thrown contract only - -```ts -/** - * Parses a signed payload. - * @param token - Signed token string. - * @returns Decoded payload. - * @throws Error when signature validation fails. - */ -export function parseSignedToken(token: string): Payload {} -``` - -## 5) TSDoc Mistakes To Avoid - -- Using JSDoc type braces in TSDoc `@param` lines. -- Missing `@typeParam` for public generic APIs. -- Writing verbose summaries that hide key behavior. -- Adding `@example` blocks that are stale or too long. -- Documenting implementation trivia instead of API contract. -- Tagging release level (`@alpha`, `@beta`, etc.) inconsistently across related APIs. - -## 6) TSDoc Review Checklist - -Use this checklist when reviewing generated comments: - -- Summary accurately states what the API does. -- Every parameter has a meaningful `@param`. -- All generics have `@typeParam` when public. -- `@returns` describes semantics, not just type. -- `@throws` documents expected caller-relevant failures. -- `@remarks` is used only when needed. -- Links use `{@link ...}` where appropriate. -- Release/visibility/modifier tags match project policy. -- Comment behavior matches implementation exactly. - -## 7) JSDoc Quick Appendix (Cross-Standard Projects) - -Use this only for JavaScript files or JSDoc-based tooling. - -### JSDoc function template - -```js -/** - * Normalizes a user profile for rendering. - * @param {Object} input - Raw user profile. - * @param {string} input.id - Stable identifier. - * @param {string} [input.displayName] - Optional display name. - * @returns {Object} Normalized profile object. - * @throws {Error} If required fields are missing. - */ -``` - -### JSDoc typedef template - -```js -/** - * @typedef {Object} UserProfile - * @property {string} id - Stable user identifier. - * @property {string} [displayName] - Optional display name. - * @property {boolean} active - Whether the user is active. - */ -``` - -### JSDoc String Syntax Cheatsheet - -Use these exact JSDoc "strings" for common cases: - -- Required param: `@param {string} name - Description` -- Optional param: `@param {string} [name] - Description` -- Optional with default: `@param {string} [name=John Doe] - Description` -- Object param: `@param {Object} options - Description` -- Nested object field: `@param {string} options.mode - Description` -- Array object field: `@param {string} items[].id - Description` -- Return type: `@returns {Promise} Description` -- Throws type: `@throws {Error} Description` -- Access style (pick one): `@access private` or `@private` (do not mix on one doc block) -- `@implements` placement: use only on classes/constructors (not regular functions) - -### TypeScript Object Param Hover Pattern (Recommended) - -For best VS Code hover and autocomplete docs on object properties, use a named type/interface and `/** ... */` on each property: - -```ts -type SearchOptions = { - /** Query string used to match results. */ - query: string; - /** Maximum number of results to return. */ - limit?: number; -}; - -/** - * @param options - Search configuration. - */ -export function search(options: SearchOptions) {} -``` - -## 8) JSDoc Consistency Checklist - -Use this quick pass before finalizing JSDoc blocks in any project: - -- Access tags: use one access style per block; valid `@access` values are `package|private|protected|public`. -- `@implements` placement: use it only on class constructors or constructor-style functions. - -## 9) Sources Used For This Skill - -- TSDoc: `/microsoft/tsdoc` (Context7) -- JSDoc: `/jsdoc/jsdoc.github.io` (Context7) diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-async.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-async.md deleted file mode 100644 index f4679c7..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-async.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Document Async Behavior with @async -impact: MEDIUM -impactDescription: clarifies asynchronous control flow in generated docs -tags: jsdoc, async, promises, functions ---- - -## Document Async Behavior with `@async` - -Use `@async` where tooling or virtual comments need explicit async annotation. - -**Use this when:** async behavior is not obvious from implementation context. -**Avoid this when:** async is already clear and the project style avoids redundant tags. - -**Incorrect (no async contract in virtual docs):** - -```js -/** - * Downloads data from a URL. - * @param {string} url - Source URL. - * @returns {Promise} Downloaded content. - */ -``` - -**Correct (explicit async behavior):** - -```js -/** - * Downloads data from a URL. - * @async - * @param {string} url - Source URL. - * @returns {Promise} Downloaded content. - */ -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-class-constructor.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-class-constructor.md deleted file mode 100644 index 4f3a1c4..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-class-constructor.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Document Constructors and Classes Clearly -impact: HIGH -impactDescription: improves class API onboarding and usage clarity -tags: jsdoc, class, constructor, oop ---- - -## Document Constructors and Classes Clearly - -Document constructor arguments and class purpose clearly. - -**Use this when:** documenting class-based JavaScript APIs. -**Avoid this when:** constructor parameters are undocumented. - -**Incorrect (class intent unclear):** - -```js -/** - * @constructor - */ -function Book(title, author) {} -``` - -**Correct (constructor contract documented):** - -```js -/** - * Represents a book in the catalog. - * @constructor - * @param {string} title - Book title. - * @param {string} author - Book author. - */ -function Book(title, author) {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-implements-on-classes.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-implements-on-classes.md deleted file mode 100644 index 59b91a3..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-implements-on-classes.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Restrict @implements to Classes or Constructors -impact: HIGH -impactDescription: prevents invalid interface implementation annotations on non-constructors -tags: jsdoc, implements, classes, constructors, lint ---- - -## Restrict `@implements` to Classes or Constructors - -Use `@implements` only on class constructors or constructor-style functions. - -**Use this when:** documenting classes, constructors, or constructor functions. -**Avoid this when:** annotating regular functions, callbacks, or `@function` docs. - -**Incorrect (non-constructor function):** - -```js -/** - * @implements {SomeClass} - */ -function quux() {} -``` - -**Correct (class constructor):** - -```js -class Foo { - /** - * @implements {SomeClass} - */ - constructor() {} -} -``` - -**Correct (constructor-style function):** - -```js -/** - * @implements {SomeClass} - * @class - */ -function quux() {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-module.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-module.md deleted file mode 100644 index 504275f..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-module.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Use @module for Module-Level Docs -impact: MEDIUM -impactDescription: clarifies module boundaries and exported responsibilities -tags: jsdoc, module, entrypoint, organization ---- - -## Use `@module` for Module-Level Docs - -Use `@module` at file/module scope for package organization and generated docs. - -**Use this when:** documenting JavaScript module entrypoints or grouped exports. -**Avoid this when:** annotating individual functions with module-level semantics. - -**Incorrect (module tag on member doc):** - -```js -/** - * @module auth - * Issues access tokens. - */ -function issueToken(userId) {} -``` - -**Correct (module-level declaration):** - -```js -/** - * Authentication helpers. - * @module auth - */ -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-typedef-property.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-typedef-property.md deleted file mode 100644 index 22699e1..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-typedef-property.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Define Reusable Shapes with @typedef and @property -impact: HIGH -impactDescription: centralizes object contracts and improves consistency -tags: jsdoc, typedef, property, object-shapes ---- - -## Define Reusable Shapes with `@typedef` and `@property` - -Use `@typedef` and `@property` for repeated object shapes. - -**Use this when:** multiple APIs share the same object structure. -**Avoid this when:** duplicating long inline object type expressions everywhere. - -**Incorrect (duplicated inline shape):** - -```js -/** - * @param {{ id: string, active: boolean }} user - User object. - */ -function saveUser(user) {} -``` - -**Correct (reusable typedef):** - -```js -/** - * @typedef {Object} UserRecord - * @property {string} id - Stable user identifier. - * @property {boolean} active - Whether the user is active. - */ - -/** - * @param {UserRecord} user - User object. - */ -function saveUser(user) {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-yields.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-yields.md deleted file mode 100644 index fd3ab87..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/advanced/jsdoc-yields.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Document Generator Output with @yields -impact: MEDIUM -impactDescription: clarifies yielded value contracts for iterators -tags: jsdoc, yields, generators, iterators ---- - -## Document Generator Output with `@yields` - -Use `@yields {Type}` to describe generator output values. - -**Use this when:** documenting generator functions. -**Avoid this when:** function is not a generator. - -**Incorrect (missing yielded type):** - -```js -/** - * Generates Fibonacci numbers. - */ -function* fibonacci() {} -``` - -**Correct (explicit yielded contract):** - -```js -/** - * Generates Fibonacci numbers. - * @yields {number} The next number in the sequence. - */ -function* fibonacci() {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-check-access.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-check-access.md deleted file mode 100644 index 5f2d6fe..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-check-access.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Validate Access Tags with check-access -impact: HIGH -impactDescription: prevents ambiguous visibility semantics in JSDoc comments -tags: jsdoc, access, lint, visibility ---- - -## Validate Access Tags with `check-access` - -When documenting visibility in JSDoc, use exactly one access style per doc block: - -- `@access package|private|protected|public`, or -- one shorthand tag: `@package`, `@private`, `@protected`, or `@public` - -**Use this when:** documenting API/member visibility in JavaScript files. -**Avoid this when:** mixing access styles or using unsupported access values. - -**Incorrect (mixed access styles):** - -```js -/** - * @access private - * @public - */ -function normalizeUser(input) {} -``` - -**Incorrect (invalid access value):** - -```js -/** - * @access internal-only - */ -function normalizeUser(input) {} -``` - -**Correct (single valid access tag):** - -```js -/** - * @access private - */ -function normalizeUser(input) {} -``` - -**Correct (single shorthand access tag):** - -```js -/** - * @private - */ -function normalizeUser(input) {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-example.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-example.md deleted file mode 100644 index 10297b9..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-example.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Add Practical @example Snippets -impact: MEDIUM -impactDescription: reduces misuse by showing expected call patterns -tags: jsdoc, example, usage, snippets ---- - -## Add Practical `@example` Snippets - -Use `@example` for behavior that is not obvious from signature alone. - -**Use this when:** API behavior or output format may be misunderstood. -**Avoid this when:** examples are trivial, stale, or verbose. - -**Incorrect (no concrete usage):** - -```js -/** - * Formats a currency value. - */ -function formatCurrency(amount, currency) {} -``` - -**Correct (practical usage):** - -```js -/** - * Formats a currency value. - * @example - * // "$12.50" - * formatCurrency(12.5, "USD") - */ -function formatCurrency(amount, currency) {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-no-defaults.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-no-defaults.md deleted file mode 100644 index 8821a23..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-no-defaults.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Avoid Defaults in JSDoc Param Tags -impact: MEDIUM -impactDescription: keeps parameter documentation aligned with modern JavaScript defaults -tags: jsdoc, param, defaults, lint ---- - -## Avoid Defaults in JSDoc Param Tags - -Do not include default values inside `@param` or `@default` tag syntax when documenting function parameters. - -**Use this when:** documenting JavaScript function parameters where defaults/optionality are represented in code. -**Avoid this when:** writing bracketed defaults like `[name="value"]` in JSDoc. - -This prevents redundant default notation in docs where ES2015+ default parameters already express runtime behavior. - -**Incorrect (default value in `@param`):** - -```js -/** - * @param {number} [foo="7"] - */ -function quux(foo) {} -``` - -**Correct (required param notation):** - -```js -/** - * @param {number} foo - */ -function quux(foo) {} -``` - -**Correct (untyped required param notation):** - -```js -/** - * @param foo - */ -function quux(foo) {} -``` - -Configuration option: - -- `noOptionalParamNames` (`boolean`, default `false`): when `true`, also report square-bracket optional names on `@param` tags. diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-optional-default.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-optional-default.md deleted file mode 100644 index 9c8eaea..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-optional-default.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Use Optional and Default Parameter Strings Correctly -impact: CRITICAL -impactDescription: prevents caller confusion around optional inputs -tags: jsdoc, param, optional, defaults, syntax ---- - -## Use Optional and Default Parameter Strings Correctly - -Use JSDoc optional/default syntax strings consistently: - -- Optional: `@param {string} [name]` -- Optional with default: `@param {string} [name=John Doe]` - -**Use this when:** documenting optional JavaScript parameters. -**Avoid this when:** optionality is described only in prose and not in tag syntax. - -**Incorrect (optionality hidden):** - -```js -/** - * @param {string} name - Optional display name. - */ -function greet(name) {} -``` - -**Correct (explicit optional/default string syntax):** - -```js -/** - * @param {string} [name=John Doe] - Optional display name. - */ -function greet(name) {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-param.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-param.md deleted file mode 100644 index d1cf937..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-param.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Document Parameters with JSDoc Type Expressions -impact: CRITICAL -impactDescription: defines input contract for JavaScript APIs -tags: jsdoc, param, types, contracts ---- - -## Document Parameters with JSDoc Type Expressions - -Use `@param {Type} name - Description` for each parameter. - -**Use this when:** documenting JavaScript API parameters. -**Avoid this when:** type expressions are missing or inconsistent. - -Each parameter comment must explain what that specific parameter does. - -**Incorrect (missing type expression):** - -```js -/** - * @param timeoutMs - Wait time. - */ -function waitForReady(timeoutMs) {} -``` - -**Correct (typed parameter contract):** - -```js -/** - * @param {number} timeoutMs - Maximum wait time in milliseconds. - */ -function waitForReady(timeoutMs) {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-property-namepaths.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-property-namepaths.md deleted file mode 100644 index d880947..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-property-namepaths.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Use Nested Property Namepaths for Object Inputs -impact: HIGH -impactDescription: makes object parameter contracts explicit -tags: jsdoc, param, property, namepath, objects ---- - -## Use Nested Property Namepaths for Object Inputs - -When a parameter is an object, document its fields using namepaths. -Each object property needs its own comment that explains what it does. - -**Use this when:** function expects structured objects or arrays of objects. -**Avoid this when:** only top-level object parameter is documented. - -**Incorrect (missing nested fields):** - -```js -/** - * @param {Object} options - Query options. - */ -function search(options) {} -``` - -**Correct (nested property paths):** - -```js -/** - * @param {Object} options - Query options. - * @param {string} options.query - Search query string. - * @param {number} [options.limit=20] - Maximum number of results. - */ -function search(options) {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param-type.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param-type.md deleted file mode 100644 index d1c3db2..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param-type.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Require Types in @param Tags -impact: HIGH -impactDescription: ensures parameter contracts include explicit JSDoc type information -tags: jsdoc, param, types, lint ---- - -## Require Types in `@param` Tags - -Ensure each JSDoc `@param` tag includes a type expression in curly braces. - -**Use this when:** documenting JavaScript function parameters with JSDoc. -**Avoid this when:** writing untyped `@param` tags like `@param foo`. - -The parameter type should be documented so callers and tooling can understand expected input shapes. - -**Incorrect (missing type in `@param`):** - -```js -/** @param foo */ -function quux(foo) {} -``` - -**Correct (typed `@param`):** - -```js -/** @param {SomeType} foo */ -function quux(foo) {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param.md deleted file mode 100644 index 9c77524..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-param.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Require @param for Function Parameters -impact: HIGH -impactDescription: ensures complete input contracts for documented functions -tags: jsdoc, param, completeness, lint ---- - -## Require `@param` for Function Parameters - -Document all function parameters with JSDoc `@param` tags. - -**Use this when:** writing JSDoc for functions, methods, and callable APIs. -**Avoid this when:** some parameters are undocumented in the JSDoc block. - -This improves code quality and maintainability by making function inputs explicit. - -**Incorrect (missing a parameter tag):** - -```js -/** @param foo */ -function quux(foo, bar) {} -``` - -**Correct (all parameters documented):** - -```js -/** @param foo */ -function quux(foo) {} -``` - -Configuration options: - -- `checkConstructors` (`boolean`, default `false`): whether to check constructor methods. -- `checkDestructured` (`boolean`, default `true`): whether to check destructured parameters. -- `checkDestructuredRoots` (`boolean`, default `true`): whether to require a root `@param` tag for root destructured parameters like `function f({a, b}) {}`. -- `checkGetters` (`boolean`, default `true`): whether to check getter methods. -- `checkRestProperty` (`boolean`, default `false`): whether to check rest properties. -- `checkSetters` (`boolean`, default `true`): whether to check setter methods. -- `checkTypesPattern` (`string`, default `"^(?:[oO]bject|[aA]rray|PlainObject|Generic(?:Object|Array))$"`): regex pattern for types exempted from checking. -- `exemptedBy` (`string[]`, default `["inheritdoc"]`): JSDoc tags that exempt functions from `@param` checking. diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-description.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-description.md deleted file mode 100644 index aa6d664..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-description.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Require Descriptions for @property Tags -impact: HIGH -impactDescription: improves object-shape clarity by documenting property intent -tags: jsdoc, property, descriptions, lint ---- - -## Require Descriptions for `@property` Tags - -Ensure all `@property` tags include a description. - -**Use this when:** documenting typedef or namespace properties. -**Avoid this when:** leaving property tags without explanatory text. - -Property descriptions should be documented so consumers understand each field's purpose. - -**Incorrect (missing property description):** - -```js -/** - * @typedef {SomeType} SomeTypedef - * @property {number} foo - */ -``` - -**Correct (property description present):** - -```js -/** - * @typedef {SomeType} SomeTypedef - * @property {number} foo Foo. - */ -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-name.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-name.md deleted file mode 100644 index 22be0bd..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-name.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Require Names in @property Tags -impact: HIGH -impactDescription: ensures each documented property is identifiable by name -tags: jsdoc, property, names, lint ---- - -## Require Names in `@property` Tags - -Ensure all `@property` tags include a property name. - -**Use this when:** documenting typedef or namespace object properties. -**Avoid this when:** writing `@property` tags without an identifier. - -Property names should be documented so object fields can be referenced unambiguously. - -**Incorrect (missing property name):** - -```js -/** - * @typedef {SomeType} SomeTypedef - * @property {number} - */ -``` - -**Correct (property name present):** - -```js -/** - * @typedef {SomeType} SomeTypedef - * @property {number} foo - */ -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-type.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-type.md deleted file mode 100644 index 1a70bd0..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property-type.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Require Types in @property Tags -impact: HIGH -impactDescription: ensures property contracts include explicit type information -tags: jsdoc, property, types, lint ---- - -## Require Types in `@property` Tags - -Ensure each `@property` tag includes a type expression in curly braces. - -**Use this when:** documenting typedef, namespace, or class properties with JSDoc. -**Avoid this when:** writing untyped `@property` tags like `@property foo`. - -Property types should be documented so consumers and tooling can infer expected data shapes. - -**Incorrect (missing property type):** - -```js -/** - * @typedef {SomeType} SomeTypedef - * @property foo - */ -``` - -**Correct (typed property):** - -```js -/** - * @typedef {SomeType} SomeTypedef - * @property {number} foo - */ -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property.md deleted file mode 100644 index 40e5d72..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-property.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Require @property for Object Typedefs and Namespaces -impact: HIGH -impactDescription: prevents incomplete object-shape documentation for typedefs and namespaces -tags: jsdoc, typedef, namespace, property, lint ---- - -## Require `@property` for Object Typedefs and Namespaces - -When using `@typedef` or `@namespace` with plain object types, include one or more `@property` tags. - -**Use this when:** documenting object-based typedefs or namespaces. -**Avoid this when:** leaving object typedefs/namespaces without property definitions. - -Object shapes should have properties defined so consumers can understand the contract. - -**Incorrect (missing `@property`):** - -```js -/** - * @typedef {Object} SomeTypedef - */ - -/** - * @namespace {Object} SomeNamespace - */ -``` - -**Correct (object typedef with properties):** - -```js -/** - * @typedef {Object} SomeTypedef - * @property {SomeType} propName Prop description - */ -``` - -**Correct (object typedef with shorthand property):** - -```js -/** - * @typedef {object} Foo - * @property someProp - */ -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-description.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-description.md deleted file mode 100644 index 02b23cb..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-description.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Require Descriptions in @returns Tags -impact: HIGH -impactDescription: clarifies return-value semantics beyond raw type information -tags: jsdoc, returns, descriptions, lint ---- - -## Require Descriptions in `@returns` Tags - -Ensure `@returns` tags include a description value. - -**Use this when:** documenting return behavior for functions and methods. -**Avoid this when:** leaving `@returns` empty or type-only without meaning. - -A `@returns` tag should explain what is returned, not just indicate that a return exists. - -The error is not reported when the return type is: - -- `void` -- `undefined` -- `Promise` -- `Promise` - -**Incorrect (missing return description):** - -```js -/** @returns */ -function quux(foo) {} -``` - -**Correct (return description present):** - -```js -/** @returns Foo. */ -function quux(foo) {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-type.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-type.md deleted file mode 100644 index 5e9bcd2..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns-type.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Require Types in @returns Tags -impact: HIGH -impactDescription: ensures return contracts include explicit type information -tags: jsdoc, returns, types, lint ---- - -## Require Types in `@returns` Tags - -Ensure each `@returns` tag includes a type expression in curly braces. - -**Use this when:** documenting return values in JavaScript JSDoc blocks. -**Avoid this when:** writing untyped `@returns` tags like `@returns`. - -A `@returns` tag should include a type value to clearly define return shape expectations. - -**Incorrect (missing return type):** - -```js -/** @returns */ -function quux(foo) {} -``` - -**Correct (typed return):** - -```js -/** @returns {string} */ -function quux(foo) {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns.md deleted file mode 100644 index 142b397..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-returns.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Require @returns for Return Statements -impact: HIGH -impactDescription: prevents missing or ambiguous return-value documentation -tags: jsdoc, returns, completeness, lint ---- - -## Require `@returns` for Return Statements - -Document return statements with `@returns` and avoid multiple `@returns` tags in a single doc block. - -**Use this when:** documenting functions or methods that return values. -**Avoid this when:** omitting `@returns` for returning functions or adding duplicate `@returns` tags. - -This prevents missing return contracts and inconsistent return documentation. - -**Incorrect (missing `@returns`):** - -```js -/** Foo. */ -function quux() { - return foo; -} -``` - -**Incorrect (duplicate `@returns` tags):** - -```js -/** - * @returns Foo! - * @returns Foo? - */ -function quux() { - return foo; -} -``` - -**Correct (single `@returns`):** - -```js -/** @returns Foo. */ -function quux() { - return foo; -} -``` - -Configuration options: - -- `checkConstructors` (`boolean`, default `false`): whether to check constructor methods. -- `checkGetters` (`boolean`, default `true`): whether to check getter methods. -- `exemptedBy` (`string[]`, default `["inheritdoc"]`): tags that exempt functions from requiring `@returns`. -- `forceRequireReturn` (`boolean`, default `false`): whether to require `@returns` even if no value is returned. -- `forceReturnsWithAsync` (`boolean`, default `false`): whether to require `@returns` on async functions. diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-yields.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-yields.md deleted file mode 100644 index effc597..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-require-yields.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Require @yields for Generator Output -impact: HIGH -impactDescription: ensures generator output contracts are documented and unambiguous -tags: jsdoc, yields, generators, lint ---- - -## Require `@yields` for Generator Output - -Document generator yields with `@yields` and avoid multiple `@yields` tags in a single doc block. - -**Use this when:** documenting generator functions that yield values. -**Avoid this when:** omitting `@yields` on yielding generators or adding duplicate `@yields` tags. - -This prevents missing yield contracts and inconsistent generator documentation. - -**Incorrect (missing `@yields`):** - -```js -function* quux(foo) { - yield foo; -} -``` - -**Incorrect (duplicate `@yields` tags):** - -```js -/** - * @yields {undefined} - * @yields {void} - */ -function* quux(foo) {} -``` - -**Correct (single `@yields`):** - -```js -/** - * @yields Foo - */ -function* quux(foo) { - yield foo; -} -``` - -Configuration options: - -- `exemptedBy` (`string[]`, default `["inheritdoc"]`): functions with these tags are exempt. -- `forceRequireYields` (`boolean`, default `false`): require `@yields` on all generators, even empty/non-yielding ones. -- `withGeneratorTag` (`boolean`, default `false`): require `@yields` when a `@generator` tag is present. diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-returns.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-returns.md deleted file mode 100644 index 44f55aa..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-returns.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Document Return Value Semantics with @returns -impact: HIGH -impactDescription: clarifies output behavior and expected shape -tags: jsdoc, returns, output, contracts ---- - -## Document Return Value Semantics with @returns - -Use `@returns {Type}` with semantic description. - -**Use this when:** functions return a value or Promise payload. -**Avoid this when:** only stating raw type without meaning. - -**Incorrect (type-only):** - -```js -/** - * @returns {Object} - */ -function parseConfig() {} -``` - -**Correct (semantics included):** - -```js -/** - * @returns {{ retries: number, timeoutMs: number }} Normalized runtime configuration. - */ -function parseConfig() {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-summary.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-summary.md deleted file mode 100644 index 740866d..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-summary.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Write JSDoc Summaries with Intent -impact: HIGH -impactDescription: improves API readability for JavaScript consumers -tags: jsdoc, summary, readability ---- - -## Write JSDoc Summaries with Intent - -Write concise summaries that explain behavior and purpose. - -**Use this when:** documenting JavaScript functions, classes, and modules. -**Avoid this when:** repeating symbol names without useful meaning. - -**Incorrect (name restatement):** - -```js -/** - * Gets data. - */ -function getData() {} -``` - -**Correct (behavior-focused):** - -```js -/** - * Fetches and parses JSON data from the configured endpoint. - */ -function getData() {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-throws.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-throws.md deleted file mode 100644 index fa932dd..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/jsdoc/core/jsdoc-throws.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Document Error Contracts with @throws -impact: HIGH -impactDescription: enables predictable caller error handling -tags: jsdoc, throws, errors, reliability ---- - -## Document Error Contracts with @throws - -Use `@throws {Type} ...` for expected, caller-relevant failures. - -**Use this when:** function may throw known errors callers should handle. -**Avoid this when:** listing incidental internal errors with no contract value. - -**Incorrect (no throws contract):** - -```js -/** - * Parses a signed token. - */ -function parseSignedToken(token) {} -``` - -**Correct (typed throws contract):** - -```js -/** - * Parses a signed token. - * @throws {Error} If the token signature is invalid. - */ -function parseSignedToken(token) {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-deprecated.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-deprecated.md deleted file mode 100644 index 9bd7587..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-deprecated.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Use @deprecated with Migration Guidance -impact: HIGH -impactDescription: prevents dead-end API usage and migration confusion -tags: tsdoc, deprecated, migration, lifecycle ---- - -## Use @deprecated with Migration Guidance - -Use `@deprecated` only with explicit replacement guidance. - -**Use this when:** a symbol should no longer be used. -**Avoid this when:** deprecation is unclear or no replacement exists. - -**Incorrect (no migration path):** - -```ts -/** - * @deprecated - */ -export function oldHash(input: string): string {} -``` - -**Correct (clear migration path):** - -```ts -/** - * @deprecated Use `hashSha256()` for stable cross-platform output. - */ -export function oldHash(input: string): string {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-example.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-example.md deleted file mode 100644 index 2639e8a..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-example.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Use @example for Non-Obvious Usage -impact: MEDIUM -impactDescription: reduces misuse and clarifies edge cases -tags: tsdoc, example, usage, snippets ---- - -## Use @example for Non-Obvious Usage - -Use `@example` for tricky, non-obvious, or high-value usage patterns. - -**Use this when:** behavior is subtle or edge-case driven. -**Avoid this when:** examples are trivial, stale, or excessively long. - -**Incorrect (no usage guidance for tricky API):** - -```ts -/** - * Retries an operation with backoff. - */ -export async function retry(fn: () => Promise): Promise {} -``` - -**Correct (clear example):** - -````ts -/** - * Retries an operation with backoff. - * @example - * ```ts - * const result = await retry(() => fetchJson("/api/data")) - * ``` - */ -export async function retry(fn: () => Promise): Promise {} -```` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-param.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-param.md deleted file mode 100644 index ffc754c..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-param.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: Document Parameters with @param -impact: CRITICAL -impactDescription: clarifies caller obligations and input semantics -tags: tsdoc, param, function-contract, api-docs ---- - -## Document Parameters with @param - -Use one `@param` line per parameter: `@param name - Description`. - -**Use this when:** documenting any function or method parameters. -**Avoid this when:** repeating type annotations already in the signature. - -Each parameter comment must explain what that specific parameter does. -If a parameter is an object, document each relevant property and what each property does. -For TypeScript object params, prefer a named object type/interface with `/** ... */` comments on each property. - -**Incorrect (missing semantics):** - -```ts -/** - * @param timeoutMs - number - */ -export function waitForReady(timeoutMs: number): Promise {} -``` - -**Correct (caller-relevant semantics):** - -```ts -/** - * @param timeoutMs - Maximum wait time in milliseconds before timing out. - */ -export function waitForReady(timeoutMs: number): Promise {} -``` - -**Incorrect (object parameter properties undocumented):** - -```ts -/** - * @param options - Search configuration. - */ -export function search(options: { query: string; limit?: number }): Result[] {} -``` - -**Correct (object parameter properties documented):** - -```ts -/** - * @param options - Search configuration. - * @param options.query - Query string used to match results. - * @param options.limit - Maximum number of results to return. - */ -export function search(options: { query: string; limit?: number }): Result[] {} -``` - -**Preferred (VS Code hover/autocomplete friendly pattern):** - -```ts -type SearchOptions = { - /** Query string used to match results. */ - query: string; - /** Maximum number of results to return. */ - limit?: number; -}; - -/** - * @param options - Search configuration. - */ -export function search(options: SearchOptions): Result[] {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-remarks.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-remarks.md deleted file mode 100644 index 7be061f..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-remarks.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Use @remarks for Long-Form Context -impact: HIGH -impactDescription: prevents overloaded summaries and preserves detail -tags: tsdoc, remarks, context, contracts ---- - -## Use @remarks for Long-Form Context - -Use `@remarks` for deeper context, caveats, and non-trivial behavior. - -**Use this when:** summary alone cannot express constraints or trade-offs. -**Avoid this when:** a one-line summary is sufficient. - -**Incorrect (long narrative in summary):** - -```ts -/** - * Generates cache keys and also normalizes locale and strips unsupported - * fields and retries once if key generation collides. - */ -export function buildCacheKey(input: Input): string {} -``` - -**Correct (summary + remarks):** - -```ts -/** - * Builds a deterministic cache key for request inputs. - * - * @remarks - * Normalizes locale casing, removes unsupported fields, and retries once on - * hash collisions to preserve key uniqueness guarantees. - */ -export function buildCacheKey(input: Input): string {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-returns.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-returns.md deleted file mode 100644 index a9d2660..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-returns.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Document Return Semantics with @returns -impact: HIGH -impactDescription: clarifies output guarantees and expectations -tags: tsdoc, returns, output, contracts ---- - -## Document Return Semantics with @returns - -Use `@returns` to describe return meaning, guarantees, and ordering. - -**Use this when:** function returns non-void values (including Promise payloads). -**Avoid this when:** output is `void` and no return contract exists. - -**Incorrect (type-only restatement):** - -```ts -/** - * @returns A string. - */ -export function getCacheKey(): string {} -``` - -**Correct (semantic output contract):** - -```ts -/** - * @returns A deterministic cache key that is stable for equivalent inputs. - */ -export function getCacheKey(): string {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-summary.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-summary.md deleted file mode 100644 index 2f3b66b..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-summary.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Write Clear Summary Sentences -impact: HIGH -impactDescription: improves API discoverability and comprehension -tags: tsdoc, summary, readability, api-docs ---- - -## Write Clear Summary Sentences - -Write a short summary that explains behavior and intent in plain language. - -**Use this when:** every documented symbol. -**Avoid this when:** copying symbol names or implementation trivia. - -**Incorrect (name restatement, no meaning):** - -```ts -/** - * Gets user. - */ -export function getUser(id: string): Promise {} -``` - -**Correct (behavior and outcome):** - -```ts -/** - * Fetches a user by ID from the primary data source. - */ -export function getUser(id: string): Promise {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-throws.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-throws.md deleted file mode 100644 index 4b1f360..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-throws.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Document Expected Failures with @throws -impact: HIGH -impactDescription: defines caller-facing error-handling contracts -tags: tsdoc, throws, errors, reliability ---- - -## Document Expected Failures with @throws - -Use `@throws` for expected, contract-relevant failures. - -**Use this when:** callers should handle known failure modes. -**Avoid this when:** documenting incidental low-level errors with no API contract value. - -**Incorrect (no throw contract):** - -```ts -/** - * Parses a signed token. - */ -export function parseToken(token: string): Payload {} -``` - -**Correct (caller-relevant error path):** - -```ts -/** - * Parses a signed token. - * @throws Error if token signature verification fails. - */ -export function parseToken(token: string): Payload {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-typeparam.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-typeparam.md deleted file mode 100644 index 88fdb3b..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/core/tsdoc-typeparam.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Document Generics with @typeParam -impact: CRITICAL -impactDescription: preserves generic API intent for consumers -tags: tsdoc, typeparam, generics, api-contract ---- - -## Document Generics with @typeParam - -Use `@typeParam` for each public generic parameter. - -**Use this when:** function/class/interface/type alias has generic type params. -**Avoid this when:** symbol has no generic parameters. - -**Incorrect (undocumented generic intent):** - -```ts -/** - * Builds an index from items. - */ -export function toIndex(items: T[]): Map {} -``` - -**Correct (clear generic role):** - -```ts -/** - * Builds an index from items. - * @typeParam T - Source item type stored in the resulting map. - */ -export function toIndex(items: T[]): Map {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-inheritdoc.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-inheritdoc.md deleted file mode 100644 index f3c4d59..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-inheritdoc.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Use @inheritDoc Only for Equivalent Contracts -impact: HIGH -impactDescription: prevents inherited docs from drifting from real behavior -tags: tsdoc, inheritdoc, inheritance, contracts ---- - -## Use @inheritDoc Only for Equivalent Contracts - -Use `@inheritDoc` only when behavior is truly equivalent to the referenced declaration. - -**Use this when:** override/wrapper keeps the same contract. -**Avoid this when:** behavior, defaults, errors, or side effects differ. - -**Incorrect (inherits despite changed behavior):** - -```ts -/** - * @inheritDoc BaseClient.fetch - */ -export class CachedClient extends BaseClient { - public override fetch(id: string): Promise {} -} -``` - -**Correct (document differences explicitly):** - -```ts -/** - * Fetches an item by ID, serving stale-while-revalidate responses from cache. - * - * @remarks - * Unlike `BaseClient.fetch`, this method may return cached data immediately - * and refresh the cache asynchronously. - */ -export class CachedClient extends BaseClient { - public override fetch(id: string): Promise {} -} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-label.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-label.md deleted file mode 100644 index dd6c85d..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-label.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Use {@label ...} Only for Structured References -impact: LOW -impactDescription: avoids unnecessary complexity in standard docs -tags: tsdoc, label, inline-tags, references ---- - -## Use `{@label ...}` Only for Structured References - -Use `{@label ...}` sparingly for advanced reference labeling scenarios. - -**Use this when:** your doc tooling relies on explicit labels for cross-reference flows. -**Avoid this when:** ordinary symbol linking via `{@link ...}` is sufficient. - -**Incorrect (using labels as normal links):** - -```ts -/** - * {@label parseToken} - * Parses a signed token. - */ -export function parseToken(token: string): Payload {} -``` - -**Correct (use link for normal references):** - -```ts -/** - * Parses a signed token. - * See {@link verifyToken} for signature checks. - */ -export function parseToken(token: string): Payload {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-link.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-link.md deleted file mode 100644 index 96a10b6..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-link.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Prefer {@link ...} for Symbol References -impact: MEDIUM -impactDescription: improves navigation and reference accuracy -tags: tsdoc, link, cross-reference, discoverability ---- - -## Prefer `{@link ...}` for Symbol References - -Use inline `{@link ...}` when referring to symbols or canonical URLs. - -**Use this when:** references should be navigable and unambiguous. -**Avoid this when:** plain text names could be confused or drift over time. - -**Incorrect (non-linking text reference):** - -```ts -/** - * Works with TokenVerifier.verify for signature validation. - */ -export function parseSignedToken(token: string): Payload {} -``` - -**Correct (navigable reference):** - -```ts -/** - * Works with {@link TokenVerifier.verify} for signature validation. - */ -export function parseSignedToken(token: string): Payload {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-see.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-see.md deleted file mode 100644 index 739971b..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/crossref/tsdoc-see.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Use @see for Related APIs -impact: MEDIUM -impactDescription: connects related APIs and reduces duplicated docs -tags: tsdoc, see, references, api-navigation ---- - -## Use @see for Related APIs - -Use `@see` to point readers to related entry points, alternatives, or companion APIs. - -**Use this when:** relationship is important for correct API selection. -**Avoid this when:** references are irrelevant or redundant. - -**Incorrect (no relationship guidance):** - -```ts -/** - * Verifies token signatures. - */ -export function verifyToken(token: string): boolean {} -``` - -**Correct (points to related APIs):** - -```ts -/** - * Verifies token signatures. - * @see {@link decodeToken} - * @see {@link parseSignedToken} - */ -export function verifyToken(token: string): boolean {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-default-value.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-default-value.md deleted file mode 100644 index 0a4cd32..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-default-value.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Document Defaults with @defaultValue -impact: MEDIUM -impactDescription: prevents ambiguity in option and property behavior -tags: tsdoc, defaultValue, options, configuration ---- - -## Document Defaults with `@defaultValue` - -Use `@defaultValue` to document meaningful default behavior. - -**Use this when:** options/properties have user-visible defaults. -**Avoid this when:** defaults are unstable, implicit, or undocumented in implementation. - -**Incorrect (default hidden from docs):** - -```ts -/** - * Maximum retry attempts. - */ -export const DEFAULT_RETRY_ATTEMPTS = 3; -``` - -**Correct (explicit default):** - -```ts -/** - * Maximum retry attempts. - * @defaultValue 3 - */ -export const DEFAULT_RETRY_ATTEMPTS = 3; -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-modifier-tags.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-modifier-tags.md deleted file mode 100644 index b628b71..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-modifier-tags.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Use Modifier Tags Only When Semantically True -impact: HIGH -impactDescription: prevents misleading API semantics in generated docs -tags: tsdoc, modifier-tags, readonly, override, virtual, sealed, decorator, eventProperty ---- - -## Use Modifier Tags Only When Semantically True - -Use modifier tags only when they match real API semantics. - -Common modifier tags: `@readonly`, `@override`, `@virtual`, `@sealed`, `@decorator`, `@eventProperty`. - -**Use this when:** code behavior and design contract actually match the tag. -**Avoid this when:** tags are decorative or used for emphasis. - -**Incorrect (tag conflicts with implementation):** - -```ts -/** - * Tracks current request count. - * @readonly - */ -export let requestCount = 0; -``` - -**Correct (tag matches behavior):** - -```ts -/** - * Current build version. - * @readonly - */ -export const BUILD_VERSION = "1.0.0"; -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md deleted file mode 100644 index 82cc164..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-no-jsdoc-braces.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Do Not Mix JSDoc Type Syntax into TSDoc -impact: CRITICAL -impactDescription: avoids parser/tooling incompatibilities and inconsistent style -tags: tsdoc, jsdoc, syntax, consistency ---- - -## Do Not Mix JSDoc Type Syntax into TSDoc - -In TSDoc, avoid JSDoc-style type braces in tag lines. - -**Use this when:** writing TSDoc in TypeScript codebases. -**Avoid this when:** authoring JavaScript files that intentionally use JSDoc tooling. - -**Incorrect (JSDoc-style braces in TSDoc):** - -```ts -/** - * @param {string} id - User identifier. - * @returns {Promise} User record. - */ -export async function getUser(id: string): Promise {} -``` - -**Correct (TSDoc syntax):** - -```ts -/** - * @param id - User identifier. - * @returns User record. - */ -export async function getUser(id: string): Promise {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-package-documentation.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-package-documentation.md deleted file mode 100644 index d56c2f6..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-package-documentation.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Use @packageDocumentation for Entrypoint Docs -impact: MEDIUM -impactDescription: improves package-level discoverability and onboarding -tags: tsdoc, packageDocumentation, modules, entrypoints ---- - -## Use `@packageDocumentation` for Entrypoint Docs - -Use `@packageDocumentation` on module/entrypoint docs, not regular members. - -**Use this when:** documenting package/module purpose and usage at top level. -**Avoid this when:** documenting individual functions, classes, or fields. - -**Incorrect (tag on a normal function):** - -```ts -/** - * @packageDocumentation - * Creates a token. - */ -export function createToken(userId: string): string {} -``` - -**Correct (tag on entrypoint-level docs):** - -```ts -/** - * @packageDocumentation - * Authentication helpers for issuing, parsing, and validating signed tokens. - */ -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-private-remarks.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-private-remarks.md deleted file mode 100644 index ab914a4..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-private-remarks.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Use @privateRemarks for Maintainer-Only Notes -impact: MEDIUM -impactDescription: separates internal guidance from public API docs -tags: tsdoc, privateRemarks, maintainers, internals ---- - -## Use `@privateRemarks` for Maintainer-Only Notes - -Use `@privateRemarks` for internal notes that should not appear in public docs. - -**Use this when:** maintainers need migration or implementation caveats. -**Avoid this when:** information is essential for API consumers. - -**Incorrect (internal rollout note in public remarks):** - -```ts -/** - * @remarks - * Keep legacy payload shape until mobile v4 rollout finishes. - */ -export function serializeUser(user: User): Payload {} -``` - -**Correct (internal detail isolated):** - -```ts -/** - * Serializes user data for API responses. - * @privateRemarks - * Keep legacy payload shape until mobile v4 rollout finishes. - */ -export function serializeUser(user: User): Payload {} -``` diff --git a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-release-tags.md b/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-release-tags.md deleted file mode 100644 index 6b3bfaf..0000000 --- a/.agents/skills/tsdoc-jsdoc-authoring/rules/tsdoc/policy/tsdoc-release-tags.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Apply Release Tags Consistently -impact: CRITICAL -impactDescription: ensures stable public API lifecycle communication -tags: tsdoc, release-tags, public, internal, alpha, beta, experimental ---- - -## Apply Release Tags Consistently - -Apply one clear release/visibility policy and keep it consistent across related APIs. - -Supported policy tags include `@public`, `@internal`, `@alpha`, `@beta`, and `@experimental`. - -**Use this when:** API lifecycle and visibility matter to consumers. -**Avoid this when:** tags are mixed arbitrarily or conflict with actual support policy. - -**Incorrect (conflicting policy):** - -```ts -/** - * Creates session tokens. - * @public - * @internal - */ -export function createSessionToken(userId: string): string {} -``` - -**Correct (single clear policy):** - -```ts -/** - * Creates session tokens. - * @beta - */ -export function createSessionToken(userId: string): string {} -``` diff --git a/.agents/skills/vitest/GENERATION.md b/.agents/skills/vitest/GENERATION.md deleted file mode 100644 index 9bc7664..0000000 --- a/.agents/skills/vitest/GENERATION.md +++ /dev/null @@ -1,5 +0,0 @@ -# Generation Info - -- **Source:** `sources/vitest` -- **Git SHA:** `4a7321e10672f00f0bb698823a381c2cc245b8f7` -- **Generated:** 2026-01-28 diff --git a/.agents/skills/vitest/SKILL.md b/.agents/skills/vitest/SKILL.md deleted file mode 100644 index d813e2c..0000000 --- a/.agents/skills/vitest/SKILL.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: vitest -description: Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures. -metadata: - author: Anthony Fu - version: "2026.1.28" - source: Generated from https://github.com/vitest-dev/vitest, scripts located at https://github.com/antfu/skills ---- - -Vitest is a next-generation testing framework powered by Vite. It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box. Vitest shares the same config, transformers, resolvers, and plugins with your Vite app. - -**Key Features:** - -- Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates -- Jest-compatible: Drop-in replacement for most Jest test suites -- Smart watch mode: Only reruns affected tests based on module graph -- Native ESM, TypeScript, JSX support without configuration -- Multi-threaded workers for parallel test execution -- Built-in coverage via V8 or Istanbul -- Snapshot testing, mocking, and spy utilities - -> The skill is based on Vitest 3.x, generated at 2026-01-28. - -## Core - -| Topic | Description | Reference | -| ------------- | --------------------------------------------------------------- | -------------------------------------------- | -| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) | -| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) | -| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) | -| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) | -| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) | -| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) | - -## Features - -| Topic | Description | Reference | -| ------------ | -------------------------------------------------------------- | ---------------------------------------------------------- | -| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) | -| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) | -| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) | -| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) | -| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) | -| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) | - -## Advanced - -| Topic | Description | Reference | -| ------------ | ------------------------------------------------------- | ------------------------------------------------------------ | -| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) | -| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) | -| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) | -| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) | diff --git a/.agents/skills/vitest/references/advanced-environments.md b/.agents/skills/vitest/references/advanced-environments.md deleted file mode 100644 index 69c60ca..0000000 --- a/.agents/skills/vitest/references/advanced-environments.md +++ /dev/null @@ -1,264 +0,0 @@ ---- -name: test-environments -description: Configure environments like jsdom, happy-dom for browser APIs ---- - -# Test Environments - -## Available Environments - -- `node` (default) - Node.js environment -- `jsdom` - Browser-like with DOM APIs -- `happy-dom` - Faster alternative to jsdom -- `edge-runtime` - Vercel Edge Runtime - -## Configuration - -```ts -// vitest.config.ts -defineConfig({ - test: { - environment: "jsdom", - - // Environment-specific options - environmentOptions: { - jsdom: { - url: "http://localhost", - }, - }, - }, -}); -``` - -## Installing Environment Packages - -```bash -# jsdom -npm i -D jsdom - -# happy-dom (faster, fewer APIs) -npm i -D happy-dom -``` - -## Per-File Environment - -Use magic comment at top of file: - -```ts -// @vitest-environment jsdom - -import { expect, test } from "vitest"; - -test("DOM test", () => { - const div = document.createElement("div"); - expect(div).toBeInstanceOf(HTMLDivElement); -}); -``` - -## jsdom Environment - -Full browser environment simulation: - -```ts -// @vitest-environment jsdom - -test("DOM manipulation", () => { - document.body.innerHTML = '
'; - - const app = document.getElementById("app"); - app.textContent = "Hello"; - - expect(app.textContent).toBe("Hello"); -}); - -test("window APIs", () => { - expect(window.location.href).toBeDefined(); - expect(localStorage).toBeDefined(); -}); -``` - -### jsdom Options - -```ts -defineConfig({ - test: { - environmentOptions: { - jsdom: { - url: "http://localhost:3000", - html: "", - userAgent: "custom-agent", - resources: "usable", - }, - }, - }, -}); -``` - -## happy-dom Environment - -Faster but fewer APIs: - -```ts -// @vitest-environment happy-dom - -test("basic DOM", () => { - const el = document.createElement("div"); - el.className = "test"; - expect(el.className).toBe("test"); -}); -``` - -## Multiple Environments per Project - -Use projects for different environments: - -```ts -defineConfig({ - test: { - projects: [ - { - test: { - name: "unit", - include: ["tests/unit/**/*.test.ts"], - environment: "node", - }, - }, - { - test: { - name: "dom", - include: ["tests/dom/**/*.test.ts"], - environment: "jsdom", - }, - }, - ], - }, -}); -``` - -## Custom Environment - -Create custom environment package: - -```ts -// vitest-environment-custom/index.ts -import type { Environment } from "vitest/runtime"; - -export default { - name: "custom", - viteEnvironment: "ssr", // or 'client' - - setup() { - // Setup global state - globalThis.myGlobal = "value"; - - return { - teardown() { - delete globalThis.myGlobal; - }, - }; - }, -}; -``` - -Use with: - -```ts -defineConfig({ - test: { - environment: "custom", - }, -}); -``` - -## Environment with VM - -For full isolation: - -```ts -export default { - name: "isolated", - viteEnvironment: "ssr", - - async setupVM() { - const vm = await import("node:vm"); - const context = vm.createContext(); - - return { - getVmContext() { - return context; - }, - teardown() {}, - }; - }, - - setup() { - return { teardown() {} }; - }, -}; -``` - -## Browser Mode (Separate from Environments) - -For real browser testing, use Vitest Browser Mode: - -```ts -defineConfig({ - test: { - browser: { - enabled: true, - name: "chromium", // or 'firefox', 'webkit' - provider: "playwright", - }, - }, -}); -``` - -## CSS and Assets - -In jsdom/happy-dom, configure CSS handling: - -```ts -defineConfig({ - test: { - css: true, // Process CSS - - // Or with options - css: { - include: /\.module\.css$/, - modules: { - classNameStrategy: "non-scoped", - }, - }, - }, -}); -``` - -## Fixing External Dependencies - -If external deps fail with CSS/asset errors: - -```ts -defineConfig({ - test: { - server: { - deps: { - inline: ["problematic-package"], - }, - }, - }, -}); -``` - -## Key Points - -- Default is `node` - no browser APIs -- Use `jsdom` for full browser simulation -- Use `happy-dom` for faster tests with basic DOM -- Per-file environment via `// @vitest-environment` comment -- Use projects for multiple environment configurations -- Browser Mode is for real browser testing, not environment - - diff --git a/.agents/skills/vitest/references/advanced-projects.md b/.agents/skills/vitest/references/advanced-projects.md deleted file mode 100644 index 2b4d37f..0000000 --- a/.agents/skills/vitest/references/advanced-projects.md +++ /dev/null @@ -1,300 +0,0 @@ ---- -name: projects-workspaces -description: Multi-project configuration for monorepos and different test types ---- - -# Projects - -Run different test configurations in the same Vitest process. - -## Basic Projects Setup - -```ts -// vitest.config.ts -defineConfig({ - test: { - projects: [ - // Glob patterns for config files - "packages/*", - - // Inline config - { - test: { - name: "unit", - include: ["tests/unit/**/*.test.ts"], - environment: "node", - }, - }, - { - test: { - name: "integration", - include: ["tests/integration/**/*.test.ts"], - environment: "jsdom", - }, - }, - ], - }, -}); -``` - -## Monorepo Pattern - -```ts -defineConfig({ - test: { - projects: [ - // Each package has its own vitest.config.ts - "packages/core", - "packages/cli", - "packages/utils", - ], - }, -}); -``` - -Package config: - -```ts -// packages/core/vitest.config.ts -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - name: "core", - include: ["src/**/*.test.ts"], - environment: "node", - }, -}); -``` - -## Different Environments - -Run same tests in different environments: - -```ts -defineConfig({ - test: { - projects: [ - { - test: { - name: "happy-dom", - root: "./shared-tests", - environment: "happy-dom", - setupFiles: ["./setup.happy-dom.ts"], - }, - }, - { - test: { - name: "node", - root: "./shared-tests", - environment: "node", - setupFiles: ["./setup.node.ts"], - }, - }, - ], - }, -}); -``` - -## Browser + Node Projects - -```ts -defineConfig({ - test: { - projects: [ - { - test: { - name: "unit", - include: ["tests/unit/**/*.test.ts"], - environment: "node", - }, - }, - { - test: { - name: "browser", - include: ["tests/browser/**/*.test.ts"], - browser: { - enabled: true, - name: "chromium", - provider: "playwright", - }, - }, - }, - ], - }, -}); -``` - -## Shared Configuration - -```ts -// vitest.shared.ts -export const sharedConfig = { - testTimeout: 10000, - setupFiles: ["./tests/setup.ts"], -}; - -// vitest.config.ts -import { sharedConfig } from "./vitest.shared"; - -defineConfig({ - test: { - projects: [ - { - test: { - ...sharedConfig, - name: "unit", - include: ["tests/unit/**/*.test.ts"], - }, - }, - { - test: { - ...sharedConfig, - name: "e2e", - include: ["tests/e2e/**/*.test.ts"], - }, - }, - ], - }, -}); -``` - -## Project-Specific Dependencies - -Each project can have different dependencies inlined: - -```ts -defineConfig({ - test: { - projects: [ - { - test: { - name: "project-a", - server: { - deps: { - inline: ["package-a"], - }, - }, - }, - }, - ], - }, -}); -``` - -## Running Specific Projects - -```bash -# Run specific project -vitest --project unit -vitest --project integration - -# Multiple projects -vitest --project unit --project e2e - -# Exclude project -vitest --project.ignore browser -``` - -## Providing Values to Projects - -Share values from config to tests: - -```ts -// vitest.config.ts -defineConfig({ - test: { - projects: [ - { - test: { - name: "staging", - provide: { - apiUrl: "https://staging.api.com", - debug: true, - }, - }, - }, - { - test: { - name: "production", - provide: { - apiUrl: "https://api.com", - debug: false, - }, - }, - }, - ], - }, -}); - -// In tests, use inject -import { inject } from "vitest"; - -test("uses correct api", () => { - const url = inject("apiUrl"); - expect(url).toContain("api.com"); -}); -``` - -## With Fixtures - -```ts -const test = base.extend({ - apiUrl: ["/default", { injected: true }], -}); - -test("uses injected url", ({ apiUrl }) => { - // apiUrl comes from project's provide config -}); -``` - -## Project Isolation - -Each project runs in its own thread pool by default: - -```ts -defineConfig({ - test: { - projects: [ - { - test: { - name: "isolated", - isolate: true, // Full isolation - pool: "forks", - }, - }, - ], - }, -}); -``` - -## Global Setup per Project - -```ts -defineConfig({ - test: { - projects: [ - { - test: { - name: "with-db", - globalSetup: ["./tests/db-setup.ts"], - }, - }, - ], - }, -}); -``` - -## Key Points - -- Projects run in same Vitest process -- Each project can have different environment, config -- Use glob patterns for monorepo packages -- Run specific projects with `--project` flag -- Use `provide` to inject config values into tests -- Projects inherit from root config unless overridden - - diff --git a/.agents/skills/vitest/references/advanced-type-testing.md b/.agents/skills/vitest/references/advanced-type-testing.md deleted file mode 100644 index 52dbd67..0000000 --- a/.agents/skills/vitest/references/advanced-type-testing.md +++ /dev/null @@ -1,242 +0,0 @@ ---- -name: type-testing -description: Test TypeScript types with expectTypeOf and assertType ---- - -# Type Testing - -Test TypeScript types without runtime execution. - -## Setup - -Type tests use `.test-d.ts` extension: - -```ts -// math.test-d.ts -import { expectTypeOf } from "vitest"; -import { add } from "./math"; - -test("add returns number", () => { - expectTypeOf(add).returns.toBeNumber(); -}); -``` - -## Configuration - -```ts -defineConfig({ - test: { - typecheck: { - enabled: true, - - // Only type check - only: false, - - // Checker: 'tsc' or 'vue-tsc' - checker: "tsc", - - // Include patterns - include: ["**/*.test-d.ts"], - - // tsconfig to use - tsconfig: "./tsconfig.json", - }, - }, -}); -``` - -## expectTypeOf API - -```ts -import { expectTypeOf } from "vitest"; - -// Basic type checks -expectTypeOf().toBeString(); -expectTypeOf().toBeNumber(); -expectTypeOf().toBeBoolean(); -expectTypeOf().toBeNull(); -expectTypeOf().toBeUndefined(); -expectTypeOf().toBeVoid(); -expectTypeOf().toBeNever(); -expectTypeOf().toBeAny(); -expectTypeOf().toBeUnknown(); -expectTypeOf().toBeObject(); -expectTypeOf().toBeFunction(); -expectTypeOf<[]>().toBeArray(); -expectTypeOf().toBeSymbol(); -``` - -## Value Type Checking - -```ts -const value = "hello"; -expectTypeOf(value).toBeString(); - -const obj = { name: "test", count: 42 }; -expectTypeOf(obj).toMatchTypeOf<{ name: string }>(); -expectTypeOf(obj).toHaveProperty("name"); -``` - -## Function Types - -```ts -function greet(name: string): string { - return `Hello, ${name}`; -} - -expectTypeOf(greet).toBeFunction(); -expectTypeOf(greet).parameters.toEqualTypeOf<[string]>(); -expectTypeOf(greet).returns.toBeString(); - -// Parameter checking -expectTypeOf(greet).parameter(0).toBeString(); -``` - -## Object Types - -```ts -interface User { - id: number; - name: string; - email?: string; -} - -expectTypeOf().toHaveProperty("id"); -expectTypeOf().toHaveProperty("name").toBeString(); - -// Check shape -expectTypeOf({ id: 1, name: "test" }).toMatchTypeOf(); -``` - -## Equality vs Matching - -```ts -interface A { - x: number; -} -interface B { - x: number; - y: string; -} - -// toMatchTypeOf - subset matching -expectTypeOf().toMatchTypeOf(); // B extends A - -// toEqualTypeOf - exact match -expectTypeOf().not.toEqualTypeOf(); // Not exact match -expectTypeOf().toEqualTypeOf<{ x: number }>(); // Exact match -``` - -## Branded Types - -```ts -type UserId = number & { __brand: "UserId" }; -type PostId = number & { __brand: "PostId" }; - -expectTypeOf().not.toEqualTypeOf(); -expectTypeOf().not.toEqualTypeOf(); -``` - -## Generic Types - -```ts -function identity(value: T): T { - return value; -} - -expectTypeOf(identity).returns.toBeString(); -expectTypeOf(identity).returns.toBeNumber(); -``` - -## Nullable Types - -```ts -type MaybeString = string | null | undefined; - -expectTypeOf().toBeNullable(); -expectTypeOf().not.toBeNullable(); -``` - -## assertType - -Assert a value matches a type (no assertion at runtime): - -```ts -import { assertType } from "vitest"; - -function getUser(): User | null { - return { id: 1, name: "test" }; -} - -test("returns user", () => { - const result = getUser(); - - // @ts-expect-error - should fail type check - assertType(result); - - // Correct type - assertType(result); -}); -``` - -## Using @ts-expect-error - -Test that code produces type error: - -```ts -test("rejects wrong types", () => { - function requireString(s: string) {} - - // @ts-expect-error - number not assignable to string - requireString(123); -}); -``` - -## Running Type Tests - -```bash -# Run type tests -vitest typecheck - -# Run alongside unit tests -vitest --typecheck - -# Type tests only -vitest --typecheck.only -``` - -## Mixed Test Files - -Combine runtime and type tests: - -```ts -// user.test.ts -import { describe, expect, expectTypeOf, test } from "vitest"; -import { createUser } from "./user"; - -describe("createUser", () => { - test("runtime: creates user", () => { - const user = createUser("John"); - expect(user.name).toBe("John"); - }); - - test("types: returns User type", () => { - expectTypeOf(createUser).returns.toMatchTypeOf<{ name: string }>(); - }); -}); -``` - -## Key Points - -- Use `.test-d.ts` for type-only tests -- `expectTypeOf` for type assertions -- `toMatchTypeOf` for subset matching -- `toEqualTypeOf` for exact type matching -- Use `@ts-expect-error` to test type errors -- Run with `vitest typecheck` or `--typecheck` - - diff --git a/.agents/skills/vitest/references/advanced-vi.md b/.agents/skills/vitest/references/advanced-vi.md deleted file mode 100644 index 51b549d..0000000 --- a/.agents/skills/vitest/references/advanced-vi.md +++ /dev/null @@ -1,251 +0,0 @@ ---- -name: vi-utilities -description: vi helper for mocking, timers, utilities ---- - -# Vi Utilities - -The `vi` helper provides mocking and utility functions. - -```ts -import { vi } from "vitest"; -``` - -## Mock Functions - -```ts -// Create mock -const fn = vi.fn(); -const fnWithImpl = vi.fn((x) => x * 2); - -// Check if mock -vi.isMockFunction(fn); // true - -// Mock methods -fn.mockReturnValue(42); -fn.mockReturnValueOnce(1); -fn.mockResolvedValue(data); -fn.mockRejectedValue(error); -fn.mockImplementation(() => "result"); -fn.mockImplementationOnce(() => "once"); - -// Clear/reset -fn.mockClear(); // Clear call history -fn.mockReset(); // Clear history + implementation -fn.mockRestore(); // Restore original (for spies) -``` - -## Spying - -```ts -const obj = { method: () => "original" }; - -const spy = vi.spyOn(obj, "method"); -obj.method(); - -expect(spy).toHaveBeenCalled(); - -// Mock implementation -spy.mockReturnValue("mocked"); - -// Spy on getter/setter -vi.spyOn(obj, "prop", "get").mockReturnValue("value"); -``` - -## Module Mocking - -```ts -// Hoisted to top of file -vi.mock("./module", () => ({ - fn: vi.fn(), -})); - -// Partial mock -vi.mock("./module", async (importOriginal) => ({ - ...(await importOriginal()), - specificFn: vi.fn(), -})); - -// Spy mode - keep implementation -vi.mock("./module", { spy: true }); - -// Import actual module inside mock -const actual = await vi.importActual("./module"); - -// Import as mock -const mocked = await vi.importMock("./module"); -``` - -## Dynamic Mocking - -```ts -// Not hoisted - use with dynamic imports -vi.doMock("./config", () => ({ key: "value" })); -const config = await import("./config"); - -// Unmock -vi.doUnmock("./config"); -vi.unmock("./module"); // Hoisted -``` - -## Reset Modules - -```ts -// Clear module cache -vi.resetModules(); - -// Wait for dynamic imports -await vi.dynamicImportSettled(); -``` - -## Fake Timers - -```ts -vi.useFakeTimers(); - -setTimeout(() => console.log("done"), 1000); - -// Advance time -vi.advanceTimersByTime(1000); -vi.advanceTimersByTimeAsync(1000); // For async callbacks -vi.advanceTimersToNextTimer(); -vi.advanceTimersToNextFrame(); // requestAnimationFrame - -// Run all timers -vi.runAllTimers(); -vi.runAllTimersAsync(); -vi.runOnlyPendingTimers(); - -// Clear timers -vi.clearAllTimers(); - -// Check state -vi.getTimerCount(); -vi.isFakeTimers(); - -// Restore -vi.useRealTimers(); -``` - -## Mock Date/Time - -```ts -vi.setSystemTime(new Date("2024-01-01")); -expect(new Date().getFullYear()).toBe(2024); - -vi.getMockedSystemTime(); // Get mocked date -vi.getRealSystemTime(); // Get real time (ms) -``` - -## Global/Env Mocking - -```ts -// Stub global -vi.stubGlobal("fetch", vi.fn()); -vi.unstubAllGlobals(); - -// Stub environment -vi.stubEnv("API_KEY", "test"); -vi.stubEnv("NODE_ENV", "test"); -vi.unstubAllEnvs(); -``` - -## Hoisted Code - -Run code before imports: - -```ts -const mock = vi.hoisted(() => vi.fn()); - -vi.mock("./module", () => ({ - fn: mock, // Can reference hoisted variable -})); -``` - -## Waiting Utilities - -```ts -// Wait for callback to succeed -await vi.waitFor( - async () => { - const el = document.querySelector(".loaded"); - expect(el).toBeTruthy(); - }, - { timeout: 5000, interval: 100 } -); - -// Wait for truthy value -const element = await vi.waitUntil(() => document.querySelector(".loaded"), { - timeout: 5000, -}); -``` - -## Mock Object - -Mock all methods of an object: - -```ts -const original = { - method: () => "real", - nested: { fn: () => "nested" }, -}; - -const mocked = vi.mockObject(original); -mocked.method(); // undefined (mocked) -mocked.method.mockReturnValue("mocked"); - -// Spy mode -const spied = vi.mockObject(original, { spy: true }); -spied.method(); // 'real' -expect(spied.method).toHaveBeenCalled(); -``` - -## Test Configuration - -```ts -vi.setConfig({ - testTimeout: 10_000, - hookTimeout: 10_000, -}); - -vi.resetConfig(); -``` - -## Global Mock Management - -```ts -vi.clearAllMocks(); // Clear all mock call history -vi.resetAllMocks(); // Reset + clear implementation -vi.restoreAllMocks(); // Restore originals (spies) -``` - -## vi.mocked Type Helper - -TypeScript helper for mocked values: - -```ts -import { myFn } from "./module"; -vi.mock("./module"); - -// Type as mock -vi.mocked(myFn).mockReturnValue("typed"); - -// Deep mocking -vi.mocked(myModule, { deep: true }); - -// Partial mock typing -vi.mocked(fn, { partial: true }).mockResolvedValue({ ok: true }); -``` - -## Key Points - -- `vi.mock` is hoisted - use `vi.doMock` for dynamic mocking -- `vi.hoisted` lets you reference variables in mock factories -- Use `vi.spyOn` to spy on existing methods -- Fake timers require explicit setup and teardown -- `vi.waitFor` retries until assertion passes - - diff --git a/.agents/skills/vitest/references/core-cli.md b/.agents/skills/vitest/references/core-cli.md deleted file mode 100644 index 9985459..0000000 --- a/.agents/skills/vitest/references/core-cli.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -name: vitest-cli -description: Command line interface commands and options ---- - -# Command Line Interface - -## Commands - -### `vitest` - -Start Vitest in watch mode (dev) or run mode (CI): - -```bash -vitest # Watch mode in dev, run mode in CI -vitest foobar # Run tests containing "foobar" in path -vitest basic/foo.test.ts:10 # Run specific test by file and line number -``` - -### `vitest run` - -Run tests once without watch mode: - -```bash -vitest run -vitest run --coverage -``` - -### `vitest watch` - -Explicitly start watch mode: - -```bash -vitest watch -``` - -### `vitest related` - -Run tests that import specific files (useful with lint-staged): - -```bash -vitest related src/index.ts src/utils.ts --run -``` - -### `vitest bench` - -Run only benchmark tests: - -```bash -vitest bench -``` - -### `vitest list` - -List all matching tests without running them: - -```bash -vitest list # List test names -vitest list --json # Output as JSON -vitest list --filesOnly # List only test files -``` - -### `vitest init` - -Initialize project setup: - -```bash -vitest init browser # Set up browser testing -``` - -## Common Options - -```bash -# Configuration ---config # Path to config file ---project # Run specific project - -# Filtering ---testNamePattern, -t # Run tests matching pattern ---changed # Run tests for changed files ---changed HEAD~1 # Tests for last commit changes - -# Reporters ---reporter # default, verbose, dot, json, html ---reporter=html --outputFile=report.html - -# Coverage ---coverage # Enable coverage ---coverage.provider v8 # Use v8 provider ---coverage.reporter text,html - -# Execution ---shard / # Split tests across machines ---bail # Stop after n failures ---retry # Retry failed tests n times ---sequence.shuffle # Randomize test order - -# Watch mode ---no-watch # Disable watch mode ---standalone # Start without running tests - -# Environment ---environment # jsdom, happy-dom, node ---globals # Enable global APIs - -# Debugging ---inspect # Enable Node inspector ---inspect-brk # Break on start - -# Output ---silent # Suppress console output ---no-color # Disable colors -``` - -## Package.json Scripts - -```json -{ - "scripts": { - "test": "vitest", - "test:run": "vitest run", - "test:ui": "vitest --ui", - "coverage": "vitest run --coverage" - } -} -``` - -## Sharding for CI - -Split tests across multiple machines: - -```bash -# Machine 1 -vitest run --shard=1/3 --reporter=blob - -# Machine 2 -vitest run --shard=2/3 --reporter=blob - -# Machine 3 -vitest run --shard=3/3 --reporter=blob - -# Merge reports -vitest --merge-reports --reporter=junit -``` - -## Watch Mode Keyboard Shortcuts - -In watch mode, press: - -- `a` - Run all tests -- `f` - Run only failed tests -- `u` - Update snapshots -- `p` - Filter by filename pattern -- `t` - Filter by test name pattern -- `q` - Quit - -## Key Points - -- Watch mode is default in dev, run mode in CI (when `process.env.CI` is set) -- Use `--run` flag to ensure single run (important for lint-staged) -- Both camelCase (`--testTimeout`) and kebab-case (`--test-timeout`) work -- Boolean options can be negated with `--no-` prefix - - diff --git a/.agents/skills/vitest/references/core-config.md b/.agents/skills/vitest/references/core-config.md deleted file mode 100644 index 4eae4c4..0000000 --- a/.agents/skills/vitest/references/core-config.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -name: vitest-configuration -description: Configure Vitest with vite.config.ts or vitest.config.ts ---- - -# Configuration - -Vitest reads configuration from `vitest.config.ts` or `vite.config.ts`. It shares the same config format as Vite. - -## Basic Setup - -```ts -// vitest.config.ts -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - // test options - }, -}); -``` - -## Using with Existing Vite Config - -Add Vitest types reference and use the `test` property: - -```ts -// vite.config.ts -/// -import { defineConfig } from "vite"; - -export default defineConfig({ - test: { - globals: true, - environment: "jsdom", - }, -}); -``` - -## Merging Configs - -If you have separate config files, use `mergeConfig`: - -```ts -// vitest.config.ts -import { defineConfig, mergeConfig } from "vitest/config"; -import viteConfig from "./vite.config"; - -export default mergeConfig( - viteConfig, - defineConfig({ - test: { - environment: "jsdom", - }, - }) -); -``` - -## Common Options - -```ts -defineConfig({ - test: { - // Enable global APIs (describe, it, expect) without imports - globals: true, - - // Test environment: 'node', 'jsdom', 'happy-dom' - environment: "node", - - // Setup files to run before each test file - setupFiles: ["./tests/setup.ts"], - - // Include patterns for test files - include: ["**/*.{test,spec}.{js,ts,jsx,tsx}"], - - // Exclude patterns - exclude: ["**/node_modules/**", "**/dist/**"], - - // Test timeout in ms - testTimeout: 5000, - - // Hook timeout in ms - hookTimeout: 10000, - - // Enable watch mode by default - watch: true, - - // Coverage configuration - coverage: { - provider: "v8", // or 'istanbul' - reporter: ["text", "html"], - include: ["src/**/*.ts"], - }, - - // Run tests in isolation (each file in separate process) - isolate: true, - - // Pool for running tests: 'threads', 'forks', 'vmThreads' - pool: "threads", - - // Number of threads/processes - poolOptions: { - threads: { - maxThreads: 4, - minThreads: 1, - }, - }, - - // Automatically clear mocks between tests - clearMocks: true, - - // Restore mocks between tests - restoreMocks: true, - - // Retry failed tests - retry: 0, - - // Stop after first failure - bail: 0, - }, -}); -``` - -## Conditional Configuration - -Use `mode` or `process.env.VITEST` for test-specific config: - -```ts -export default defineConfig(({ mode }) => ({ - plugins: mode === "test" ? [] : [myPlugin()], - test: { - // test options - }, -})); -``` - -## Projects (Monorepos) - -Run different configurations in the same Vitest process: - -```ts -defineConfig({ - test: { - projects: [ - "packages/*", - { - test: { - name: "unit", - include: ["tests/unit/**/*.test.ts"], - environment: "node", - }, - }, - { - test: { - name: "integration", - include: ["tests/integration/**/*.test.ts"], - environment: "jsdom", - }, - }, - ], - }, -}); -``` - -## Key Points - -- Vitest uses Vite's transformation pipeline - same `resolve.alias`, plugins work -- `vitest.config.ts` takes priority over `vite.config.ts` -- Use `--config` flag to specify a custom config path -- `process.env.VITEST` is set to `true` when running tests -- Test config uses `test` property, rest is Vite config - - diff --git a/.agents/skills/vitest/references/core-describe.md b/.agents/skills/vitest/references/core-describe.md deleted file mode 100644 index 80a1a46..0000000 --- a/.agents/skills/vitest/references/core-describe.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -name: describe-api -description: describe/suite for grouping tests into logical blocks ---- - -# Describe API - -Group related tests into suites for organization and shared setup. - -## Basic Usage - -```ts -import { describe, expect, test } from "vitest"; - -describe("Math", () => { - test("adds numbers", () => { - expect(1 + 1).toBe(2); - }); - - test("subtracts numbers", () => { - expect(3 - 1).toBe(2); - }); -}); - -// Alias: suite -import { suite } from "vitest"; -suite("equivalent to describe", () => {}); -``` - -## Nested Suites - -```ts -describe("User", () => { - describe("when logged in", () => { - test("shows dashboard", () => {}); - test("can update profile", () => {}); - }); - - describe("when logged out", () => { - test("shows login page", () => {}); - }); -}); -``` - -## Suite Options - -```ts -// All tests inherit options -describe("slow tests", { timeout: 30_000 }, () => { - test("test 1", () => {}); // 30s timeout - test("test 2", () => {}); // 30s timeout -}); -``` - -## Suite Modifiers - -### Skip Suites - -```ts -describe.skip("skipped suite", () => { - test("wont run", () => {}); -}); - -// Conditional -describe.skipIf(process.env.CI)("not in CI", () => {}); -describe.runIf(!process.env.CI)("only local", () => {}); -``` - -### Focus Suites - -```ts -describe.only("only this suite runs", () => { - test("runs", () => {}); -}); -``` - -### Todo Suites - -```ts -describe.todo("implement later"); -``` - -### Concurrent Suites - -```ts -// All tests run in parallel -describe.concurrent("parallel tests", () => { - test("test 1", async ({ expect }) => {}); - test("test 2", async ({ expect }) => {}); -}); -``` - -### Sequential in Concurrent - -```ts -describe.concurrent("parallel", () => { - test("concurrent 1", async () => {}); - - describe.sequential("must be sequential", () => { - test("step 1", async () => {}); - test("step 2", async () => {}); - }); -}); -``` - -### Shuffle Tests - -```ts -describe.shuffle("random order", () => { - test("test 1", () => {}); - test("test 2", () => {}); - test("test 3", () => {}); -}); - -// Or with option -describe("random", { shuffle: true }, () => {}); -``` - -## Parameterized Suites - -### describe.each - -```ts -describe.each([ - { name: "Chrome", version: 100 }, - { name: "Firefox", version: 90 }, -])("$name browser", ({ name, version }) => { - test("has version", () => { - expect(version).toBeGreaterThan(0); - }); -}); -``` - -### describe.for - -```ts -describe.for([ - ["Chrome", 100], - ["Firefox", 90], -])("%s browser", ([name, version]) => { - test("has version", () => { - expect(version).toBeGreaterThan(0); - }); -}); -``` - -## Hooks in Suites - -```ts -describe("Database", () => { - let db; - - beforeAll(async () => { - db = await createDb(); - }); - - afterAll(async () => { - await db.close(); - }); - - beforeEach(async () => { - await db.clear(); - }); - - test("insert works", async () => { - await db.insert({ name: "test" }); - expect(await db.count()).toBe(1); - }); -}); -``` - -## Modifier Combinations - -All modifiers can be chained: - -```ts -describe.skip.concurrent("skipped concurrent", () => {}); -describe.only.shuffle("only and shuffled", () => {}); -describe.concurrent.skip("equivalent", () => {}); -``` - -## Key Points - -- Top-level tests belong to an implicit file suite -- Nested suites inherit parent's options (timeout, retry, etc.) -- Hooks are scoped to their suite and nested suites -- Use `describe.concurrent` with context's `expect` for snapshots -- Shuffle order depends on `sequence.seed` config - - diff --git a/.agents/skills/vitest/references/core-expect.md b/.agents/skills/vitest/references/core-expect.md deleted file mode 100644 index 306e6a7..0000000 --- a/.agents/skills/vitest/references/core-expect.md +++ /dev/null @@ -1,211 +0,0 @@ ---- -name: expect-api -description: Assertions with matchers, asymmetric matchers, and custom matchers ---- - -# Expect API - -Vitest uses Chai assertions with Jest-compatible API. - -## Basic Assertions - -```ts -import { expect, test } from "vitest"; - -test("assertions", () => { - // Equality - expect(1 + 1).toBe(2); // Strict equality (===) - expect({ a: 1 }).toEqual({ a: 1 }); // Deep equality - - // Truthiness - expect(true).toBeTruthy(); - expect(false).toBeFalsy(); - expect(null).toBeNull(); - expect(undefined).toBeUndefined(); - expect("value").toBeDefined(); - - // Numbers - expect(10).toBeGreaterThan(5); - expect(10).toBeGreaterThanOrEqual(10); - expect(5).toBeLessThan(10); - expect(0.1 + 0.2).toBeCloseTo(0.3, 5); - - // Strings - expect("hello world").toMatch(/world/); - expect("hello").toContain("ell"); - - // Arrays - expect([1, 2, 3]).toContain(2); - expect([{ a: 1 }]).toContainEqual({ a: 1 }); - expect([1, 2, 3]).toHaveLength(3); - - // Objects - expect({ a: 1, b: 2 }).toHaveProperty("a"); - expect({ a: 1, b: 2 }).toHaveProperty("a", 1); - expect({ a: { b: 1 } }).toHaveProperty("a.b", 1); - expect({ a: 1 }).toMatchObject({ a: 1 }); - - // Types - expect("string").toBeTypeOf("string"); - expect(new Date()).toBeInstanceOf(Date); -}); -``` - -## Negation - -```ts -expect(1).not.toBe(2); -expect({ a: 1 }).not.toEqual({ a: 2 }); -``` - -## Error Assertions - -```ts -// Sync errors - wrap in function -expect(() => throwError()).toThrow(); -expect(() => throwError()).toThrow("message"); -expect(() => throwError()).toThrow(/pattern/); -expect(() => throwError()).toThrow(CustomError); - -// Async errors - use rejects -await expect(asyncThrow()).rejects.toThrow("error"); -``` - -## Promise Assertions - -```ts -// Resolves -await expect(Promise.resolve(1)).resolves.toBe(1); -await expect(fetchData()).resolves.toEqual({ data: true }); - -// Rejects -await expect(Promise.reject("error")).rejects.toBe("error"); -await expect(failingFetch()).rejects.toThrow(); -``` - -## Spy/Mock Assertions - -```ts -const fn = vi.fn(); -fn("arg1", "arg2"); -fn("arg3"); - -expect(fn).toHaveBeenCalled(); -expect(fn).toHaveBeenCalledTimes(2); -expect(fn).toHaveBeenCalledWith("arg1", "arg2"); -expect(fn).toHaveBeenLastCalledWith("arg3"); -expect(fn).toHaveBeenNthCalledWith(1, "arg1", "arg2"); - -expect(fn).toHaveReturned(); -expect(fn).toHaveReturnedWith(value); -``` - -## Asymmetric Matchers - -Use inside `toEqual`, `toHaveBeenCalledWith`, etc: - -```ts -expect({ id: 1, name: "test" }).toEqual({ - id: expect.any(Number), - name: expect.any(String), -}); - -expect({ a: 1, b: 2, c: 3 }).toEqual(expect.objectContaining({ a: 1 })); - -expect([1, 2, 3, 4]).toEqual(expect.arrayContaining([1, 3])); - -expect("hello world").toEqual(expect.stringContaining("world")); - -expect("hello world").toEqual(expect.stringMatching(/world$/)); - -expect({ value: null }).toEqual({ - value: expect.anything(), // Matches anything except null/undefined -}); - -// Negate with expect.not -expect([1, 2]).toEqual(expect.not.arrayContaining([3])); -``` - -## Soft Assertions - -Continue test after failure: - -```ts -expect.soft(1).toBe(2); // Marks test failed but continues -expect.soft(2).toBe(3); // Also runs -// All failures reported at end -``` - -## Poll Assertions - -Retry until passes: - -```ts -await expect.poll(() => fetchStatus()).toBe("ready"); - -await expect - .poll(() => document.querySelector(".element"), { - interval: 100, - timeout: 5000, - }) - .toBeTruthy(); -``` - -## Assertion Count - -```ts -test("async assertions", async () => { - expect.assertions(2); // Exactly 2 assertions must run - - await doAsync((data) => { - expect(data).toBeDefined(); - expect(data.id).toBe(1); - }); -}); - -test("at least one", () => { - expect.hasAssertions(); // At least 1 assertion must run -}); -``` - -## Extending Matchers - -```ts -expect.extend({ - toBeWithinRange(received, floor, ceiling) { - const pass = received >= floor && received <= ceiling; - return { - pass, - message: () => - `expected ${received} to be within range ${floor} - ${ceiling}`, - }; - }, -}); - -test("custom matcher", () => { - expect(100).toBeWithinRange(90, 110); -}); -``` - -## Snapshot Assertions - -```ts -expect(data).toMatchSnapshot() -expect(data).toMatchInlineSnapshot(`{ "id": 1 }`) -await expect(result).toMatchFileSnapshot('./expected.json') - -expect(() => throw new Error('fail')).toThrowErrorMatchingSnapshot() -``` - -## Key Points - -- Use `toBe` for primitives, `toEqual` for objects/arrays -- `toStrictEqual` checks undefined properties and array sparseness -- Always `await` async assertions (`resolves`, `rejects`, `poll`) -- Use context's `expect` in concurrent tests for correct tracking -- `toThrow` requires wrapping sync code in a function - - diff --git a/.agents/skills/vitest/references/core-hooks.md b/.agents/skills/vitest/references/core-hooks.md deleted file mode 100644 index 4f34893..0000000 --- a/.agents/skills/vitest/references/core-hooks.md +++ /dev/null @@ -1,245 +0,0 @@ ---- -name: lifecycle-hooks -description: beforeEach, afterEach, beforeAll, afterAll, and around hooks ---- - -# Lifecycle Hooks - -## Basic Hooks - -```ts -import { afterAll, afterEach, beforeAll, beforeEach, test } from "vitest"; - -beforeAll(async () => { - // Runs once before all tests in file/suite - await setupDatabase(); -}); - -afterAll(async () => { - // Runs once after all tests in file/suite - await teardownDatabase(); -}); - -beforeEach(async () => { - // Runs before each test - await clearTestData(); -}); - -afterEach(async () => { - // Runs after each test - await cleanupMocks(); -}); -``` - -## Cleanup Return Pattern - -Return cleanup function from `before*` hooks: - -```ts -beforeAll(async () => { - const server = await startServer(); - - // Returned function runs as afterAll - return async () => { - await server.close(); - }; -}); - -beforeEach(async () => { - const connection = await connect(); - - // Runs as afterEach - return () => connection.close(); -}); -``` - -## Scoped Hooks - -Hooks apply to current suite and nested suites: - -```ts -describe("outer", () => { - beforeEach(() => console.log("outer before")); - - test("test 1", () => {}); // outer before → test - - describe("inner", () => { - beforeEach(() => console.log("inner before")); - - test("test 2", () => {}); // outer before → inner before → test - }); -}); -``` - -## Hook Timeout - -```ts -beforeAll(async () => { - await slowSetup(); -}, 30_000); // 30 second timeout -``` - -## Around Hooks - -Wrap tests with setup/teardown context: - -```ts -import { aroundEach, test } from "vitest"; - -// Wrap each test in database transaction -aroundEach(async (runTest) => { - await db.beginTransaction(); - await runTest(); // Must be called! - await db.rollback(); -}); - -test("insert user", async () => { - await db.insert({ name: "Alice" }); - // Automatically rolled back after test -}); -``` - -### aroundAll - -Wrap entire suite: - -```ts -import { aroundAll, test } from "vitest"; - -aroundAll(async (runSuite) => { - console.log("before all tests"); - await runSuite(); // Must be called! - console.log("after all tests"); -}); -``` - -### Multiple Around Hooks - -Nested like onion layers: - -```ts -aroundEach(async (runTest) => { - console.log("outer before"); - await runTest(); - console.log("outer after"); -}); - -aroundEach(async (runTest) => { - console.log("inner before"); - await runTest(); - console.log("inner after"); -}); - -// Order: outer before → inner before → test → inner after → outer after -``` - -## Test Hooks - -Inside test body: - -```ts -import { onTestFailed, onTestFinished, test } from "vitest"; - -test("with cleanup", () => { - const db = connect(); - - // Runs after test finishes (pass or fail) - onTestFinished(() => db.close()); - - // Only runs if test fails - onTestFailed(({ task }) => { - console.log("Failed:", task.result?.errors); - }); - - db.query("SELECT * FROM users"); -}); -``` - -### Reusable Cleanup Pattern - -```ts -function useTestDb() { - const db = connect(); - onTestFinished(() => db.close()); - return db; -} - -test("query users", () => { - const db = useTestDb(); - expect(db.query("SELECT * FROM users")).toBeDefined(); -}); - -test("query orders", () => { - const db = useTestDb(); // Fresh connection, auto-closed - expect(db.query("SELECT * FROM orders")).toBeDefined(); -}); -``` - -## Concurrent Test Hooks - -For concurrent tests, use context's hooks: - -```ts -test.concurrent("concurrent", ({ onTestFinished }) => { - const resource = allocate(); - onTestFinished(() => resource.release()); -}); -``` - -## Extended Test Hooks - -With `test.extend`, hooks are type-aware: - -```ts -const test = base.extend<{ db: Database }>({ - db: async ({}, use) => { - const db = await createDb(); - await use(db); - await db.close(); - }, -}); - -// These hooks know about `db` fixture -test.beforeEach(({ db }) => { - db.seed(); -}); - -test.afterEach(({ db }) => { - db.clear(); -}); -``` - -## Hook Execution Order - -Default order (stack): - -1. `beforeAll` (in order) -2. `beforeEach` (in order) -3. Test -4. `afterEach` (reverse order) -5. `afterAll` (reverse order) - -Configure with `sequence.hooks`: - -```ts -defineConfig({ - test: { - sequence: { - hooks: "list", // 'stack' (default), 'list', 'parallel' - }, - }, -}); -``` - -## Key Points - -- Hooks are not called during type checking -- Return cleanup function from `before*` to avoid `after*` duplication -- `aroundEach`/`aroundAll` must call `runTest()`/`runSuite()` -- `onTestFinished` always runs, even if test fails -- Use context hooks for concurrent tests - - diff --git a/.agents/skills/vitest/references/core-test-api.md b/.agents/skills/vitest/references/core-test-api.md deleted file mode 100644 index bdf65b0..0000000 --- a/.agents/skills/vitest/references/core-test-api.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -name: test-api -description: test/it function for defining tests with modifiers ---- - -# Test API - -## Basic Test - -```ts -import { expect, test } from "vitest"; - -test("adds numbers", () => { - expect(1 + 1).toBe(2); -}); - -// Alias: it -import { it } from "vitest"; - -it("works the same", () => { - expect(true).toBe(true); -}); -``` - -## Async Tests - -```ts -test("async test", async () => { - const result = await fetchData(); - expect(result).toBeDefined(); -}); - -// Promises are automatically awaited -test("returns promise", () => { - return fetchData().then((result) => { - expect(result).toBeDefined(); - }); -}); -``` - -## Test Options - -```ts -// Timeout (default: 5000ms) -test("slow test", async () => { - // ... -}, 10_000); - -// Or with options object -test("with options", { timeout: 10_000, retry: 2 }, async () => { - // ... -}); -``` - -## Test Modifiers - -### Skip Tests - -```ts -test.skip("skipped test", () => { - // Won't run -}); - -// Conditional skip -test.skipIf(process.env.CI)("not in CI", () => {}); -test.runIf(process.env.CI)("only in CI", () => {}); - -// Dynamic skip via context -test("dynamic skip", ({ skip }) => { - skip(someCondition, "reason"); - // ... -}); -``` - -### Focus Tests - -```ts -test.only("only this runs", () => { - // Other tests in file are skipped -}); -``` - -### Todo Tests - -```ts -test.todo("implement later"); - -test.todo("with body", () => { - // Not run, shows in report -}); -``` - -### Failing Tests - -```ts -test.fails("expected to fail", () => { - expect(1).toBe(2); // Test passes because assertion fails -}); -``` - -### Concurrent Tests - -```ts -// Run tests in parallel -test.concurrent("test 1", async ({ expect }) => { - // Use context.expect for concurrent tests - expect(await fetch1()).toBe("result"); -}); - -test.concurrent("test 2", async ({ expect }) => { - expect(await fetch2()).toBe("result"); -}); -``` - -### Sequential Tests - -```ts -// Force sequential in concurrent context -test.sequential("must run alone", async () => {}); -``` - -## Parameterized Tests - -### test.each - -```ts -test.each([ - [1, 1, 2], - [1, 2, 3], - [2, 1, 3], -])("add(%i, %i) = %i", (a, b, expected) => { - expect(a + b).toBe(expected); -}); - -// With objects -test.each([ - { a: 1, b: 1, expected: 2 }, - { a: 1, b: 2, expected: 3 }, -])("add($a, $b) = $expected", ({ a, b, expected }) => { - expect(a + b).toBe(expected); -}); - -// Template literal -test.each` - a | b | expected - ${1} | ${1} | ${2} - ${1} | ${2} | ${3} -`("add($a, $b) = $expected", ({ a, b, expected }) => { - expect(a + b).toBe(expected); -}); -``` - -### test.for - -Preferred over `.each` - doesn't spread arrays: - -```ts -test.for([ - [1, 1, 2], - [1, 2, 3], -])("add(%i, %i) = %i", ([a, b, expected], { expect }) => { - // Second arg is TestContext - expect(a + b).toBe(expected); -}); -``` - -## Test Context - -First argument provides context utilities: - -```ts -test("with context", ({ expect, skip, task }) => { - console.log(task.name); // Test name - skip(someCondition); // Skip dynamically - expect(1).toBe(1); // Context-bound expect -}); -``` - -## Custom Test with Fixtures - -```ts -import { test as base } from "vitest"; - -const test = base.extend({ - db: async ({}, use) => { - const db = await createDb(); - await use(db); - await db.close(); - }, -}); - -test("query", async ({ db }) => { - const users = await db.query("SELECT * FROM users"); - expect(users).toBeDefined(); -}); -``` - -## Retry Configuration - -```ts -test("flaky test", { retry: 3 }, async () => { - // Retries up to 3 times on failure -}); - -// Advanced retry options -test( - "with delay", - { - retry: { - count: 3, - delay: 1000, - condition: /timeout/i, // Only retry on timeout errors - }, - }, - async () => {} -); -``` - -## Tags - -```ts -test("database test", { tags: ["db", "slow"] }, async () => {}); - -// Run with: vitest --tags db -``` - -## Key Points - -- Tests with no body are marked as `todo` -- `test.only` throws in CI unless `allowOnly: true` -- Use context's `expect` for concurrent tests and snapshots -- Function name is used as test name if passed as first arg - - diff --git a/.agents/skills/vitest/references/features-concurrency.md b/.agents/skills/vitest/references/features-concurrency.md deleted file mode 100644 index 956a709..0000000 --- a/.agents/skills/vitest/references/features-concurrency.md +++ /dev/null @@ -1,250 +0,0 @@ ---- -name: concurrency-parallelism -description: Concurrent tests, parallel execution, and sharding ---- - -# Concurrency & Parallelism - -## File Parallelism - -By default, Vitest runs test files in parallel across workers: - -```ts -defineConfig({ - test: { - // Run files in parallel (default: true) - fileParallelism: true, - - // Number of worker threads - maxWorkers: 4, - minWorkers: 1, - - // Pool type: 'threads', 'forks', 'vmThreads' - pool: "threads", - }, -}); -``` - -## Concurrent Tests - -Run tests within a file in parallel: - -```ts -// Individual concurrent tests -test.concurrent("test 1", async ({ expect }) => { - expect(await fetch1()).toBe("result"); -}); - -test.concurrent("test 2", async ({ expect }) => { - expect(await fetch2()).toBe("result"); -}); - -// All tests in suite concurrent -describe.concurrent("parallel suite", () => { - test("test 1", async ({ expect }) => {}); - test("test 2", async ({ expect }) => {}); -}); -``` - -**Important:** Use `{ expect }` from context for concurrent tests. - -## Sequential in Concurrent Context - -Force sequential execution: - -```ts -describe.concurrent("mostly parallel", () => { - test("parallel 1", async () => {}); - test("parallel 2", async () => {}); - - test.sequential("must run alone 1", async () => {}); - test.sequential("must run alone 2", async () => {}); -}); - -// Or entire suite -describe.sequential("sequential suite", () => { - test("first", () => {}); - test("second", () => {}); -}); -``` - -## Max Concurrency - -Limit concurrent tests: - -```ts -defineConfig({ - test: { - maxConcurrency: 5, // Max concurrent tests per file - }, -}); -``` - -## Isolation - -Each file runs in isolated environment by default: - -```ts -defineConfig({ - test: { - // Disable isolation for faster runs (less safe) - isolate: false, - }, -}); -``` - -## Sharding - -Split tests across machines: - -```bash -# Machine 1 -vitest run --shard=1/3 - -# Machine 2 -vitest run --shard=2/3 - -# Machine 3 -vitest run --shard=3/3 -``` - -### CI Example (GitHub Actions) - -```yaml -jobs: - test: - strategy: - matrix: - shard: [1, 2, 3] - steps: - - run: vitest run --shard=${{ matrix.shard }}/3 --reporter=blob - - merge: - needs: test - steps: - - run: vitest --merge-reports --reporter=junit -``` - -### Merge Reports - -```bash -# Each shard outputs blob -vitest run --shard=1/3 --reporter=blob --coverage -vitest run --shard=2/3 --reporter=blob --coverage - -# Merge all blobs -vitest --merge-reports --reporter=json --coverage -``` - -## Test Sequence - -Control test order: - -```ts -defineConfig({ - test: { - sequence: { - // Run tests in random order - shuffle: true, - - // Seed for reproducible shuffle - seed: 12345, - - // Hook execution order - hooks: "stack", // 'stack', 'list', 'parallel' - - // All tests concurrent by default - concurrent: true, - }, - }, -}); -``` - -## Shuffle Tests - -Randomize to catch hidden dependencies: - -```ts -// Via CLI -vitest --sequence.shuffle - -// Per suite -describe.shuffle('random order', () => { - test('test 1', () => {}) - test('test 2', () => {}) - test('test 3', () => {}) -}) -``` - -## Pool Options - -### Threads (Default) - -```ts -defineConfig({ - test: { - pool: "threads", - poolOptions: { - threads: { - maxThreads: 8, - minThreads: 2, - isolate: true, - }, - }, - }, -}); -``` - -### Forks - -Better isolation, slower: - -```ts -defineConfig({ - test: { - pool: "forks", - poolOptions: { - forks: { - maxForks: 4, - isolate: true, - }, - }, - }, -}); -``` - -### VM Threads - -Full VM isolation per file: - -```ts -defineConfig({ - test: { - pool: "vmThreads", - }, -}); -``` - -## Bail on Failure - -Stop after first failure: - -```bash -vitest --bail 1 # Stop after 1 failure -vitest --bail # Stop on first failure (same as --bail 1) -``` - -## Key Points - -- Files run in parallel by default -- Use `.concurrent` for parallel tests within file -- Always use context's `expect` in concurrent tests -- Sharding splits tests across CI machines -- Use `--merge-reports` to combine sharded results -- Shuffle tests to find hidden dependencies - - diff --git a/.agents/skills/vitest/references/features-context.md b/.agents/skills/vitest/references/features-context.md deleted file mode 100644 index 73732ce..0000000 --- a/.agents/skills/vitest/references/features-context.md +++ /dev/null @@ -1,240 +0,0 @@ ---- -name: test-context-fixtures -description: Test context, custom fixtures with test.extend ---- - -# Test Context & Fixtures - -## Built-in Context - -Every test receives context as first argument: - -```ts -test("context", ({ task, expect, skip }) => { - console.log(task.name); // Test name - expect(1).toBe(1); // Context-bound expect - skip(); // Skip test dynamically -}); -``` - -### Context Properties - -- `task` - Test metadata (name, file, etc.) -- `expect` - Expect bound to this test (important for concurrent tests) -- `skip(condition?, message?)` - Skip the test -- `onTestFinished(fn)` - Cleanup after test -- `onTestFailed(fn)` - Run on failure only - -## Custom Fixtures with test.extend - -Create reusable test utilities: - -```ts -import { test as base } from "vitest"; - -// Define fixture types -interface Fixtures { - db: Database; - user: User; -} - -// Create extended test -export const test = base.extend({ - // Fixture with setup/teardown - db: async ({}, use) => { - const db = await createDatabase(); - await use(db); // Provide to test - await db.close(); // Cleanup - }, - - // Fixture depending on another fixture - user: async ({ db }, use) => { - const user = await db.createUser({ name: "Test" }); - await use(user); - await db.deleteUser(user.id); - }, -}); -``` - -Using fixtures: - -```ts -test("query user", async ({ db, user }) => { - const found = await db.findUser(user.id); - expect(found).toEqual(user); -}); -``` - -## Fixture Initialization - -Fixtures only initialize when accessed: - -```ts -const test = base.extend({ - expensive: async ({}, use) => { - console.log("initializing"); // Only runs if test uses it - await use("value"); - }, -}); - -test("no fixture", () => {}); // expensive not called -test("uses fixture", ({ expensive }) => {}); // expensive called -``` - -## Auto Fixtures - -Run fixture for every test: - -```ts -const test = base.extend({ - setup: [ - async ({}, use) => { - await globalSetup(); - await use(); - await globalTeardown(); - }, - { auto: true }, // Always run - ], -}); -``` - -## Scoped Fixtures - -### File Scope - -Initialize once per file: - -```ts -const test = base.extend({ - connection: [ - async ({}, use) => { - const conn = await connect(); - await use(conn); - await conn.close(); - }, - { scope: "file" }, - ], -}); -``` - -### Worker Scope - -Initialize once per worker: - -```ts -const test = base.extend({ - sharedResource: [ - async ({}, use) => { - await use(globalResource); - }, - { scope: "worker" }, - ], -}); -``` - -## Injected Fixtures (from Config) - -Override fixtures per project: - -```ts -// test file -const test = base.extend({ - apiUrl: ["/default", { injected: true }], -}); - -// vitest.config.ts -defineConfig({ - test: { - projects: [ - { - test: { - name: "prod", - provide: { apiUrl: "https://api.prod.com" }, - }, - }, - ], - }, -}); -``` - -## Scoped Values per Suite - -Override fixture for specific suite: - -```ts -const test = base.extend({ - environment: "development", -}); - -describe("production tests", () => { - test.scoped({ environment: "production" }); - - test("uses production", ({ environment }) => { - expect(environment).toBe("production"); - }); -}); - -test("uses default", ({ environment }) => { - expect(environment).toBe("development"); -}); -``` - -## Extended Test Hooks - -Type-aware hooks with fixtures: - -```ts -const test = base.extend<{ db: Database }>({ - db: async ({}, use) => { - const db = await createDb(); - await use(db); - await db.close(); - }, -}); - -// Hooks know about fixtures -test.beforeEach(({ db }) => { - db.seed(); -}); - -test.afterEach(({ db }) => { - db.clear(); -}); -``` - -## Composing Fixtures - -Extend from another extended test: - -```ts -// base-test.ts -export const test = base.extend<{ db: Database }>({ - db: async ({}, use) => { - /* ... */ - }, -}); - -// admin-test.ts -import { test as dbTest } from "./base-test"; - -export const test = dbTest.extend<{ admin: User }>({ - admin: async ({ db }, use) => { - const admin = await db.createAdmin(); - await use(admin); - }, -}); -``` - -## Key Points - -- Use `{ }` destructuring to access fixtures -- Fixtures are lazy - only initialize when accessed -- Return cleanup function from fixtures -- Use `{ auto: true }` for setup fixtures -- Use `{ scope: 'file' }` for expensive shared resources -- Fixtures compose - extend from extended tests - - diff --git a/.agents/skills/vitest/references/features-coverage.md b/.agents/skills/vitest/references/features-coverage.md deleted file mode 100644 index 6167c2d..0000000 --- a/.agents/skills/vitest/references/features-coverage.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -name: code-coverage -description: Code coverage with V8 or Istanbul providers ---- - -# Code Coverage - -## Setup - -```bash -# Run tests with coverage -vitest run --coverage -``` - -## Configuration - -```ts -// vitest.config.ts -defineConfig({ - test: { - coverage: { - // Provider: 'v8' (default, faster) or 'istanbul' (more compatible) - provider: "v8", - - // Enable coverage - enabled: true, - - // Reporters - reporter: ["text", "json", "html"], - - // Files to include - include: ["src/**/*.{ts,tsx}"], - - // Files to exclude - exclude: ["node_modules/", "tests/", "**/*.d.ts", "**/*.test.ts"], - - // Report uncovered files - all: true, - - // Thresholds - thresholds: { - lines: 80, - functions: 80, - branches: 80, - statements: 80, - }, - }, - }, -}); -``` - -## Providers - -### V8 (Default) - -```bash -npm i -D @vitest/coverage-v8 -``` - -- Faster, no pre-instrumentation -- Uses V8's native coverage -- Recommended for most projects - -### Istanbul - -```bash -npm i -D @vitest/coverage-istanbul -``` - -- Pre-instruments code -- Works in any JS runtime -- More overhead but widely compatible - -## Reporters - -```ts -coverage: { - reporter: [ - 'text', // Terminal output - 'text-summary', // Summary only - 'json', // JSON file - 'html', // HTML report - 'lcov', // For CI tools - 'cobertura', // XML format - ], - reportsDirectory: './coverage', -} -``` - -## Thresholds - -Fail tests if coverage is below threshold: - -```ts -coverage: { - thresholds: { - // Global thresholds - lines: 80, - functions: 75, - branches: 70, - statements: 80, - - // Per-file thresholds - perFile: true, - - // Auto-update thresholds (for gradual improvement) - autoUpdate: true, - }, -} -``` - -## Ignoring Code - -### V8 - -```ts -/* v8 ignore next -- @preserve */ -function ignored() { - return "not covered"; -} - -/* v8 ignore start -- @preserve */ -// All code here ignored -/* v8 ignore stop -- @preserve */ -``` - -### Istanbul - -```ts -/* istanbul ignore next -- @preserve */ -function ignored() {} - -/* istanbul ignore if -- @preserve */ -if (condition) { - // ignored -} -``` - -Note: `@preserve` keeps comments through esbuild. - -## Package.json Scripts - -```json -{ - "scripts": { - "test": "vitest", - "test:coverage": "vitest run --coverage", - "test:coverage:watch": "vitest --coverage" - } -} -``` - -## Vitest UI Coverage - -Enable HTML coverage in Vitest UI: - -```ts -coverage: { - enabled: true, - reporter: ['text', 'html'], -} -``` - -Run with `vitest --ui` to view coverage visually. - -## CI Integration - -```yaml -# GitHub Actions -- name: Run tests with coverage - run: npm run test:coverage - -- name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - files: ./coverage/lcov.info -``` - -## Coverage with Sharding - -Merge coverage from sharded runs: - -```bash -vitest run --shard=1/3 --coverage --reporter=blob -vitest run --shard=2/3 --coverage --reporter=blob -vitest run --shard=3/3 --coverage --reporter=blob - -vitest --merge-reports --coverage --reporter=json -``` - -## Key Points - -- V8 is faster, Istanbul is more compatible -- Use `--coverage` flag or `coverage.enabled: true` -- Include `all: true` to see uncovered files -- Set thresholds to enforce minimum coverage -- Use `@preserve` comment to keep ignore hints - - diff --git a/.agents/skills/vitest/references/features-filtering.md b/.agents/skills/vitest/references/features-filtering.md deleted file mode 100644 index ad79909..0000000 --- a/.agents/skills/vitest/references/features-filtering.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -name: test-filtering -description: Filter tests by name, file patterns, and tags ---- - -# Test Filtering - -## CLI Filtering - -### By File Path - -```bash -# Run files containing "user" -vitest user - -# Multiple patterns -vitest user auth - -# Specific file -vitest src/user.test.ts - -# By line number -vitest src/user.test.ts:25 -``` - -### By Test Name - -```bash -# Tests matching pattern -vitest -t "login" -vitest --testNamePattern "should.*work" - -# Regex patterns -vitest -t "/user|auth/" -``` - -## Changed Files - -```bash -# Uncommitted changes -vitest --changed - -# Since specific commit -vitest --changed HEAD~1 -vitest --changed abc123 - -# Since branch -vitest --changed origin/main -``` - -## Related Files - -Run tests that import specific files: - -```bash -vitest related src/utils.ts src/api.ts --run -``` - -Useful with lint-staged: - -```js -// .lintstagedrc.js -export default { - "*.{ts,tsx}": "vitest related --run", -}; -``` - -## Focus Tests (.only) - -```ts -test.only("only this runs", () => {}); - -describe.only("only this suite", () => { - test("runs", () => {}); -}); -``` - -In CI, `.only` throws error unless configured: - -```ts -defineConfig({ - test: { - allowOnly: true, // Allow .only in CI - }, -}); -``` - -## Skip Tests - -```ts -test.skip("skipped", () => {}); - -// Conditional -test.skipIf(process.env.CI)("not in CI", () => {}); -test.runIf(!process.env.CI)("local only", () => {}); - -// Dynamic skip -test("dynamic", ({ skip }) => { - skip(someCondition, "reason"); -}); -``` - -## Tags - -Filter by custom tags: - -```ts -test("database test", { tags: ["db"] }, () => {}); -test("slow test", { tags: ["slow", "integration"] }, () => {}); -``` - -Run tagged tests: - -```bash -vitest --tags db -vitest --tags "db,slow" # OR -vitest --tags db --tags slow # OR -``` - -Configure allowed tags: - -```ts -defineConfig({ - test: { - tags: ["db", "slow", "integration"], - strictTags: true, // Fail on unknown tags - }, -}); -``` - -## Include/Exclude Patterns - -```ts -defineConfig({ - test: { - // Test file patterns - include: ["**/*.{test,spec}.{ts,tsx}"], - - // Exclude patterns - exclude: ["**/node_modules/**", "**/e2e/**", "**/*.skip.test.ts"], - - // Include source for in-source testing - includeSource: ["src/**/*.ts"], - }, -}); -``` - -## Watch Mode Filtering - -In watch mode, press: - -- `p` - Filter by filename pattern -- `t` - Filter by test name pattern -- `a` - Run all tests -- `f` - Run only failed tests - -## Projects Filtering - -Run specific project: - -```bash -vitest --project unit -vitest --project integration --project e2e -``` - -## Environment-based Filtering - -```ts -const isDev = process.env.NODE_ENV === "development"; -const isCI = process.env.CI; - -describe.skipIf(isCI)("local only tests", () => {}); -describe.runIf(isDev)("dev tests", () => {}); -``` - -## Combining Filters - -```bash -# File pattern + test name + changed -vitest user -t "login" --changed - -# Related files + run mode -vitest related src/auth.ts --run -``` - -## List Tests Without Running - -```bash -vitest list # Show all test names -vitest list -t "user" # Filter by name -vitest list --filesOnly # Show only file paths -vitest list --json # JSON output -``` - -## Key Points - -- Use `-t` for test name pattern filtering -- `--changed` runs only tests affected by changes -- `--related` runs tests importing specific files -- Tags provide semantic test grouping -- Use `.only` for debugging, but configure CI to reject it -- Watch mode has interactive filtering - - diff --git a/.agents/skills/vitest/references/features-mocking.md b/.agents/skills/vitest/references/features-mocking.md deleted file mode 100644 index 4c9e4c7..0000000 --- a/.agents/skills/vitest/references/features-mocking.md +++ /dev/null @@ -1,272 +0,0 @@ ---- -name: mocking -description: Mock functions, modules, timers, and dates with vi utilities ---- - -# Mocking - -## Mock Functions - -```ts -import { expect, vi } from "vitest"; - -// Create mock function -const fn = vi.fn(); -fn("hello"); - -expect(fn).toHaveBeenCalled(); -expect(fn).toHaveBeenCalledWith("hello"); - -// With implementation -const add = vi.fn((a, b) => a + b); -expect(add(1, 2)).toBe(3); - -// Mock return values -fn.mockReturnValue(42); -fn.mockReturnValueOnce(1).mockReturnValueOnce(2); -fn.mockResolvedValue({ data: true }); -fn.mockRejectedValue(new Error("fail")); - -// Mock implementation -fn.mockImplementation((x) => x * 2); -fn.mockImplementationOnce(() => "first call"); -``` - -## Spying on Objects - -```ts -const cart = { - getTotal: () => 100, -}; - -const spy = vi.spyOn(cart, "getTotal"); -cart.getTotal(); - -expect(spy).toHaveBeenCalled(); - -// Mock implementation -spy.mockReturnValue(200); -expect(cart.getTotal()).toBe(200); - -// Restore original -spy.mockRestore(); -``` - -## Module Mocking - -```ts -// vi.mock is hoisted to top of file -vi.mock("./api", () => ({ - fetchUser: vi.fn(() => ({ id: 1, name: "Mock" })), -})); - -import { fetchUser } from "./api"; - -test("mocked module", () => { - expect(fetchUser()).toEqual({ id: 1, name: "Mock" }); -}); -``` - -### Partial Mock - -```ts -vi.mock("./utils", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - specificFunction: vi.fn(), - }; -}); -``` - -### Auto-mock with Spy - -```ts -// Keep implementation but spy on calls -vi.mock("./calculator", { spy: true }); - -import { add } from "./calculator"; - -test("spy on module", () => { - const result = add(1, 2); // Real implementation - expect(result).toBe(3); - expect(add).toHaveBeenCalledWith(1, 2); -}); -``` - -### Manual Mocks (**mocks**) - -``` -src/ - __mocks__/ - axios.ts # Mocks 'axios' - api/ - __mocks__/ - client.ts # Mocks './client' - client.ts -``` - -```ts -// Just call vi.mock with no factory -vi.mock("axios"); -vi.mock("./api/client"); -``` - -## Dynamic Mocking (vi.doMock) - -Not hoisted - use for dynamic imports: - -```ts -test("dynamic mock", async () => { - vi.doMock("./config", () => ({ - apiUrl: "http://test.local", - })); - - const { apiUrl } = await import("./config"); - expect(apiUrl).toBe("http://test.local"); - - vi.doUnmock("./config"); -}); -``` - -## Mock Timers - -```ts -import { afterEach, beforeEach, vi } from "vitest"; - -beforeEach(() => { - vi.useFakeTimers(); -}); - -afterEach(() => { - vi.useRealTimers(); -}); - -test("timers", () => { - const fn = vi.fn(); - setTimeout(fn, 1000); - - expect(fn).not.toHaveBeenCalled(); - - vi.advanceTimersByTime(1000); - expect(fn).toHaveBeenCalled(); -}); - -// Other timer methods -vi.runAllTimers(); // Run all pending timers -vi.runOnlyPendingTimers(); // Run only currently pending -vi.advanceTimersToNextTimer(); // Advance to next timer -``` - -### Async Timer Methods - -```ts -test("async timers", async () => { - vi.useFakeTimers(); - - let resolved = false; - setTimeout( - () => - Promise.resolve().then(() => { - resolved = true; - }), - 100 - ); - - await vi.advanceTimersByTimeAsync(100); - expect(resolved).toBe(true); -}); -``` - -## Mock Dates - -```ts -vi.setSystemTime(new Date("2024-01-01")); -expect(new Date().getFullYear()).toBe(2024); - -vi.useRealTimers(); // Restore -``` - -## Mock Globals - -```ts -vi.stubGlobal( - "fetch", - vi.fn(() => Promise.resolve({ json: () => ({ data: "mock" }) })) -); - -// Restore -vi.unstubAllGlobals(); -``` - -## Mock Environment Variables - -```ts -vi.stubEnv("API_KEY", "test-key"); -expect(import.meta.env.API_KEY).toBe("test-key"); - -// Restore -vi.unstubAllEnvs(); -``` - -## Clearing Mocks - -```ts -const fn = vi.fn(); -fn(); - -fn.mockClear(); // Clear call history -fn.mockReset(); // Clear history + implementation -fn.mockRestore(); // Restore original (for spies) - -// Global -vi.clearAllMocks(); -vi.resetAllMocks(); -vi.restoreAllMocks(); -``` - -## Config Auto-Reset - -```ts -// vitest.config.ts -defineConfig({ - test: { - clearMocks: true, // Clear before each test - mockReset: true, // Reset before each test - restoreMocks: true, // Restore after each test - unstubEnvs: true, // Restore env vars - unstubGlobals: true, // Restore globals - }, -}); -``` - -## Hoisted Variables for Mocks - -```ts -const mockFn = vi.hoisted(() => vi.fn()); - -vi.mock("./module", () => ({ - getData: mockFn, -})); - -import { getData } from "./module"; - -test("hoisted mock", () => { - mockFn.mockReturnValue("test"); - expect(getData()).toBe("test"); -}); -``` - -## Key Points - -- `vi.mock` is hoisted - called before imports -- Use `vi.doMock` for dynamic, non-hoisted mocking -- Always restore mocks to avoid test pollution -- Use `{ spy: true }` to keep implementation but track calls -- `vi.hoisted` lets you reference variables in mock factories - - diff --git a/.agents/skills/vitest/references/features-snapshots.md b/.agents/skills/vitest/references/features-snapshots.md deleted file mode 100644 index 2e51223..0000000 --- a/.agents/skills/vitest/references/features-snapshots.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -name: snapshot-testing -description: Snapshot testing with file, inline, and file snapshots ---- - -# Snapshot Testing - -Snapshot tests capture output and compare against stored references. - -## Basic Snapshot - -```ts -import { expect, test } from "vitest"; - -test("snapshot", () => { - const result = generateOutput(); - expect(result).toMatchSnapshot(); -}); -``` - -First run creates `.snap` file: - -```js -// __snapshots__/test.spec.ts.snap -exports["snapshot 1"] = ` -{ - "id": 1, - "name": "test" -} -`; -``` - -## Inline Snapshots - -Stored directly in test file: - -```ts -test("inline snapshot", () => { - const data = { foo: "bar" }; - expect(data).toMatchInlineSnapshot(); -}); -``` - -Vitest updates the test file: - -```ts -test("inline snapshot", () => { - const data = { foo: "bar" }; - expect(data).toMatchInlineSnapshot(` - { - "foo": "bar", - } - `); -}); -``` - -## File Snapshots - -Compare against explicit file: - -```ts -test("render html", async () => { - const html = renderComponent(); - await expect(html).toMatchFileSnapshot("./expected/component.html"); -}); -``` - -## Snapshot Hints - -Add descriptive hints: - -```ts -test("multiple snapshots", () => { - expect(header).toMatchSnapshot("header"); - expect(body).toMatchSnapshot("body content"); - expect(footer).toMatchSnapshot("footer"); -}); -``` - -## Object Shape Matching - -Match partial structure: - -```ts -test("shape snapshot", () => { - const data = { - id: Math.random(), - created: new Date(), - name: "test", - }; - - expect(data).toMatchSnapshot({ - id: expect.any(Number), - created: expect.any(Date), - }); -}); -``` - -## Error Snapshots - -```ts -test("error message", () => { - expect(() => { - throw new Error("Something went wrong"); - }).toThrowErrorMatchingSnapshot(); -}); - -test("inline error", () => { - expect(() => { - throw new Error("Bad input"); - }).toThrowErrorMatchingInlineSnapshot(`[Error: Bad input]`); -}); -``` - -## Updating Snapshots - -```bash -# Update all snapshots -vitest -u -vitest --update - -# In watch mode, press 'u' to update failed snapshots -``` - -## Custom Serializers - -Add custom snapshot formatting: - -```ts -expect.addSnapshotSerializer({ - test(val) { - return val && typeof val.toJSON === "function"; - }, - serialize(val, config, indentation, depth, refs, printer) { - return printer(val.toJSON(), config, indentation, depth, refs); - }, -}); -``` - -Or via config: - -```ts -// vitest.config.ts -defineConfig({ - test: { - snapshotSerializers: ["./my-serializer.ts"], - }, -}); -``` - -## Snapshot Format Options - -```ts -defineConfig({ - test: { - snapshotFormat: { - printBasicPrototype: false, // Don't print Array/Object prototypes - escapeString: false, - }, - }, -}); -``` - -## Concurrent Test Snapshots - -Use context's expect: - -```ts -test.concurrent("concurrent 1", async ({ expect }) => { - expect(await getData()).toMatchSnapshot(); -}); - -test.concurrent("concurrent 2", async ({ expect }) => { - expect(await getOther()).toMatchSnapshot(); -}); -``` - -## Snapshot File Location - -Default: `__snapshots__/.snap` - -Customize: - -```ts -defineConfig({ - test: { - resolveSnapshotPath: (testPath, snapExtension) => { - return testPath.replace("__tests__", "__snapshots__") + snapExtension; - }, - }, -}); -``` - -## Key Points - -- Commit snapshot files to version control -- Review snapshot changes in code review -- Use hints for multiple snapshots in one test -- Use `toMatchFileSnapshot` for large outputs (HTML, JSON) -- Inline snapshots auto-update in test file -- Use context's `expect` for concurrent tests - - diff --git a/.claude/skills/tsdoc-jsdoc-authoring b/.claude/skills/tsdoc-jsdoc-authoring deleted file mode 120000 index 74d9f11..0000000 --- a/.claude/skills/tsdoc-jsdoc-authoring +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/tsdoc-jsdoc-authoring \ No newline at end of file diff --git a/.claude/skills/vitest b/.claude/skills/vitest deleted file mode 120000 index 7661536..0000000 --- a/.claude/skills/vitest +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/vitest \ No newline at end of file diff --git a/.cursor/hooks.json b/.cursor/hooks.json deleted file mode 100644 index 0a92b6a..0000000 --- a/.cursor/hooks.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": 1, - "hooks": { - "afterFileEdit": [ - { - "command": "bun x ultracite fix" - } - ] - } -} diff --git a/.cursor/skills/tsdoc-jsdoc-authoring b/.cursor/skills/tsdoc-jsdoc-authoring deleted file mode 120000 index 74d9f11..0000000 --- a/.cursor/skills/tsdoc-jsdoc-authoring +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/tsdoc-jsdoc-authoring \ No newline at end of file diff --git a/.cursor/skills/vitest b/.cursor/skills/vitest deleted file mode 120000 index 7661536..0000000 --- a/.cursor/skills/vitest +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/vitest \ No newline at end of file From 5dcac6c4de165ba70311b09a976a951b23c2dee7 Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 21:22:48 +0000 Subject: [PATCH 06/12] Refactor JSON configuration files for improved formatting and consistency - Updated .oxfmtrc.jsonc, .oxlintrc.json, and package.json to use consistent indentation and formatting styles. - Enhanced readability by aligning JSON properties and values. - Made minor adjustments to other configuration files to maintain uniformity across the project. --- .changeset/config.json | 18 +- .oxfmtrc.jsonc | 38 +- .oxlintrc.json | 4 +- README.md | 4 +- examples/basic/readie.json | 38 +- examples/c15t/README.md | 12 +- examples/c15t/old.md | 12 +- examples/c15t/readie.global.json | 130 ++--- examples/c15t/readie.json | 70 +-- package.json | 148 +++--- readie.json | 128 ++--- schemas/readie.global.schema.json | 278 +++++----- schemas/readie.schema.json | 310 +++++------ src/cli/commands/generate-workspace.ts | 152 +++--- src/cli/commands/generate.ts | 100 ++-- src/cli/commands/init.ts | 78 +-- src/cli/help.ts | 2 +- src/cli/resolve-invocation.ts | 56 +- src/config/load-config.ts | 542 ++++++++++---------- src/config/starter-config.ts | 20 +- src/config/types.ts | 96 ++-- src/index.ts | 82 +-- src/readme-generator/generator.ts | 360 ++++++------- src/readme-generator/template.ts | 424 +++++++-------- test/generator-global-interpolation.test.ts | 76 +-- test/merge-config.test.ts | 102 ++-- test/resolve-invocation.test.ts | 38 +- test/template.test.ts | 42 +- test/validate-config.test.ts | 48 +- tsconfig.json | 58 +-- 30 files changed, 1733 insertions(+), 1733 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index 3698b62..3fb9a10 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,11 +1,11 @@ { - "$schema": "https://unpkg.com/@changesets/config@latest/schema.json", - "changelog": "@changesets/cli/changelog", - "commit": false, - "fixed": [], - "linked": [], - "ignore": [], - "access": "public", - "baseBranch": "main", - "updateInternalDependencies": "patch" + "$schema": "https://unpkg.com/@changesets/config@latest/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "ignore": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch" } diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc index b1bb9b1..01f9593 100644 --- a/.oxfmtrc.jsonc +++ b/.oxfmtrc.jsonc @@ -1,23 +1,23 @@ // Ultracite oxfmt Configuration // https://oxc.rs/docs/guide/usage/formatter/config-file-reference.html { - "$schema": "./node_modules/oxfmt/configuration_schema.json", - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "semi": true, - "singleQuote": false, - "quoteProps": "as-needed", - "jsxSingleQuote": false, - "trailingComma": "es5", - "bracketSpacing": true, - "bracketSameLine": false, - "arrowParens": "always", - "endOfLine": "lf", - "experimentalSortPackageJson": true, - "experimentalSortImports": { - "ignoreCase": true, - "newlinesBetween": true, - "order": "asc", - }, + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 80, + "tabWidth": 2, + "useTabs": true, + "semi": true, + "singleQuote": false, + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "endOfLine": "lf", + "experimentalSortPackageJson": true, + "experimentalSortImports": { + "ignoreCase": true, + "newlinesBetween": true, + "order": "asc", + }, } diff --git a/.oxlintrc.json b/.oxlintrc.json index 076354e..c5a5e50 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,4 +1,4 @@ { - "$schema": "./node_modules/oxlint/configuration_schema.json", - "extends": ["./node_modules/ultracite/config/oxlint/core/.oxlintrc.json"] + "$schema": "./node_modules/oxlint/configuration_schema.json", + "extends": ["./node_modules/ultracite/config/oxlint/core/.oxlintrc.json"] } diff --git a/README.md b/README.md index c0a2eea..e11e829 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ You can also run it without global install via `npx readie`. ```json { - "banner": "

{{title}}

", - "footer": "https://example.com?ref={{packageNameEncoded}}" + "banner": "

{{title}}

", + "footer": "https://example.com?ref={{packageNameEncoded}}" } ``` diff --git a/examples/basic/readie.json b/examples/basic/readie.json index 7c76ce4..536a122 100644 --- a/examples/basic/readie.json +++ b/examples/basic/readie.json @@ -1,21 +1,21 @@ { - "$schema": "https://unpkg.com/readie/schemas/readie.schema.json", - "version": "1", - "title": "Acme Toolkit", - "description": "A TypeScript toolkit for building internal tools.", - "includeTableOfContents": true, - "badges": [ - { - "label": "npm", - "image": "https://img.shields.io/npm/v/acme-toolkit.svg", - "link": "https://www.npmjs.com/package/acme-toolkit" - } - ], - "features": ["Typed APIs", "Fast local setup", "Works in Node.js and Bun"], - "installation": ["```bash\nnpm install acme-toolkit\n```"], - "usage": [ - "Import the toolkit and initialize your client.", - "```ts\nimport { createClient } from 'acme-toolkit'\n\nconst client = createClient()\n```" - ], - "support": ["Open an issue in your repository tracker."] + "$schema": "https://unpkg.com/readie/schemas/readie.schema.json", + "version": "1", + "title": "Acme Toolkit", + "description": "A TypeScript toolkit for building internal tools.", + "includeTableOfContents": true, + "badges": [ + { + "label": "npm", + "image": "https://img.shields.io/npm/v/acme-toolkit.svg", + "link": "https://www.npmjs.com/package/acme-toolkit" + } + ], + "features": ["Typed APIs", "Fast local setup", "Works in Node.js and Bun"], + "installation": ["```bash\nnpm install acme-toolkit\n```"], + "usage": [ + "Import the toolkit and initialize your client.", + "```ts\nimport { createClient } from 'acme-toolkit'\n\nconst client = createClient()\n```" + ], + "support": ["Open an issue in your repository tracker."] } diff --git a/examples/c15t/README.md b/examples/c15t/README.md index a0ee31a..816b3cb 100644 --- a/examples/c15t/README.md +++ b/examples/c15t/README.md @@ -77,12 +77,12 @@ To manually install, follow the guide in our [docs – manual setup](https://c15 import { ConsentManagerProvider, CookieBanner } from "@c15t/react"; function App() { - return ( - - - - - ); + return ( + + + + + ); } ``` diff --git a/examples/c15t/old.md b/examples/c15t/old.md index a0ee31a..816b3cb 100644 --- a/examples/c15t/old.md +++ b/examples/c15t/old.md @@ -77,12 +77,12 @@ To manually install, follow the guide in our [docs – manual setup](https://c15 import { ConsentManagerProvider, CookieBanner } from "@c15t/react"; function App() { - return ( - - - - - ); + return ( + + + + + ); } ``` diff --git a/examples/c15t/readie.global.json b/examples/c15t/readie.global.json index 289d8e3..929e860 100644 --- a/examples/c15t/readie.global.json +++ b/examples/c15t/readie.global.json @@ -1,67 +1,67 @@ { - "$schema": "../../schemas/readie.global.schema.json", - "banner": "

\n \n \n \n \"c15t\n \n \n
\n

{{title}}

\n

", - "badges": [ - { - "label": "GitHub stars", - "image": "https://img.shields.io/github/stars/c15t/c15t?style=flat-square", - "link": "https://github.com/c15t/c15t" - }, - { - "label": "CI", - "image": "https://img.shields.io/github/actions/workflow/status/c15t/c15t/ci.yml?style=flat-square", - "link": "https://github.com/c15t/c15t/actions/workflows/ci.yml" - }, - { - "label": "License", - "image": "https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square", - "link": "https://github.com/c15t/c15t/blob/main/LICENSE.md" - }, - { - "label": "Discord", - "image": "https://img.shields.io/discord/1312171102268690493?style=flat-square", - "link": "https://c15t.link/discord" - }, - { - "label": "npm version", - "image": "https://img.shields.io/npm/v/%40c15t%2Freact?style=flat-square", - "link": "https://www.npmjs.com/package/@c15t/react" - }, - { - "label": "Top Language", - "image": "https://img.shields.io/github/languages/top/c15t/c15t?style=flat-square", - "link": "https://github.com/c15t/c15t" - }, - { - "label": "Last Commit", - "image": "https://img.shields.io/github/last-commit/c15t/c15t?style=flat-square", - "link": "https://github.com/c15t/c15t/commits/main" - }, - { - "label": "Open Issues", - "image": "https://img.shields.io/github/issues/c15t/c15t?style=flat-square", - "link": "https://github.com/c15t/c15t/issues" - } - ], - "support": [ - "Join our [Discord community](https://c15t.link/discord)", - "Open an issue on our [GitHub repository](https://github.com/c15t/c15t/issues)", - "Visit [consent.io](https://consent.io) and use the chat widget", - "Contact our support team via email [support@consent.io](mailto:support@consent.io)" - ], - "contributing": [ - "We're open to all community contributions!", - "Read our [Contribution Guidelines](https://c15t.com/docs/oss/contributing)", - "Review our [Code of Conduct](https://c15t.com/docs/oss/code-of-conduct)", - "Fork the repository", - "Create a new branch for your feature", - "Submit a pull request", - "**All contributions, big or small, are welcome and appreciated!**" - ], - "security": "If you believe you have found a security vulnerability in c15t, we encourage you to **_responsibly disclose this and NOT open a public issue_**. We will investigate all legitimate reports.\n\nOur preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software. To do this, please visit [https://github.com/c15t/c15t/security](https://github.com/c15t/c15t/security) and click the \"Report a vulnerability\" button.\n\n### Security Policy\n\n- Please do not share security vulnerabilities in public forums, issues, or pull requests\n- Provide detailed information about the potential vulnerability\n- Allow reasonable time for us to address the issue before any public disclosure\n- We are committed to addressing security concerns promptly and transparently", - "license": { - "name": "GNU General Public License v3.0", - "url": "https://github.com/c15t/c15t/blob/main/LICENSE.md" - }, - "footer": "---\n\n**Built with ❤️ by the [consent.io](https://www.consent.io?utm_source=github&utm_medium=repopage_{{packageNameEncoded}}) team**" + "$schema": "../../schemas/readie.global.schema.json", + "banner": "

\n \n \n \n \"c15t\n \n \n
\n

{{title}}

\n

", + "badges": [ + { + "label": "GitHub stars", + "image": "https://img.shields.io/github/stars/c15t/c15t?style=flat-square", + "link": "https://github.com/c15t/c15t" + }, + { + "label": "CI", + "image": "https://img.shields.io/github/actions/workflow/status/c15t/c15t/ci.yml?style=flat-square", + "link": "https://github.com/c15t/c15t/actions/workflows/ci.yml" + }, + { + "label": "License", + "image": "https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square", + "link": "https://github.com/c15t/c15t/blob/main/LICENSE.md" + }, + { + "label": "Discord", + "image": "https://img.shields.io/discord/1312171102268690493?style=flat-square", + "link": "https://c15t.link/discord" + }, + { + "label": "npm version", + "image": "https://img.shields.io/npm/v/%40c15t%2Freact?style=flat-square", + "link": "https://www.npmjs.com/package/@c15t/react" + }, + { + "label": "Top Language", + "image": "https://img.shields.io/github/languages/top/c15t/c15t?style=flat-square", + "link": "https://github.com/c15t/c15t" + }, + { + "label": "Last Commit", + "image": "https://img.shields.io/github/last-commit/c15t/c15t?style=flat-square", + "link": "https://github.com/c15t/c15t/commits/main" + }, + { + "label": "Open Issues", + "image": "https://img.shields.io/github/issues/c15t/c15t?style=flat-square", + "link": "https://github.com/c15t/c15t/issues" + } + ], + "support": [ + "Join our [Discord community](https://c15t.link/discord)", + "Open an issue on our [GitHub repository](https://github.com/c15t/c15t/issues)", + "Visit [consent.io](https://consent.io) and use the chat widget", + "Contact our support team via email [support@consent.io](mailto:support@consent.io)" + ], + "contributing": [ + "We're open to all community contributions!", + "Read our [Contribution Guidelines](https://c15t.com/docs/oss/contributing)", + "Review our [Code of Conduct](https://c15t.com/docs/oss/code-of-conduct)", + "Fork the repository", + "Create a new branch for your feature", + "Submit a pull request", + "**All contributions, big or small, are welcome and appreciated!**" + ], + "security": "If you believe you have found a security vulnerability in c15t, we encourage you to **_responsibly disclose this and NOT open a public issue_**. We will investigate all legitimate reports.\n\nOur preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software. To do this, please visit [https://github.com/c15t/c15t/security](https://github.com/c15t/c15t/security) and click the \"Report a vulnerability\" button.\n\n### Security Policy\n\n- Please do not share security vulnerabilities in public forums, issues, or pull requests\n- Provide detailed information about the potential vulnerability\n- Allow reasonable time for us to address the issue before any public disclosure\n- We are committed to addressing security concerns promptly and transparently", + "license": { + "name": "GNU General Public License v3.0", + "url": "https://github.com/c15t/c15t/blob/main/LICENSE.md" + }, + "footer": "---\n\n**Built with ❤️ by the [consent.io](https://www.consent.io?utm_source=github&utm_medium=repopage_{{packageNameEncoded}}) team**" } diff --git a/examples/c15t/readie.json b/examples/c15t/readie.json index d2b1c47..aa4a948 100644 --- a/examples/c15t/readie.json +++ b/examples/c15t/readie.json @@ -1,38 +1,38 @@ { - "$schema": "../../schemas/readie.schema.json", - "title": "@c15t/react: React Consent Components", - "description": "Developer-first CMP for React: cookie banner, consent manager, preferences centre. GDPR ready with minimal setup and rich customization", - "includeTableOfContents": false, - "features": [ - "Works with React 19, 18, 17, and 16.8", - "Full 'use client' support for React Server Components", - "Headless and fully customizable UI components", - "Automatic GDPR compliance", - "Minimal configuration with TypeScript-first design", - "Comprehensive Consent Management Platform (CMP)", - "Flexible Cookie Banner and Preference Center components", - "Built-in internationalization support", - "Seamless consent storage and tracking" - ], - "prerequisites": [ - "React 16.8 or later", - "Node.js 18.17.0 or later", - "A hosted [c15t instance](https://consent.io) (free sign-up) or [self-hosted deployment](https://c15t.com/docs/self-host/v2)" - ], - "manualInstallation": [ - "", - "```bash\npnpm add @c15t/react\n```", - "", - "To manually install, follow the guide in our [docs – manual setup](https://c15t.com/docs/frameworks/react/quickstart#manual-setup)." - ], - "quickStart": "Easiest setup with @c15t/cli:\n\n```bash\n# Generate schema and code\npnpm dlx @c15t/cli generate\n# Alternatives:\n# npx @c15t/cli generate\n# bunx --bun @c15t/cli generate\n```\n\nThe CLI will:\n\n- Install necessary packages\n- Configure your c15t instance\n- Set up environment variables\n- Add consent management components to your app", - "usage": [ - "Import `ConsentManagerProvider` in your app's root component", - "Add `CookieBanner` and `PreferenceCenter` components", - "Customise styling and behaviour to fit your app", - "For full implementation details, see the [React quickstart docs](https://c15t.com/docs/frameworks/react/quickstart)", + "$schema": "../../schemas/readie.schema.json", + "title": "@c15t/react: React Consent Components", + "description": "Developer-first CMP for React: cookie banner, consent manager, preferences centre. GDPR ready with minimal setup and rich customization", + "includeTableOfContents": false, + "features": [ + "Works with React 19, 18, 17, and 16.8", + "Full 'use client' support for React Server Components", + "Headless and fully customizable UI components", + "Automatic GDPR compliance", + "Minimal configuration with TypeScript-first design", + "Comprehensive Consent Management Platform (CMP)", + "Flexible Cookie Banner and Preference Center components", + "Built-in internationalization support", + "Seamless consent storage and tracking" + ], + "prerequisites": [ + "React 16.8 or later", + "Node.js 18.17.0 or later", + "A hosted [c15t instance](https://consent.io) (free sign-up) or [self-hosted deployment](https://c15t.com/docs/self-host/v2)" + ], + "manualInstallation": [ + "", + "```bash\npnpm add @c15t/react\n```", + "", + "To manually install, follow the guide in our [docs – manual setup](https://c15t.com/docs/frameworks/react/quickstart#manual-setup)." + ], + "quickStart": "Easiest setup with @c15t/cli:\n\n```bash\n# Generate schema and code\npnpm dlx @c15t/cli generate\n# Alternatives:\n# npx @c15t/cli generate\n# bunx --bun @c15t/cli generate\n```\n\nThe CLI will:\n\n- Install necessary packages\n- Configure your c15t instance\n- Set up environment variables\n- Add consent management components to your app", + "usage": [ + "Import `ConsentManagerProvider` in your app's root component", + "Add `CookieBanner` and `PreferenceCenter` components", + "Customise styling and behaviour to fit your app", + "For full implementation details, see the [React quickstart docs](https://c15t.com/docs/frameworks/react/quickstart)", - "```tsx\n// App.tsx\nimport { ConsentManagerProvider, CookieBanner } from '@c15t/react'\n\nfunction App() {\n return (\n \n \n \n \n )\n}\n```" - ], - "docsLink": "https://c15t.com/docs/frameworks/react/quickstart" + "```tsx\n// App.tsx\nimport { ConsentManagerProvider, CookieBanner } from '@c15t/react'\n\nfunction App() {\n return (\n \n \n \n \n )\n}\n```" + ], + "docsLink": "https://c15t.com/docs/frameworks/react/quickstart" } diff --git a/package.json b/package.json index f7fd572..3f1c7ac 100644 --- a/package.json +++ b/package.json @@ -1,76 +1,76 @@ { - "name": "readie", - "version": "0.0.1", - "description": "Generate high-quality README files from a validated JSON config.", - "keywords": [ - "cli", - "documentation", - "generator", - "markdown", - "readme" - ], - "homepage": "https://github.com/consentdotio/readie", - "bugs": { - "url": "https://github.com/consentdotio/readie/issues" - }, - "license": "MIT", - "author": "Christopher Burns", - "repository": { - "type": "git", - "url": "git+https://github.com/consentdotio/readie.git" - }, - "bin": { - "readie": "dist/index.js" - }, - "files": [ - "dist", - "schemas", - "README.md", - "LICENSE", - "CHANGELOG.md", - "CONTRIBUTING.md", - "CODE_OF_CONDUCT.md", - "SECURITY.md" - ], - "type": "module", - "main": "dist/index.js", - "imports": { - "#src/*": "./src/*" - }, - "scripts": { - "build": "bun build src/index.ts --outdir dist --target node --minify", - "clean": "bun --eval \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true });\"", - "dev": "bun run src/index.ts", - "test": "bunx vitest run", - "test:watch": "bunx vitest", - "typecheck": "bunx tsc -p tsconfig.json --noEmit", - "prepare": "lefthook install", - "changeset": "changeset", - "check": "ultracite check", - "fix": "ultracite fix" - }, - "dependencies": { - "@effect/cli": "^0.73.2", - "@effect/platform": "^0.94.5", - "@effect/platform-node": "^0.104.1", - "@effect/printer": "^0.47.0", - "@effect/printer-ansi": "^0.47.0", - "effect": "^3.19.18" - }, - "devDependencies": { - "@changesets/changelog-github": "^0.5.2", - "@changesets/cli": "^2.29.8", - "@types/fs-extra": "^11.0.4", - "@types/node": "^25.3.0", - "changeset-conventional-commits": "^0.2.5", - "fs-extra": "^11.3.3", - "lefthook": "^2.1.1", - "oxfmt": "^0.34.0", - "oxlint": "^1.49.0", - "pathe": "^2.0.3", - "tempy": "^3.2.0", - "typescript": "^5.9.3", - "ultracite": "7.2.3", - "vitest": "^4.0.18" - } + "name": "readie", + "version": "0.0.1", + "description": "Generate high-quality README files from a validated JSON config.", + "keywords": [ + "cli", + "documentation", + "generator", + "markdown", + "readme" + ], + "homepage": "https://github.com/consentdotio/readie", + "bugs": { + "url": "https://github.com/consentdotio/readie/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/consentdotio/readie.git" + }, + "license": "MIT", + "author": "Christopher Burns ", + "type": "module", + "imports": { + "#src/*": "./src/*" + }, + "main": "dist/index.js", + "bin": { + "readie": "dist/index.js" + }, + "files": [ + "dist", + "schemas", + "README.md", + "LICENSE", + "CHANGELOG.md", + "CONTRIBUTING.md", + "CODE_OF_CONDUCT.md", + "SECURITY.md" + ], + "scripts": { + "build": "bun build src/index.ts --outdir dist --target node --minify", + "changeset": "changeset", + "check": "ultracite check", + "clean": "bun --eval \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true });\"", + "dev": "bun run src/index.ts", + "fix": "ultracite fix", + "prepare": "lefthook install", + "test": "bunx vitest run", + "test:watch": "bunx vitest", + "typecheck": "bunx tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@effect/cli": "^0.73.2", + "@effect/platform": "^0.94.5", + "@effect/platform-node": "^0.104.1", + "@effect/printer": "^0.47.0", + "@effect/printer-ansi": "^0.47.0", + "effect": "^3.19.18" + }, + "devDependencies": { + "@changesets/changelog-github": "^0.5.2", + "@changesets/cli": "^2.29.8", + "@types/fs-extra": "^11.0.4", + "@types/node": "^25.3.0", + "changeset-conventional-commits": "^0.2.5", + "fs-extra": "^11.3.3", + "lefthook": "^2.1.1", + "oxfmt": "^0.34.0", + "oxlint": "^1.49.0", + "pathe": "^2.0.3", + "tempy": "^3.2.0", + "typescript": "^5.9.3", + "ultracite": "7.2.3", + "vitest": "^4.0.18" + } } diff --git a/readie.json b/readie.json index c84c998..aebf0de 100644 --- a/readie.json +++ b/readie.json @@ -1,66 +1,66 @@ { - "$schema": "https://unpkg.com/readie/schemas/readie.schema.json", - "version": "1", - "title": "Readie", - "description": "Developer-first CLI for generating polished, consistent README files from a simple config.", - "includeTableOfContents": true, - "features": [ - "Generate a single project README or many READMEs across a workspace", - "Schema-backed configuration with editor autocomplete and validation", - "Composable sections for installation, quick start, commands, flags, and more", - "Supports custom markdown sections, badges, and project-specific content", - "Easy onboarding with starter config generation", - "Dry-run and strict workspace options for safe large-scale updates" - ], - "prerequisites": [ - "Node.js 18 or later", - "npm, pnpm, or yarn", - "A project with a `readie.json` config file" - ], - "manualInstallation": [ - "", - "```bash\nnpm install -g readie\n```", - "", - "You can also run it without global install via `npx readie`." - ], - "quickStart": "Create a starter config and generate your README in minutes:\n\n```bash\n# 1) Initialize a config\nnpx readie init\n\n# 2) Generate README.md from readie.json\nnpx readie\n```\n\nFor monorepos/workspaces:\n\n```bash\nnpx readie generate:workspace --root ./packages --config-name readie.json\n```\n\nThis workflow helps teams keep README files consistent while still allowing per-project customization.", - "usage": [ - "Create a config file with `npx readie init`.", - "Run `npx readie` (or `npx readie generate`) to generate one README.", - "Use `npx readie generate:workspace --root ./packages` to generate for multiple packages.", - "Use `--dry-run` to preview changes and `--strict` to fail CI on generation errors.", - "Extend generated docs with rich markdown via `quickStart`, `customSections`, and `footer`.", - "Use placeholders in top-level strings of `readie.global.json`: `{{title}}`, `{{packageName}}`, and `{{packageNameEncoded}}`.", - "```json\n{\n \"banner\": \"

{{title}}

\",\n \"footer\": \"https://example.com?ref={{packageNameEncoded}}\"\n}\n```", - "```bash\n# Single project\nnpx readie --config ./readie.json\n\n# Workspace with package filtering\nnpx readie generate:workspace --root ./packages --package ui --package api --dry-run\n```" - ], - "commands": [ - { - "name": "readie", - "description": "Generate a README from the local readie.json (default command)." - }, - { - "name": "readie generate", - "description": "Explicit single-project generation command." - }, - { - "name": "readie generate:workspace", - "description": "Generate READMEs for multiple projects in a workspace." - }, - { - "name": "readie init", - "description": "Create a starter readie.json in the current directory." - } - ], - "globalFlags": [ - { - "flag": "--help, -h", - "description": "Show command help." - }, - { - "flag": "--config, -c", - "description": "Set a custom config path for supported commands." - } - ], - "docsLink": "https://github.com/readie-cli/readie" + "$schema": "https://unpkg.com/readie/schemas/readie.schema.json", + "version": "1", + "title": "Readie", + "description": "Developer-first CLI for generating polished, consistent README files from a simple config.", + "includeTableOfContents": true, + "features": [ + "Generate a single project README or many READMEs across a workspace", + "Schema-backed configuration with editor autocomplete and validation", + "Composable sections for installation, quick start, commands, flags, and more", + "Supports custom markdown sections, badges, and project-specific content", + "Easy onboarding with starter config generation", + "Dry-run and strict workspace options for safe large-scale updates" + ], + "prerequisites": [ + "Node.js 18 or later", + "npm, pnpm, or yarn", + "A project with a `readie.json` config file" + ], + "manualInstallation": [ + "", + "```bash\nnpm install -g readie\n```", + "", + "You can also run it without global install via `npx readie`." + ], + "quickStart": "Create a starter config and generate your README in minutes:\n\n```bash\n# 1) Initialize a config\nnpx readie init\n\n# 2) Generate README.md from readie.json\nnpx readie\n```\n\nFor monorepos/workspaces:\n\n```bash\nnpx readie generate:workspace --root ./packages --config-name readie.json\n```\n\nThis workflow helps teams keep README files consistent while still allowing per-project customization.", + "usage": [ + "Create a config file with `npx readie init`.", + "Run `npx readie` (or `npx readie generate`) to generate one README.", + "Use `npx readie generate:workspace --root ./packages` to generate for multiple packages.", + "Use `--dry-run` to preview changes and `--strict` to fail CI on generation errors.", + "Extend generated docs with rich markdown via `quickStart`, `customSections`, and `footer`.", + "Use placeholders in top-level strings of `readie.global.json`: `{{title}}`, `{{packageName}}`, and `{{packageNameEncoded}}`.", + "```json\n{\n \"banner\": \"

{{title}}

\",\n \"footer\": \"https://example.com?ref={{packageNameEncoded}}\"\n}\n```", + "```bash\n# Single project\nnpx readie --config ./readie.json\n\n# Workspace with package filtering\nnpx readie generate:workspace --root ./packages --package ui --package api --dry-run\n```" + ], + "commands": [ + { + "name": "readie", + "description": "Generate a README from the local readie.json (default command)." + }, + { + "name": "readie generate", + "description": "Explicit single-project generation command." + }, + { + "name": "readie generate:workspace", + "description": "Generate READMEs for multiple projects in a workspace." + }, + { + "name": "readie init", + "description": "Create a starter readie.json in the current directory." + } + ], + "globalFlags": [ + { + "flag": "--help, -h", + "description": "Show command help." + }, + { + "flag": "--config, -c", + "description": "Set a custom config path for supported commands." + } + ], + "docsLink": "https://github.com/readie-cli/readie" } diff --git a/schemas/readie.global.schema.json b/schemas/readie.global.schema.json index b1a7872..7edc188 100644 --- a/schemas/readie.global.schema.json +++ b/schemas/readie.global.schema.json @@ -1,141 +1,141 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://unpkg.com/readie/schemas/readie.global.schema.json", - "title": "Readie Global Config", - "description": "Global defaults for README generation that can be merged into project readie.json files.", - "type": "object", - "additionalProperties": false, - "properties": { - "$schema": { - "type": "string", - "description": "Optional JSON Schema URL for editor integration." - }, - "version": { - "type": "string", - "description": "Config version. Current value is 1.", - "default": "1", - "enum": ["1"] - }, - "title": { - "type": "string", - "minLength": 1 - }, - "description": { - "type": "string", - "minLength": 1 - }, - "output": { - "type": "string", - "description": "Output README path. Relative paths are resolved from the project config file directory." - }, - "includeTableOfContents": { - "type": "boolean", - "default": true - }, - "features": { - "type": "array", - "items": { "type": "string" } - }, - "prerequisites": { - "type": "array", - "items": { "type": "string" } - }, - "installation": { - "type": "array", - "items": { "type": "string" } - }, - "manualInstallation": { - "type": "array", - "items": { "type": "string" } - }, - "usage": { - "type": "array", - "items": { "type": "string" } - }, - "commands": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["name", "description"], - "properties": { - "name": { "type": "string", "minLength": 1 }, - "description": { "type": "string", "minLength": 1 } - } - } - }, - "globalFlags": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["flag", "description"], - "properties": { - "flag": { "type": "string", "minLength": 1 }, - "description": { "type": "string", "minLength": 1 } - } - } - }, - "badges": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["label", "image"], - "properties": { - "label": { "type": "string", "minLength": 1 }, - "image": { "type": "string", "minLength": 1 }, - "link": { "type": "string", "minLength": 1 } - } - } - }, - "banner": { - "type": "string", - "description": "Raw markdown or HTML rendered before the title. Supports {{title}}, {{packageName}}, and {{packageNameEncoded}} interpolation." - }, - "quickStart": { - "type": "string", - "description": "Raw markdown rendered in a Quick Start section." - }, - "support": { - "type": "array", - "items": { "type": "string" } - }, - "contributing": { - "type": "array", - "items": { "type": "string" } - }, - "security": { - "type": "string" - }, - "license": { - "description": "License section content as markdown text or a named URL object.", - "oneOf": [ - { "type": "string", "minLength": 1 }, - { - "type": "object", - "additionalProperties": false, - "required": ["name", "url"], - "properties": { - "name": { "type": "string", "minLength": 1 }, - "url": { "type": "string", "minLength": 1 } - } - } - ] - }, - "footer": { - "type": "string", - "description": "Raw markdown appended to the end of the README. Supports {{title}}, {{packageName}}, and {{packageNameEncoded}} interpolation." - }, - "docsLink": { - "type": "string" - }, - "quickStartLink": { - "type": "string" - }, - "customSections": { - "type": "object", - "additionalProperties": { "type": "string" } - } - } + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://unpkg.com/readie/schemas/readie.global.schema.json", + "title": "Readie Global Config", + "description": "Global defaults for README generation that can be merged into project readie.json files.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "description": "Optional JSON Schema URL for editor integration." + }, + "version": { + "type": "string", + "description": "Config version. Current value is 1.", + "default": "1", + "enum": ["1"] + }, + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "output": { + "type": "string", + "description": "Output README path. Relative paths are resolved from the project config file directory." + }, + "includeTableOfContents": { + "type": "boolean", + "default": true + }, + "features": { + "type": "array", + "items": { "type": "string" } + }, + "prerequisites": { + "type": "array", + "items": { "type": "string" } + }, + "installation": { + "type": "array", + "items": { "type": "string" } + }, + "manualInstallation": { + "type": "array", + "items": { "type": "string" } + }, + "usage": { + "type": "array", + "items": { "type": "string" } + }, + "commands": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "description"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 } + } + } + }, + "globalFlags": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["flag", "description"], + "properties": { + "flag": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 } + } + } + }, + "badges": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["label", "image"], + "properties": { + "label": { "type": "string", "minLength": 1 }, + "image": { "type": "string", "minLength": 1 }, + "link": { "type": "string", "minLength": 1 } + } + } + }, + "banner": { + "type": "string", + "description": "Raw markdown or HTML rendered before the title. Supports {{title}}, {{packageName}}, and {{packageNameEncoded}} interpolation." + }, + "quickStart": { + "type": "string", + "description": "Raw markdown rendered in a Quick Start section." + }, + "support": { + "type": "array", + "items": { "type": "string" } + }, + "contributing": { + "type": "array", + "items": { "type": "string" } + }, + "security": { + "type": "string" + }, + "license": { + "description": "License section content as markdown text or a named URL object.", + "oneOf": [ + { "type": "string", "minLength": 1 }, + { + "type": "object", + "additionalProperties": false, + "required": ["name", "url"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "url": { "type": "string", "minLength": 1 } + } + } + ] + }, + "footer": { + "type": "string", + "description": "Raw markdown appended to the end of the README. Supports {{title}}, {{packageName}}, and {{packageNameEncoded}} interpolation." + }, + "docsLink": { + "type": "string" + }, + "quickStartLink": { + "type": "string" + }, + "customSections": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } } diff --git a/schemas/readie.schema.json b/schemas/readie.schema.json index 66b75f5..0207b89 100644 --- a/schemas/readie.schema.json +++ b/schemas/readie.schema.json @@ -1,157 +1,157 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://unpkg.com/readie/schemas/readie.schema.json", - "title": "Readie Config", - "description": "Configuration for generating README files with Readie.", - "type": "object", - "additionalProperties": false, - "required": ["title", "description"], - "properties": { - "$schema": { - "type": "string", - "description": "Optional JSON Schema URL for editor integration." - }, - "version": { - "type": "string", - "description": "Config version. Current value is 1.", - "default": "1", - "enum": ["1"] - }, - "title": { - "type": "string", - "minLength": 1 - }, - "description": { - "type": "string", - "minLength": 1 - }, - "output": { - "type": "string", - "description": "Output README path. Relative paths are resolved from the config file directory." - }, - "includeTableOfContents": { - "type": "boolean", - "default": true - }, - "features": { - "type": "array", - "items": { "type": "string" } - }, - "prerequisites": { - "type": "array", - "items": { "type": "string" } - }, - "installation": { - "type": "array", - "items": { "type": "string" } - }, - "manualInstallation": { - "type": "array", - "items": { "type": "string" } - }, - "usage": { - "type": "array", - "items": { "type": "string" } - }, - "commands": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["name", "description"], - "properties": { - "name": { "type": "string", "minLength": 1 }, - "description": { "type": "string", "minLength": 1 } - } - } - }, - "globalFlags": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["flag", "description"], - "properties": { - "flag": { "type": "string", "minLength": 1 }, - "description": { "type": "string", "minLength": 1 } - } - } - }, - "badges": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["label", "image"], - "properties": { - "label": { "type": "string", "minLength": 1 }, - "image": { "type": "string", "minLength": 1 }, - "link": { "type": "string", "minLength": 1 } - } - } - }, - "banner": { - "type": "string", - "description": "Raw markdown or HTML rendered before the title. In readie.global.json, top-level string values support `{{title}}`, `{{packageName}}`, and `{{packageNameEncoded}}` interpolation." - }, - "quickStart": { - "type": "string", - "description": "Raw markdown rendered in a Quick Start section." - }, - "support": { - "type": "array", - "items": { "type": "string" } - }, - "contributing": { - "type": "array", - "items": { "type": "string" } - }, - "security": { - "type": "string" - }, - "license": { - "description": "License section content as markdown text or a named URL object.", - "oneOf": [ - { "type": "string", "minLength": 1 }, - { - "type": "object", - "additionalProperties": false, - "required": ["name", "url"], - "properties": { - "name": { "type": "string", "minLength": 1 }, - "url": { "type": "string", "minLength": 1 } - } - } - ] - }, - "footer": { - "type": "string", - "description": "Raw markdown appended to the end of the README." - }, - "docsLink": { - "type": "string" - }, - "quickStartLink": { - "type": "string" - }, - "customSections": { - "type": "object", - "additionalProperties": { "type": "string" } - } - }, - "examples": [ - { - "$schema": "https://unpkg.com/readie/schemas/readie.schema.json", - "version": "1", - "title": "my-library", - "description": "A concise project description.", - "includeTableOfContents": true, - "features": ["Fast", "Typed", "Zero-config"], - "installation": ["```bash\\nnpm i my-library\\n```"], - "usage": ["Import and use the library in your project."], - "commands": [ - { "name": "my-cli init", "description": "Initialize project files." } - ] - } - ] + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://unpkg.com/readie/schemas/readie.schema.json", + "title": "Readie Config", + "description": "Configuration for generating README files with Readie.", + "type": "object", + "additionalProperties": false, + "required": ["title", "description"], + "properties": { + "$schema": { + "type": "string", + "description": "Optional JSON Schema URL for editor integration." + }, + "version": { + "type": "string", + "description": "Config version. Current value is 1.", + "default": "1", + "enum": ["1"] + }, + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "output": { + "type": "string", + "description": "Output README path. Relative paths are resolved from the config file directory." + }, + "includeTableOfContents": { + "type": "boolean", + "default": true + }, + "features": { + "type": "array", + "items": { "type": "string" } + }, + "prerequisites": { + "type": "array", + "items": { "type": "string" } + }, + "installation": { + "type": "array", + "items": { "type": "string" } + }, + "manualInstallation": { + "type": "array", + "items": { "type": "string" } + }, + "usage": { + "type": "array", + "items": { "type": "string" } + }, + "commands": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "description"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 } + } + } + }, + "globalFlags": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["flag", "description"], + "properties": { + "flag": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 } + } + } + }, + "badges": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["label", "image"], + "properties": { + "label": { "type": "string", "minLength": 1 }, + "image": { "type": "string", "minLength": 1 }, + "link": { "type": "string", "minLength": 1 } + } + } + }, + "banner": { + "type": "string", + "description": "Raw markdown or HTML rendered before the title. In readie.global.json, top-level string values support `{{title}}`, `{{packageName}}`, and `{{packageNameEncoded}}` interpolation." + }, + "quickStart": { + "type": "string", + "description": "Raw markdown rendered in a Quick Start section." + }, + "support": { + "type": "array", + "items": { "type": "string" } + }, + "contributing": { + "type": "array", + "items": { "type": "string" } + }, + "security": { + "type": "string" + }, + "license": { + "description": "License section content as markdown text or a named URL object.", + "oneOf": [ + { "type": "string", "minLength": 1 }, + { + "type": "object", + "additionalProperties": false, + "required": ["name", "url"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "url": { "type": "string", "minLength": 1 } + } + } + ] + }, + "footer": { + "type": "string", + "description": "Raw markdown appended to the end of the README." + }, + "docsLink": { + "type": "string" + }, + "quickStartLink": { + "type": "string" + }, + "customSections": { + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "examples": [ + { + "$schema": "https://unpkg.com/readie/schemas/readie.schema.json", + "version": "1", + "title": "my-library", + "description": "A concise project description.", + "includeTableOfContents": true, + "features": ["Fast", "Typed", "Zero-config"], + "installation": ["```bash\\nnpm i my-library\\n```"], + "usage": ["Import and use the library in your project."], + "commands": [ + { "name": "my-cli init", "description": "Initialize project files." } + ] + } + ] } diff --git a/src/cli/commands/generate-workspace.ts b/src/cli/commands/generate-workspace.ts index 5211d0a..5764cbd 100644 --- a/src/cli/commands/generate-workspace.ts +++ b/src/cli/commands/generate-workspace.ts @@ -3,88 +3,88 @@ import { Effect } from "effect"; import { resolve } from "pathe"; import { - generateWorkspaceReadmes, - parsePackageList, + generateWorkspaceReadmes, + parsePackageList, } from "#src/readme-generator/generator"; interface GenerateWorkspaceCommandArgs { - root: string; - configName: string; - packageValues: string[]; - dryRun: boolean; - strict: boolean; - noGlobal: boolean; + root: string; + configName: string; + packageValues: string[]; + dryRun: boolean; + strict: boolean; + noGlobal: boolean; } export const generateWorkspaceCommand = Command.make( - "generate:workspace", - { - configName: Options.text("config-name").pipe( - Options.withDescription("Config filename to search for in each project"), - Options.withDefault("readie.json") - ), - dryRun: Options.boolean("dry-run").pipe( - Options.withDescription("Show changes without writing files") - ), - noGlobal: Options.boolean("no-global").pipe( - Options.withDescription("Disable readie.global.json discovery and merge") - ), - packageValues: Options.text("package").pipe( - Options.withAlias("p"), - Options.withDescription( - "Project name filter (repeatable, comma-separated supported)" - ), - Options.repeated - ), - root: Options.directory("root").pipe( - Options.withAlias("r"), - Options.withDescription("Workspace root directory"), - Options.withDefault(resolve("./packages")) - ), - strict: Options.boolean("strict").pipe( - Options.withDescription("Exit with code 1 if any project fails") - ), - }, - ({ - root, - configName, - packageValues, - dryRun, - strict, - noGlobal, - }: GenerateWorkspaceCommandArgs) => - Effect.gen(function* runGenerateWorkspaceCommand() { - const result = yield* Effect.tryPromise({ - catch: (error: unknown) => - error instanceof Error - ? error - : new Error(`Workspace generation failed: ${String(error)}`), - try: () => - generateWorkspaceReadmes({ - configName, - dryRun, - packageFilter: parsePackageList(packageValues), - rootDir: root, - useGlobalConfig: !noGlobal, - }), - }); + "generate:workspace", + { + configName: Options.text("config-name").pipe( + Options.withDescription("Config filename to search for in each project"), + Options.withDefault("readie.json") + ), + dryRun: Options.boolean("dry-run").pipe( + Options.withDescription("Show changes without writing files") + ), + noGlobal: Options.boolean("no-global").pipe( + Options.withDescription("Disable readie.global.json discovery and merge") + ), + packageValues: Options.text("package").pipe( + Options.withAlias("p"), + Options.withDescription( + "Project name filter (repeatable, comma-separated supported)" + ), + Options.repeated + ), + root: Options.directory("root").pipe( + Options.withAlias("r"), + Options.withDescription("Workspace root directory"), + Options.withDefault(resolve("./packages")) + ), + strict: Options.boolean("strict").pipe( + Options.withDescription("Exit with code 1 if any project fails") + ), + }, + ({ + root, + configName, + packageValues, + dryRun, + strict, + noGlobal, + }: GenerateWorkspaceCommandArgs) => + Effect.gen(function* runGenerateWorkspaceCommand() { + const result = yield* Effect.tryPromise({ + catch: (error: unknown) => + error instanceof Error + ? error + : new Error(`Workspace generation failed: ${String(error)}`), + try: () => + generateWorkspaceReadmes({ + configName, + dryRun, + packageFilter: parsePackageList(packageValues), + rootDir: root, + useGlobalConfig: !noGlobal, + }), + }); - yield* Effect.sync(() => { - console.log(""); - console.log("Summary"); - console.log(`- Updated: ${result.updated.length}`); - console.log(`- Unchanged: ${result.unchanged.length}`); - console.log(`- Failed: ${result.failed.length}`); - if (result.skippedByFilter.length > 0) { - console.log(`- Skipped by filter: ${result.skippedByFilter.length}`); - } - if (strict && result.failed.length > 0) { - process.exitCode = 1; - } - }); - }) + yield* Effect.sync(() => { + console.log(""); + console.log("Summary"); + console.log(`- Updated: ${result.updated.length}`); + console.log(`- Unchanged: ${result.unchanged.length}`); + console.log(`- Failed: ${result.failed.length}`); + if (result.skippedByFilter.length > 0) { + console.log(`- Skipped by filter: ${result.skippedByFilter.length}`); + } + if (strict && result.failed.length > 0) { + process.exitCode = 1; + } + }); + }) ).pipe( - Command.withDescription( - "Generate READMEs for projects inside a workspace root." - ) + Command.withDescription( + "Generate READMEs for projects inside a workspace root." + ) ); diff --git a/src/cli/commands/generate.ts b/src/cli/commands/generate.ts index 033c95c..8a61db2 100644 --- a/src/cli/commands/generate.ts +++ b/src/cli/commands/generate.ts @@ -4,62 +4,62 @@ import { Effect } from "effect"; import { generateReadmeFromConfig } from "#src/readme-generator/generator"; interface GenerateCommandArgs { - config: string; - output: string; - dryRun: boolean; - noGlobal: boolean; + config: string; + output: string; + dryRun: boolean; + noGlobal: boolean; } const resultStatus = (updated: boolean, dryRun: boolean) => { - if (!updated) { - return "No changes"; - } - return dryRun ? "Would update" : "Generated"; + if (!updated) { + return "No changes"; + } + return dryRun ? "Would update" : "Generated"; }; export const generateCommand = Command.make( - "generate", - { - config: Options.text("config").pipe( - Options.withAlias("c"), - Options.withDescription("Path to readie config file"), - Options.withDefault("./readie.json") - ), - dryRun: Options.boolean("dry-run").pipe( - Options.withDescription("Show changes without writing files") - ), - noGlobal: Options.boolean("no-global").pipe( - Options.withDescription("Disable readie.global.json discovery and merge") - ), - output: Options.text("output").pipe( - Options.withAlias("o"), - Options.withDescription("Optional output path for README"), - Options.withDefault("") - ), - }, - ({ config, output, dryRun, noGlobal }: GenerateCommandArgs) => - Effect.gen(function* runGenerateCommand() { - const result = yield* Effect.tryPromise({ - catch: (error: unknown) => - error instanceof Error - ? error - : new Error(`Generation failed: ${String(error)}`), - try: () => - generateReadmeFromConfig({ - configPath: config, - dryRun, - outputPath: output.trim().length > 0 ? output : undefined, - useGlobalConfig: !noGlobal, - }), - }); + "generate", + { + config: Options.text("config").pipe( + Options.withAlias("c"), + Options.withDescription("Path to readie config file"), + Options.withDefault("./readie.json") + ), + dryRun: Options.boolean("dry-run").pipe( + Options.withDescription("Show changes without writing files") + ), + noGlobal: Options.boolean("no-global").pipe( + Options.withDescription("Disable readie.global.json discovery and merge") + ), + output: Options.text("output").pipe( + Options.withAlias("o"), + Options.withDescription("Optional output path for README"), + Options.withDefault("") + ), + }, + ({ config, output, dryRun, noGlobal }: GenerateCommandArgs) => + Effect.gen(function* runGenerateCommand() { + const result = yield* Effect.tryPromise({ + catch: (error: unknown) => + error instanceof Error + ? error + : new Error(`Generation failed: ${String(error)}`), + try: () => + generateReadmeFromConfig({ + configPath: config, + dryRun, + outputPath: output.trim().length > 0 ? output : undefined, + useGlobalConfig: !noGlobal, + }), + }); - yield* Effect.sync(() => { - const status = resultStatus(result.updated, dryRun); - console.log(`${status}: ${result.outputPath}`); - }); - }) + yield* Effect.sync(() => { + const status = resultStatus(result.updated, dryRun); + console.log(`${status}: ${result.outputPath}`); + }); + }) ).pipe( - Command.withDescription( - "Generate a README from a single readie.json config file." - ) + Command.withDescription( + "Generate a README from a single readie.json config file." + ) ); diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index bb31313..a851be7 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -6,50 +6,50 @@ import { resolve } from "pathe"; import { starterConfigText } from "#src/config/starter-config"; interface InitCommandArgs { - config: string; - force: boolean; + config: string; + force: boolean; } export const initCommand = Command.make( - "init", - { - config: Options.text("config").pipe( - Options.withAlias("c"), - Options.withDescription("Path for generated starter config"), - Options.withDefault("./readie.json") - ), - force: Options.boolean("force").pipe( - Options.withAlias("f"), - Options.withDescription("Overwrite existing config file if it exists") - ), - }, - ({ config, force }: InitCommandArgs) => - Effect.gen(function* runInitCommand() { - const configPath = resolve(config); - const exists = existsSync(configPath); + "init", + { + config: Options.text("config").pipe( + Options.withAlias("c"), + Options.withDescription("Path for generated starter config"), + Options.withDefault("./readie.json") + ), + force: Options.boolean("force").pipe( + Options.withAlias("f"), + Options.withDescription("Overwrite existing config file if it exists") + ), + }, + ({ config, force }: InitCommandArgs) => + Effect.gen(function* runInitCommand() { + const configPath = resolve(config); + const exists = existsSync(configPath); - if (exists && !force) { - yield* Effect.fail( - new Error( - `Config already exists at ${configPath}. Use --force to overwrite.` - ) - ); - } + if (exists && !force) { + yield* Effect.fail( + new Error( + `Config already exists at ${configPath}. Use --force to overwrite.` + ) + ); + } - yield* Effect.tryPromise({ - catch: (error: unknown) => - error instanceof Error - ? error - : new Error(`Failed to write config: ${String(error)}`), - try: () => writeFile(configPath, starterConfigText, "utf8"), - }); + yield* Effect.tryPromise({ + catch: (error: unknown) => + error instanceof Error + ? error + : new Error(`Failed to write config: ${String(error)}`), + try: () => writeFile(configPath, starterConfigText, "utf8"), + }); - yield* Effect.sync(() => { - console.log(`Created starter config: ${configPath}`); - }); - }) + yield* Effect.sync(() => { + console.log(`Created starter config: ${configPath}`); + }); + }) ).pipe( - Command.withDescription( - "Create a starter readie.json file in the current directory." - ) + Command.withDescription( + "Create a starter readie.json file in the current directory." + ) ); diff --git a/src/cli/help.ts b/src/cli/help.ts index 76d55eb..6460134 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -1,5 +1,5 @@ export const printRootHelp = () => { - console.log(`readie + console.log(`readie Generate high-quality README files from readie.json. diff --git a/src/cli/resolve-invocation.ts b/src/cli/resolve-invocation.ts index 5e33c32..19dfdcf 100644 --- a/src/cli/resolve-invocation.ts +++ b/src/cli/resolve-invocation.ts @@ -1,43 +1,43 @@ export type InvocationMode = - | "generate" - | "generate:workspace" - | "init" - | "help" - | "unknown"; + | "generate" + | "generate:workspace" + | "init" + | "help" + | "unknown"; export interface ResolvedInvocation { - mode: InvocationMode; - commandArgs: string[]; - originalArgs: string[]; + mode: InvocationMode; + commandArgs: string[]; + originalArgs: string[]; } const isHelpFlag = (value: string | undefined) => - value === "--help" || value === "-h"; + value === "--help" || value === "-h"; const modeFromToken = (token: string | undefined): InvocationMode => { - if (!token) { - return "generate"; - } - if (isHelpFlag(token) || token === "help") { - return "help"; - } + if (!token) { + return "generate"; + } + if (isHelpFlag(token) || token === "help") { + return "help"; + } - const commandModes: Record = { - generate: "generate", - "generate:workspace": "generate:workspace", - init: "init", - }; + const commandModes: Record = { + generate: "generate", + "generate:workspace": "generate:workspace", + init: "init", + }; - return commandModes[token] ?? "unknown"; + return commandModes[token] ?? "unknown"; }; export const resolveInvocation = (args: string[]): ResolvedInvocation => { - const [first, ...rest] = args; - const mode = modeFromToken(first); + const [first, ...rest] = args; + const mode = modeFromToken(first); - return { - commandArgs: mode === "unknown" ? args : rest, - mode, - originalArgs: args, - }; + return { + commandArgs: mode === "unknown" ? args : rest, + mode, + originalArgs: args, + }; }; diff --git a/src/config/load-config.ts b/src/config/load-config.ts index 30093c7..9b80e04 100644 --- a/src/config/load-config.ts +++ b/src/config/load-config.ts @@ -5,351 +5,351 @@ import { dirname, join, resolve } from "pathe"; import type { ReadieConfig, ReadieGlobalConfig } from "./types"; const commandSchema = Schema.Struct({ - description: Schema.NonEmptyString, - name: Schema.NonEmptyString, + description: Schema.NonEmptyString, + name: Schema.NonEmptyString, }); const globalFlagSchema = Schema.Struct({ - description: Schema.NonEmptyString, - flag: Schema.NonEmptyString, + description: Schema.NonEmptyString, + flag: Schema.NonEmptyString, }); const badgeSchema = Schema.Struct({ - image: Schema.NonEmptyString, - label: Schema.NonEmptyString, - link: Schema.optional(Schema.NonEmptyString), + image: Schema.NonEmptyString, + label: Schema.NonEmptyString, + link: Schema.optional(Schema.NonEmptyString), }); const licenseSchema = Schema.Union( - Schema.NonEmptyString, - Schema.Struct({ - name: Schema.NonEmptyString, - url: Schema.NonEmptyString, - }) + Schema.NonEmptyString, + Schema.Struct({ + name: Schema.NonEmptyString, + url: Schema.NonEmptyString, + }) ); const readieConfigSchema = Schema.Struct({ - $schema: Schema.optional(Schema.String), - badges: Schema.optional(Schema.Array(badgeSchema)), - banner: Schema.optional(Schema.String), - commands: Schema.optional(Schema.Array(commandSchema)), - contributing: Schema.optional(Schema.Array(Schema.String)), - customSections: Schema.optional( - Schema.Record({ - key: Schema.String, - value: Schema.String, - }) - ), - description: Schema.NonEmptyString, - docsLink: Schema.optional(Schema.String), - features: Schema.optional(Schema.Array(Schema.String)), - footer: Schema.optional(Schema.String), - globalFlags: Schema.optional(Schema.Array(globalFlagSchema)), - includeTableOfContents: Schema.optional(Schema.Boolean), - installation: Schema.optional(Schema.Array(Schema.String)), - license: Schema.optional(licenseSchema), - manualInstallation: Schema.optional(Schema.Array(Schema.String)), - output: Schema.optional(Schema.String), - prerequisites: Schema.optional(Schema.Array(Schema.String)), - quickStart: Schema.optional(Schema.String), - quickStartLink: Schema.optional(Schema.String), - security: Schema.optional(Schema.String), - support: Schema.optional(Schema.Array(Schema.String)), - title: Schema.NonEmptyString, - usage: Schema.optional(Schema.Array(Schema.String)), - version: Schema.optional(Schema.Literal("1")), + $schema: Schema.optional(Schema.String), + badges: Schema.optional(Schema.Array(badgeSchema)), + banner: Schema.optional(Schema.String), + commands: Schema.optional(Schema.Array(commandSchema)), + contributing: Schema.optional(Schema.Array(Schema.String)), + customSections: Schema.optional( + Schema.Record({ + key: Schema.String, + value: Schema.String, + }) + ), + description: Schema.NonEmptyString, + docsLink: Schema.optional(Schema.String), + features: Schema.optional(Schema.Array(Schema.String)), + footer: Schema.optional(Schema.String), + globalFlags: Schema.optional(Schema.Array(globalFlagSchema)), + includeTableOfContents: Schema.optional(Schema.Boolean), + installation: Schema.optional(Schema.Array(Schema.String)), + license: Schema.optional(licenseSchema), + manualInstallation: Schema.optional(Schema.Array(Schema.String)), + output: Schema.optional(Schema.String), + prerequisites: Schema.optional(Schema.Array(Schema.String)), + quickStart: Schema.optional(Schema.String), + quickStartLink: Schema.optional(Schema.String), + security: Schema.optional(Schema.String), + support: Schema.optional(Schema.Array(Schema.String)), + title: Schema.NonEmptyString, + usage: Schema.optional(Schema.Array(Schema.String)), + version: Schema.optional(Schema.Literal("1")), }); const readieGlobalConfigSchema = Schema.Struct({ - $schema: Schema.optional(Schema.String), - badges: Schema.optional(Schema.Array(badgeSchema)), - banner: Schema.optional(Schema.String), - commands: Schema.optional(Schema.Array(commandSchema)), - contributing: Schema.optional(Schema.Array(Schema.String)), - customSections: Schema.optional( - Schema.Record({ - key: Schema.String, - value: Schema.String, - }) - ), - description: Schema.optional(Schema.NonEmptyString), - docsLink: Schema.optional(Schema.String), - features: Schema.optional(Schema.Array(Schema.String)), - footer: Schema.optional(Schema.String), - globalFlags: Schema.optional(Schema.Array(globalFlagSchema)), - includeTableOfContents: Schema.optional(Schema.Boolean), - installation: Schema.optional(Schema.Array(Schema.String)), - license: Schema.optional(licenseSchema), - manualInstallation: Schema.optional(Schema.Array(Schema.String)), - output: Schema.optional(Schema.String), - prerequisites: Schema.optional(Schema.Array(Schema.String)), - quickStart: Schema.optional(Schema.String), - quickStartLink: Schema.optional(Schema.String), - security: Schema.optional(Schema.String), - support: Schema.optional(Schema.Array(Schema.String)), - title: Schema.optional(Schema.NonEmptyString), - usage: Schema.optional(Schema.Array(Schema.String)), - version: Schema.optional(Schema.Literal("1")), + $schema: Schema.optional(Schema.String), + badges: Schema.optional(Schema.Array(badgeSchema)), + banner: Schema.optional(Schema.String), + commands: Schema.optional(Schema.Array(commandSchema)), + contributing: Schema.optional(Schema.Array(Schema.String)), + customSections: Schema.optional( + Schema.Record({ + key: Schema.String, + value: Schema.String, + }) + ), + description: Schema.optional(Schema.NonEmptyString), + docsLink: Schema.optional(Schema.String), + features: Schema.optional(Schema.Array(Schema.String)), + footer: Schema.optional(Schema.String), + globalFlags: Schema.optional(Schema.Array(globalFlagSchema)), + includeTableOfContents: Schema.optional(Schema.Boolean), + installation: Schema.optional(Schema.Array(Schema.String)), + license: Schema.optional(licenseSchema), + manualInstallation: Schema.optional(Schema.Array(Schema.String)), + output: Schema.optional(Schema.String), + prerequisites: Schema.optional(Schema.Array(Schema.String)), + quickStart: Schema.optional(Schema.String), + quickStartLink: Schema.optional(Schema.String), + security: Schema.optional(Schema.String), + support: Schema.optional(Schema.Array(Schema.String)), + title: Schema.optional(Schema.NonEmptyString), + usage: Schema.optional(Schema.Array(Schema.String)), + version: Schema.optional(Schema.Literal("1")), }); const decodeReadieConfig = Schema.decodeUnknownSync(readieConfigSchema); const decodeReadieGlobalConfig = Schema.decodeUnknownSync( - readieGlobalConfigSchema + readieGlobalConfigSchema ); const GLOBAL_CONFIG_NAME = "readie.global.json"; const parseJsonFile = async (absolutePath: string): Promise => { - const raw = await readFile(absolutePath, "utf8"); - try { - return JSON.parse(raw); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to parse JSON in ${absolutePath}: ${message}`, { - cause: error, - }); - } + const raw = await readFile(absolutePath, "utf8"); + try { + return JSON.parse(raw); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to parse JSON in ${absolutePath}: ${message}`, { + cause: error, + }); + } }; export const loadReadieConfig = async ( - configPath: string + configPath: string ): Promise => { - const absolutePath = resolve(configPath); - const parsed = await parseJsonFile(absolutePath); - - try { - return decodeReadieConfig(parsed) as ReadieConfig; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error( - `Configuration validation failed for ${absolutePath}\n${message}`, - { cause: error } - ); - } + const absolutePath = resolve(configPath); + const parsed = await parseJsonFile(absolutePath); + + try { + return decodeReadieConfig(parsed) as ReadieConfig; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error( + `Configuration validation failed for ${absolutePath}\n${message}`, + { cause: error } + ); + } }; const loadGlobalReadieConfig = async ( - configPath: string + configPath: string ): Promise => { - const absolutePath = resolve(configPath); - const parsed = await parseJsonFile(absolutePath); - - try { - return decodeReadieGlobalConfig(parsed) as ReadieGlobalConfig; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error( - `Global configuration validation failed for ${absolutePath}\n${message}`, - { cause: error } - ); - } + const absolutePath = resolve(configPath); + const parsed = await parseJsonFile(absolutePath); + + try { + return decodeReadieGlobalConfig(parsed) as ReadieGlobalConfig; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error( + `Global configuration validation failed for ${absolutePath}\n${message}`, + { cause: error } + ); + } }; export const loadGlobalConfig = async ( - startDir: string + startDir: string ): Promise => { - let current = resolve(startDir); - - while (true) { - const candidate = join(current, GLOBAL_CONFIG_NAME); - if (await pathExists(candidate)) { - return await loadGlobalReadieConfig(candidate); - } - - const parent = dirname(current); - if (parent === current) { - return null; - } - current = parent; - } + let current = resolve(startDir); + + while (true) { + const candidate = join(current, GLOBAL_CONFIG_NAME); + if (await pathExists(candidate)) { + return await loadGlobalReadieConfig(candidate); + } + + const parent = dirname(current); + if (parent === current) { + return null; + } + current = parent; + } }; const hasOwn = (obj: object, key: string): boolean => Object.hasOwn(obj, key); interface InterpolationContext { - packageName?: string; + packageName?: string; } const interpolatePlaceholders = ( - value: string, - placeholders: Record + value: string, + placeholders: Record ): string => - value.replaceAll( - /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, - (match, key: string) => placeholders[key] ?? match - ); + value.replaceAll( + /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, + (match, key: string) => placeholders[key] ?? match + ); const createInterpolationPlaceholders = ( - config: ReadieConfig, - interpolationContext: InterpolationContext + config: ReadieConfig, + interpolationContext: InterpolationContext ): Record => { - const resolvedPackageName = - interpolationContext.packageName?.trim() || config.title; - - return { - packageName: resolvedPackageName, - packageNameEncoded: encodeURIComponent(resolvedPackageName), - title: config.title, - }; + const resolvedPackageName = + interpolationContext.packageName?.trim() || config.title; + + return { + packageName: resolvedPackageName, + packageNameEncoded: encodeURIComponent(resolvedPackageName), + title: config.title, + }; }; const interpolateCustomSections = ( - customSections: Record | undefined, - placeholders: Record + customSections: Record | undefined, + placeholders: Record ): Record | undefined => { - if (!customSections) { - return; - } - - const interpolatedSections: Record = {}; - for (const [key, value] of Object.entries(customSections)) { - interpolatedSections[key] = interpolatePlaceholders(value, placeholders); - } - return interpolatedSections; + if (!customSections) { + return; + } + + const interpolatedSections: Record = {}; + for (const [key, value] of Object.entries(customSections)) { + interpolatedSections[key] = interpolatePlaceholders(value, placeholders); + } + return interpolatedSections; }; const interpolateTopLevelStrings = ( - config: ReadieConfig, - interpolationContext: InterpolationContext + config: ReadieConfig, + interpolationContext: InterpolationContext ): ReadieConfig => { - const placeholders = createInterpolationPlaceholders( - config, - interpolationContext - ); - const interpolated = { ...config } as Record; - - for (const [key, value] of Object.entries(interpolated)) { - if (typeof value === "string") { - interpolated[key] = interpolatePlaceholders(value, placeholders); - } - } - - interpolated.customSections = interpolateCustomSections( - config.customSections, - placeholders - ); - - return interpolated as unknown as ReadieConfig; + const placeholders = createInterpolationPlaceholders( + config, + interpolationContext + ); + const interpolated = { ...config } as Record; + + for (const [key, value] of Object.entries(interpolated)) { + if (typeof value === "string") { + interpolated[key] = interpolatePlaceholders(value, placeholders); + } + } + + interpolated.customSections = interpolateCustomSections( + config.customSections, + placeholders + ); + + return interpolated as unknown as ReadieConfig; }; const resolveMergedValue = ( - key: keyof ReadieConfig, - projectConfig: ReadieConfig, - globalConfig: ReadieGlobalConfig | null + key: keyof ReadieConfig, + projectConfig: ReadieConfig, + globalConfig: ReadieGlobalConfig | null ): T | undefined => { - const project = projectConfig as unknown as Record; - const global = (globalConfig ?? {}) as Record; + const project = projectConfig as unknown as Record; + const global = (globalConfig ?? {}) as Record; - if (hasOwn(project, key)) { - const projectValue = project[key]; - return projectValue === null ? undefined : (projectValue as T); - } + if (hasOwn(project, key)) { + const projectValue = project[key]; + return projectValue === null ? undefined : (projectValue as T); + } - const globalValue = global[key]; - return globalValue === null ? undefined : (globalValue as T | undefined); + const globalValue = global[key]; + return globalValue === null ? undefined : (globalValue as T | undefined); }; const readGlobalCustomSections = (global: Record) => { - if (typeof global.customSections !== "object" || !global.customSections) { - return; - } - return global.customSections as Record; + if (typeof global.customSections !== "object" || !global.customSections) { + return; + } + return global.customSections as Record; }; const readProjectCustomSections = (project: Record) => { - const projectCustomSections = project.customSections; - if (projectCustomSections === null) { - return null; - } - if (typeof projectCustomSections !== "object" || !projectCustomSections) { - return; - } - return projectCustomSections as Record; + const projectCustomSections = project.customSections; + if (projectCustomSections === null) { + return null; + } + if (typeof projectCustomSections !== "object" || !projectCustomSections) { + return; + } + return projectCustomSections as Record; }; const resolveMergedCustomSections = ( - globalConfig: ReadieGlobalConfig | null, - projectConfig: ReadieConfig + globalConfig: ReadieGlobalConfig | null, + projectConfig: ReadieConfig ) => { - const project = projectConfig as unknown as Record; - const global = (globalConfig ?? {}) as Record; - const globalCustomSections = readGlobalCustomSections(global); - - if (!hasOwn(project, "customSections")) { - return globalCustomSections; - } - - const projectCustomSections = readProjectCustomSections(project); - if (projectCustomSections === null || !projectCustomSections) { - return; - } - - return { - ...globalCustomSections, - ...projectCustomSections, - }; + const project = projectConfig as unknown as Record; + const global = (globalConfig ?? {}) as Record; + const globalCustomSections = readGlobalCustomSections(global); + + if (!hasOwn(project, "customSections")) { + return globalCustomSections; + } + + const projectCustomSections = readProjectCustomSections(project); + if (projectCustomSections === null || !projectCustomSections) { + return; + } + + return { + ...globalCustomSections, + ...projectCustomSections, + }; }; export const mergeConfigs = ( - globalConfig: ReadieGlobalConfig | null, - projectConfig: ReadieConfig, - interpolationContext: InterpolationContext = {} + globalConfig: ReadieGlobalConfig | null, + projectConfig: ReadieConfig, + interpolationContext: InterpolationContext = {} ): ReadieConfig => { - const mergedCustomSections = resolveMergedCustomSections( - globalConfig, - projectConfig - ); - - const merged: ReadieConfig = { - $schema: resolveMergedValue("$schema", projectConfig, globalConfig), - badges: resolveMergedValue("badges", projectConfig, globalConfig), - banner: resolveMergedValue("banner", projectConfig, globalConfig), - commands: resolveMergedValue("commands", projectConfig, globalConfig), - contributing: resolveMergedValue( - "contributing", - projectConfig, - globalConfig - ), - customSections: mergedCustomSections, - description: projectConfig.description, - docsLink: resolveMergedValue("docsLink", projectConfig, globalConfig), - features: resolveMergedValue("features", projectConfig, globalConfig), - footer: resolveMergedValue("footer", projectConfig, globalConfig), - globalFlags: resolveMergedValue("globalFlags", projectConfig, globalConfig), - includeTableOfContents: resolveMergedValue( - "includeTableOfContents", - projectConfig, - globalConfig - ), - installation: resolveMergedValue( - "installation", - projectConfig, - globalConfig - ), - license: resolveMergedValue("license", projectConfig, globalConfig), - manualInstallation: resolveMergedValue( - "manualInstallation", - projectConfig, - globalConfig - ), - output: resolveMergedValue("output", projectConfig, globalConfig), - prerequisites: resolveMergedValue( - "prerequisites", - projectConfig, - globalConfig - ), - quickStart: resolveMergedValue("quickStart", projectConfig, globalConfig), - quickStartLink: resolveMergedValue( - "quickStartLink", - projectConfig, - globalConfig - ), - security: resolveMergedValue("security", projectConfig, globalConfig), - support: resolveMergedValue("support", projectConfig, globalConfig), - title: projectConfig.title, - usage: resolveMergedValue("usage", projectConfig, globalConfig), - version: resolveMergedValue("version", projectConfig, globalConfig), - }; - - return interpolateTopLevelStrings(merged, interpolationContext); + const mergedCustomSections = resolveMergedCustomSections( + globalConfig, + projectConfig + ); + + const merged: ReadieConfig = { + $schema: resolveMergedValue("$schema", projectConfig, globalConfig), + badges: resolveMergedValue("badges", projectConfig, globalConfig), + banner: resolveMergedValue("banner", projectConfig, globalConfig), + commands: resolveMergedValue("commands", projectConfig, globalConfig), + contributing: resolveMergedValue( + "contributing", + projectConfig, + globalConfig + ), + customSections: mergedCustomSections, + description: projectConfig.description, + docsLink: resolveMergedValue("docsLink", projectConfig, globalConfig), + features: resolveMergedValue("features", projectConfig, globalConfig), + footer: resolveMergedValue("footer", projectConfig, globalConfig), + globalFlags: resolveMergedValue("globalFlags", projectConfig, globalConfig), + includeTableOfContents: resolveMergedValue( + "includeTableOfContents", + projectConfig, + globalConfig + ), + installation: resolveMergedValue( + "installation", + projectConfig, + globalConfig + ), + license: resolveMergedValue("license", projectConfig, globalConfig), + manualInstallation: resolveMergedValue( + "manualInstallation", + projectConfig, + globalConfig + ), + output: resolveMergedValue("output", projectConfig, globalConfig), + prerequisites: resolveMergedValue( + "prerequisites", + projectConfig, + globalConfig + ), + quickStart: resolveMergedValue("quickStart", projectConfig, globalConfig), + quickStartLink: resolveMergedValue( + "quickStartLink", + projectConfig, + globalConfig + ), + security: resolveMergedValue("security", projectConfig, globalConfig), + support: resolveMergedValue("support", projectConfig, globalConfig), + title: projectConfig.title, + usage: resolveMergedValue("usage", projectConfig, globalConfig), + version: resolveMergedValue("version", projectConfig, globalConfig), + }; + + return interpolateTopLevelStrings(merged, interpolationContext); }; diff --git a/src/config/starter-config.ts b/src/config/starter-config.ts index ab5eb46..01d2b91 100644 --- a/src/config/starter-config.ts +++ b/src/config/starter-config.ts @@ -1,18 +1,18 @@ import type { ReadieConfig } from "./types"; export const DEFAULT_SCHEMA_URL = - "https://unpkg.com/readie/schemas/readie.schema.json"; + "https://unpkg.com/readie/schemas/readie.schema.json"; export const starterConfig: ReadieConfig = { - $schema: DEFAULT_SCHEMA_URL, - description: "A short description of what this project does.", - docsLink: "https://example.com/docs", - features: ["Fast setup", "Clear docs", "Simple CLI usage"], - includeTableOfContents: true, - installation: ["```bash\nnpm install my-project\n```"], - title: "My Project", - usage: ["Explain basic usage in a few steps.", "```bash\nnpm run start\n```"], - version: "1", + $schema: DEFAULT_SCHEMA_URL, + description: "A short description of what this project does.", + docsLink: "https://example.com/docs", + features: ["Fast setup", "Clear docs", "Simple CLI usage"], + includeTableOfContents: true, + installation: ["```bash\nnpm install my-project\n```"], + title: "My Project", + usage: ["Explain basic usage in a few steps.", "```bash\nnpm run start\n```"], + version: "1", }; export const starterConfigText = `${JSON.stringify(starterConfig, null, 2)}\n`; diff --git a/src/config/types.ts b/src/config/types.ts index 4eaec67..59741cd 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,78 +1,78 @@ export interface ReadieCommand { - name: string; - description: string; + name: string; + description: string; } export interface ReadieFlag { - flag: string; - description: string; + flag: string; + description: string; } export interface ReadieBadge { - label: string; - image: string; - link?: string; + label: string; + image: string; + link?: string; } export interface ReadieLicenseObject { - name: string; - url: string; + name: string; + url: string; } export type ReadieLicense = string | ReadieLicenseObject; export interface ReadieConfig { - $schema?: string; - version?: "1"; - title: string; - description: string; - output?: string; - includeTableOfContents?: boolean; - features?: string[]; - prerequisites?: string[]; - installation?: string[]; - manualInstallation?: string[]; - usage?: string[]; - commands?: ReadieCommand[]; - globalFlags?: ReadieFlag[]; - badges?: ReadieBadge[]; - banner?: string; - quickStart?: string; - support?: string[]; - contributing?: string[]; - security?: string; - license?: ReadieLicense; - footer?: string; - docsLink?: string; - quickStartLink?: string; - customSections?: Record; + $schema?: string; + version?: "1"; + title: string; + description: string; + output?: string; + includeTableOfContents?: boolean; + features?: string[]; + prerequisites?: string[]; + installation?: string[]; + manualInstallation?: string[]; + usage?: string[]; + commands?: ReadieCommand[]; + globalFlags?: ReadieFlag[]; + badges?: ReadieBadge[]; + banner?: string; + quickStart?: string; + support?: string[]; + contributing?: string[]; + security?: string; + license?: ReadieLicense; + footer?: string; + docsLink?: string; + quickStartLink?: string; + customSections?: Record; } export type ReadieGlobalConfig = Partial; export interface GenerateSingleOptions { - configPath: string; - outputPath?: string; - dryRun: boolean; - useGlobalConfig?: boolean; + configPath: string; + outputPath?: string; + dryRun: boolean; + useGlobalConfig?: boolean; } export interface GenerateSingleResult { - outputPath: string; - updated: boolean; + outputPath: string; + updated: boolean; } export interface GenerateWorkspaceOptions { - rootDir: string; - configName: string; - packageFilter: Set; - dryRun: boolean; - useGlobalConfig?: boolean; + rootDir: string; + configName: string; + packageFilter: Set; + dryRun: boolean; + useGlobalConfig?: boolean; } export interface GenerateWorkspaceResult { - updated: string[]; - unchanged: string[]; - failed: { projectDir: string; error: unknown }[]; - skippedByFilter: string[]; + updated: string[]; + unchanged: string[]; + failed: { projectDir: string; error: unknown }[]; + skippedByFilter: string[]; } diff --git a/src/index.ts b/src/index.ts index 007c79f..96a2844 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,63 +13,63 @@ import { resolveInvocation } from "./cli/resolve-invocation"; const version = "0.1.0"; const runGenerate = (args: string[]) => - Command.run(generateCommand, { - name: "readie", - version, - })(args).pipe(Effect.provide(NodeContext.layer)); + Command.run(generateCommand, { + name: "readie", + version, + })(args).pipe(Effect.provide(NodeContext.layer)); const runGenerateWorkspace = (args: string[]) => - Command.run(generateWorkspaceCommand, { - name: "readie", - version, - })(args).pipe(Effect.provide(NodeContext.layer)); + Command.run(generateWorkspaceCommand, { + name: "readie", + version, + })(args).pipe(Effect.provide(NodeContext.layer)); const runInit = (args: string[]) => - Command.run(initCommand, { - name: "readie", - version, - })(args).pipe(Effect.provide(NodeContext.layer)); + Command.run(initCommand, { + name: "readie", + version, + })(args).pipe(Effect.provide(NodeContext.layer)); const selectCommandEffect = ( - resolved: ReturnType + resolved: ReturnType ) => { - if (resolved.mode === "generate") { - return runGenerate(resolved.commandArgs); - } - if (resolved.mode === "generate:workspace") { - return runGenerateWorkspace(resolved.commandArgs); - } - return runInit(resolved.commandArgs); + if (resolved.mode === "generate") { + return runGenerate(resolved.commandArgs); + } + if (resolved.mode === "generate:workspace") { + return runGenerateWorkspace(resolved.commandArgs); + } + return runInit(resolved.commandArgs); }; const handleError = (error: unknown) => { - if (ValidationError.isValidationError(error)) { - console.error(String(error)); - process.exitCode = 1; - return; - } + if (ValidationError.isValidationError(error)) { + console.error(String(error)); + process.exitCode = 1; + return; + } - console.error(error instanceof Error ? error.message : String(error)); - process.exitCode = 1; + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; }; const main = async () => { - const resolved = resolveInvocation(process.argv.slice(2)); + const resolved = resolveInvocation(process.argv.slice(2)); - if (resolved.mode === "help") { - printRootHelp(); - process.exit(0); - } - if (resolved.mode === "unknown") { - printRootHelp(); - process.exit(1); - } + if (resolved.mode === "help") { + printRootHelp(); + process.exit(0); + } + if (resolved.mode === "unknown") { + printRootHelp(); + process.exit(1); + } - try { - await Effect.runPromise(selectCommandEffect(resolved)); - } catch (error) { - handleError(error); - } + try { + await Effect.runPromise(selectCommandEffect(resolved)); + } catch (error) { + handleError(error); + } }; await main(); diff --git a/src/readme-generator/generator.ts b/src/readme-generator/generator.ts index 7169b91..2e42b31 100644 --- a/src/readme-generator/generator.ts +++ b/src/readme-generator/generator.ts @@ -1,235 +1,235 @@ import { - existsSync, - pathExists, - readdir, - readFile, - readJson, - writeFile, + existsSync, + pathExists, + readdir, + readFile, + readJson, + writeFile, } from "fs-extra"; import { basename, dirname, join, resolve } from "pathe"; import { - loadGlobalConfig, - loadReadieConfig, - mergeConfigs, + loadGlobalConfig, + loadReadieConfig, + mergeConfigs, } from "#src/config/load-config"; import type { - GenerateSingleOptions, - GenerateSingleResult, - GenerateWorkspaceOptions, - GenerateWorkspaceResult, + GenerateSingleOptions, + GenerateSingleResult, + GenerateWorkspaceOptions, + GenerateWorkspaceResult, } from "#src/config/types"; import { baseReadmeTemplate } from "./template"; export const parsePackageList = (values: string[]): Set => { - const packages = new Set(); - for (const value of values) { - for (const part of value.split(",")) { - const name = part.trim(); - if (name.length > 0) { - packages.add(name); - } - } - } - return packages; + const packages = new Set(); + for (const value of values) { + for (const part of value.split(",")) { + const name = part.trim(); + if (name.length > 0) { + packages.add(name); + } + } + } + return packages; }; const resolveOutputPath = ( - configPath: string, - configOutputPath: string | undefined, - cliOutputPath: string | undefined + configPath: string, + configOutputPath: string | undefined, + cliOutputPath: string | undefined ) => { - if (cliOutputPath) { - return resolve(cliOutputPath); - } - if (configOutputPath) { - return resolve(dirname(configPath), configOutputPath); - } - return resolve(dirname(configPath), "README.md"); + if (cliOutputPath) { + return resolve(cliOutputPath); + } + if (configOutputPath) { + return resolve(dirname(configPath), configOutputPath); + } + return resolve(dirname(configPath), "README.md"); }; const resolvePackageName = async ( - configPath: string + configPath: string ): Promise => { - const packageJsonPath = join(dirname(configPath), "package.json"); - - try { - const parsed = (await readJson(packageJsonPath)) as { name?: unknown }; - return typeof parsed.name === "string" && parsed.name.trim().length > 0 - ? parsed.name - : undefined; - } catch { - return undefined; - } + const packageJsonPath = join(dirname(configPath), "package.json"); + + try { + const parsed = (await readJson(packageJsonPath)) as { name?: unknown }; + return typeof parsed.name === "string" && parsed.name.trim().length > 0 + ? parsed.name + : undefined; + } catch { + return undefined; + } }; const loadMergedConfig = async ( - absoluteConfigPath: string, - useGlobalConfig: boolean + absoluteConfigPath: string, + useGlobalConfig: boolean ) => { - const projectConfig = await loadReadieConfig(absoluteConfigPath); - const globalConfig = useGlobalConfig - ? await loadGlobalConfig(dirname(absoluteConfigPath)) - : null; - const packageName = await resolvePackageName(absoluteConfigPath); - return mergeConfigs(globalConfig, projectConfig, { packageName }); + const projectConfig = await loadReadieConfig(absoluteConfigPath); + const globalConfig = useGlobalConfig + ? await loadGlobalConfig(dirname(absoluteConfigPath)) + : null; + const packageName = await resolvePackageName(absoluteConfigPath); + return mergeConfigs(globalConfig, projectConfig, { packageName }); }; const readExistingContent = async (filePath: string) => { - if (!(await pathExists(filePath))) { - return null; - } - return readFile(filePath, "utf8"); + if (!(await pathExists(filePath))) { + return null; + } + return readFile(filePath, "utf8"); }; export const generateReadmeFromConfig = async ({ - configPath, - outputPath, - dryRun, - useGlobalConfig = true, + configPath, + outputPath, + dryRun, + useGlobalConfig = true, }: GenerateSingleOptions): Promise => { - const absoluteConfigPath = resolve(configPath); - const config = await loadMergedConfig(absoluteConfigPath, useGlobalConfig); - const resolvedOutputPath = resolveOutputPath( - absoluteConfigPath, - config.output, - outputPath - ); - - const content = baseReadmeTemplate(config); - const existingContent = await readExistingContent(resolvedOutputPath); - - if (existingContent === content) { - return { - outputPath: resolvedOutputPath, - updated: false, - }; - } - - if (!dryRun) { - await writeFile(resolvedOutputPath, content, "utf8"); - } - - return { - outputPath: resolvedOutputPath, - updated: true, - }; + const absoluteConfigPath = resolve(configPath); + const config = await loadMergedConfig(absoluteConfigPath, useGlobalConfig); + const resolvedOutputPath = resolveOutputPath( + absoluteConfigPath, + config.output, + outputPath + ); + + const content = baseReadmeTemplate(config); + const existingContent = await readExistingContent(resolvedOutputPath); + + if (existingContent === content) { + return { + outputPath: resolvedOutputPath, + updated: false, + }; + } + + if (!dryRun) { + await writeFile(resolvedOutputPath, content, "utf8"); + } + + return { + outputPath: resolvedOutputPath, + updated: true, + }; }; const listProjectDirsWithConfig = async ( - rootDir: string, - configName: string + rootDir: string, + configName: string ) => { - const entries = await readdir(rootDir, { withFileTypes: true }); - const allProjectDirs = entries - .filter((entry) => entry.isDirectory()) - .map((entry) => join(rootDir, entry.name)); - - const projectChecks = await Promise.all( - allProjectDirs.map(async (dir) => ({ - dir, - hasConfig: await pathExists(join(dir, configName)), - })) - ); - - return projectChecks.filter((item) => item.hasConfig).map((item) => item.dir); + const entries = await readdir(rootDir, { withFileTypes: true }); + const allProjectDirs = entries + .filter((entry) => entry.isDirectory()) + .map((entry) => join(rootDir, entry.name)); + + const projectChecks = await Promise.all( + allProjectDirs.map(async (dir) => ({ + dir, + hasConfig: await pathExists(join(dir, configName)), + })) + ); + + return projectChecks.filter((item) => item.hasConfig).map((item) => item.dir); }; const selectProjectDirs = ( - allProjectDirs: string[], - packageFilter: Set + allProjectDirs: string[], + packageFilter: Set ) => - packageFilter.size > 0 - ? allProjectDirs.filter((dir) => packageFilter.has(basename(dir))) - : allProjectDirs; + packageFilter.size > 0 + ? allProjectDirs.filter((dir) => packageFilter.has(basename(dir))) + : allProjectDirs; const collectSkippedByFilter = ( - allProjectDirs: string[], - packageFilter: Set + allProjectDirs: string[], + packageFilter: Set ) => { - if (packageFilter.size === 0) { - return []; - } - return allProjectDirs - .map((dir) => basename(dir)) - .filter((dirName) => !packageFilter.has(dirName)); + if (packageFilter.size === 0) { + return []; + } + return allProjectDirs + .map((dir) => basename(dir)) + .filter((dirName) => !packageFilter.has(dirName)); }; const pushProjectResult = ( - result: GenerateWorkspaceResult, - projectName: string, - updated: boolean, - dryRun: boolean + result: GenerateWorkspaceResult, + projectName: string, + updated: boolean, + dryRun: boolean ) => { - if (updated) { - result.updated.push(projectName); - const action = dryRun ? "Would update" : "Generated"; - console.log(`${action} README for ${projectName}`); - return; - } - - result.unchanged.push(projectName); - console.log(`No changes for ${projectName}`); + if (updated) { + result.updated.push(projectName); + const action = dryRun ? "Would update" : "Generated"; + console.log(`${action} README for ${projectName}`); + return; + } + + result.unchanged.push(projectName); + console.log(`No changes for ${projectName}`); }; const processWorkspaceProject = async ( - projectDir: string, - configName: string, - dryRun: boolean, - useGlobalConfig: boolean, - result: GenerateWorkspaceResult + projectDir: string, + configName: string, + dryRun: boolean, + useGlobalConfig: boolean, + result: GenerateWorkspaceResult ) => { - const projectName = basename(projectDir); - const configPath = join(projectDir, configName); - try { - const singleResult = await generateReadmeFromConfig({ - configPath, - dryRun, - useGlobalConfig, - }); - pushProjectResult(result, projectName, singleResult.updated, dryRun); - } catch (error) { - result.failed.push({ error, projectDir: projectName }); - console.error(`Error generating README for ${projectName}:`, error); - } + const projectName = basename(projectDir); + const configPath = join(projectDir, configName); + try { + const singleResult = await generateReadmeFromConfig({ + configPath, + dryRun, + useGlobalConfig, + }); + pushProjectResult(result, projectName, singleResult.updated, dryRun); + } catch (error) { + result.failed.push({ error, projectDir: projectName }); + console.error(`Error generating README for ${projectName}:`, error); + } }; export const generateWorkspaceReadmes = async ({ - rootDir, - configName, - packageFilter, - dryRun, - useGlobalConfig = true, + rootDir, + configName, + packageFilter, + dryRun, + useGlobalConfig = true, }: GenerateWorkspaceOptions): Promise => { - const absoluteRootDir = resolve(rootDir); - if (!existsSync(absoluteRootDir)) { - throw new Error(`Workspace root not found at ${absoluteRootDir}`); - } - - const allProjectDirs = await listProjectDirsWithConfig( - absoluteRootDir, - configName - ); - const selectedProjectDirs = selectProjectDirs(allProjectDirs, packageFilter); - const skippedByFilter = collectSkippedByFilter(allProjectDirs, packageFilter); - - const result: GenerateWorkspaceResult = { - failed: [], - skippedByFilter, - unchanged: [], - updated: [], - }; - - for (const projectDir of selectedProjectDirs) { - await processWorkspaceProject( - projectDir, - configName, - dryRun, - useGlobalConfig, - result - ); - } - - return result; + const absoluteRootDir = resolve(rootDir); + if (!existsSync(absoluteRootDir)) { + throw new Error(`Workspace root not found at ${absoluteRootDir}`); + } + + const allProjectDirs = await listProjectDirsWithConfig( + absoluteRootDir, + configName + ); + const selectedProjectDirs = selectProjectDirs(allProjectDirs, packageFilter); + const skippedByFilter = collectSkippedByFilter(allProjectDirs, packageFilter); + + const result: GenerateWorkspaceResult = { + failed: [], + skippedByFilter, + unchanged: [], + updated: [], + }; + + for (const projectDir of selectedProjectDirs) { + await processWorkspaceProject( + projectDir, + configName, + dryRun, + useGlobalConfig, + result + ); + } + + return result; }; diff --git a/src/readme-generator/template.ts b/src/readme-generator/template.ts index b0b84a6..b92b914 100644 --- a/src/readme-generator/template.ts +++ b/src/readme-generator/template.ts @@ -1,276 +1,276 @@ import type { - ReadieBadge, - ReadieConfig, - ReadieLicenseObject, + ReadieBadge, + ReadieConfig, + ReadieLicenseObject, } from "#src/config/types.js"; const isNonEmpty = (value?: string | null): value is string => - typeof value === "string" && value.trim().length > 0; + typeof value === "string" && value.trim().length > 0; const normalizeSections = (sections: string[]) => - sections - .join("\n") - .replaceAll(/\n{3,}/g, "\n\n") - .trim(); + sections + .join("\n") + .replaceAll(/\n{3,}/g, "\n\n") + .trim(); const appendUsageItem = (lines: string[], item: string, index: number) => { - if (item.startsWith("```")) { - if (lines.length > 0 && lines.at(-1) !== "") { - lines.push(""); - } - lines.push(item); - lines.push(""); - return index; - } + if (item.startsWith("```")) { + if (lines.length > 0 && lines.at(-1) !== "") { + lines.push(""); + } + lines.push(item); + lines.push(""); + return index; + } - const cleaned = item.startsWith("- ") ? item.slice(2) : item; - lines.push(`${index}. ${cleaned}`); - return index + 1; + const cleaned = item.startsWith("- ") ? item.slice(2) : item; + lines.push(`${index}. ${cleaned}`); + return index + 1; }; const renderNumberedWithCodeBlocks = (items: string[]) => { - const lines: string[] = []; - let i = 1; + const lines: string[] = []; + let i = 1; - for (const rawItem of items) { - const item = rawItem.trim(); - if (!item) { - continue; - } - i = appendUsageItem(lines, item, i); - } + for (const rawItem of items) { + const item = rawItem.trim(); + if (!item) { + continue; + } + i = appendUsageItem(lines, item, i); + } - return normalizeSections(lines); + return normalizeSections(lines); }; const addSection = ( - header: string, - content: string[] | undefined, - formatter: (item: string, index?: number) => string = (item) => `- ${item}` + header: string, + content: string[] | undefined, + formatter: (item: string, index?: number) => string = (item) => `- ${item}` ) => { - if (!content || content.length === 0) { - return ""; - } - const body = content.map(formatter).join("\n"); - return `${header}\n\n${body}`.trim(); + if (!content || content.length === 0) { + return ""; + } + const body = content.map(formatter).join("\n"); + return `${header}\n\n${body}`.trim(); }; const renderHeadingBlock = ( - heading: string, - content: string | undefined + heading: string, + content: string | undefined ): string => { - if (!isNonEmpty(content)) { - return ""; - } - if (content.trimStart().startsWith("## ")) { - return content; - } - return `${heading}\n\n${content}`; + if (!isNonEmpty(content)) { + return ""; + } + if (content.trimStart().startsWith("## ")) { + return content; + } + return `${heading}\n\n${content}`; }; const renderBadges = (badges: ReadieBadge[] | undefined) => { - if (!badges || badges.length === 0) { - return ""; - } - return badges - .map((badge) => { - const image = `![${badge.label}](${badge.image})`; - return isNonEmpty(badge.link) ? `[${image}](${badge.link})` : image; - }) - .join("\n"); + if (!badges || badges.length === 0) { + return ""; + } + return badges + .map((badge) => { + const image = `![${badge.label}](${badge.image})`; + return isNonEmpty(badge.link) ? `[${image}](${badge.link})` : image; + }) + .join("\n"); }; const renderSimpleListSection = ( - heading: string, - items: string[] | undefined, - formatter: (value: string) => string = (value) => value + heading: string, + items: string[] | undefined, + formatter: (value: string) => string = (value) => value ) => { - if (!items || items.length === 0) { - return ""; - } - return `${heading}\n\n${items.map(formatter).join("\n")}`; + if (!items || items.length === 0) { + return ""; + } + return `${heading}\n\n${items.map(formatter).join("\n")}`; }; const renderCommandsSection = (config: ReadieConfig) => - renderSimpleListSection( - "## Available Commands", - config.commands?.map((cmd) => `- \`${cmd.name}\`: ${cmd.description}`) - ); + renderSimpleListSection( + "## Available Commands", + config.commands?.map((cmd) => `- \`${cmd.name}\`: ${cmd.description}`) + ); const renderGlobalFlagsSection = (config: ReadieConfig) => - renderSimpleListSection( - "## Global Flags", - config.globalFlags?.map((flag) => `- \`${flag.flag}\`: ${flag.description}`) - ); + renderSimpleListSection( + "## Global Flags", + config.globalFlags?.map((flag) => `- \`${flag.flag}\`: ${flag.description}`) + ); const renderLicenseBlock = (license: ReadieConfig["license"]) => { - if (!license) { - return ""; - } - if (typeof license === "string") { - return renderHeadingBlock("## License", license); - } - const { name, url } = license as ReadieLicenseObject; - return `## License\n\n[${name}](${url})`; + if (!license) { + return ""; + } + if (typeof license === "string") { + return renderHeadingBlock("## License", license); + } + const { name, url } = license as ReadieLicenseObject; + return `## License\n\n[${name}](${url})`; }; interface ReadmeSections { - bannerBlock: string; - titleBlock: string; - badgesBlock: string; - featuresBlock: string; - prerequisitesBlock: string; - quickStartBlock: string; - installationBlock: string; - manualInstallationBlock: string; - usageBlock: string; - commandsBlock: string; - globalFlagsBlock: string; - docsBlock: string; - quickStartLinkBlock: string; - supportBlock: string; - contributingBlock: string; - securityBlock: string; - licenseBlock: string; - customSectionsBlock: string; - footerBlock: string; + bannerBlock: string; + titleBlock: string; + badgesBlock: string; + featuresBlock: string; + prerequisitesBlock: string; + quickStartBlock: string; + installationBlock: string; + manualInstallationBlock: string; + usageBlock: string; + commandsBlock: string; + globalFlagsBlock: string; + docsBlock: string; + quickStartLinkBlock: string; + supportBlock: string; + contributingBlock: string; + securityBlock: string; + licenseBlock: string; + customSectionsBlock: string; + footerBlock: string; } const createReadmeSections = (config: ReadieConfig): ReadmeSections => { - const bannerBlock = isNonEmpty(config.banner) ? config.banner : ""; - const titleBlock = - isNonEmpty(bannerBlock) && bannerBlock.toLowerCase().includes(" `## ${heading}\n\n${content}`) - .join("\n\n") - : "", - docsBlock: config.docsLink - ? `## Documentation + return { + badgesBlock: renderBadges(config.badges), + bannerBlock, + commandsBlock: renderCommandsSection(config), + contributingBlock: addSection("## Contributing", config.contributing), + customSectionsBlock: config.customSections + ? Object.entries(config.customSections) + .map(([heading, content]) => `## ${heading}\n\n${content}`) + .join("\n\n") + : "", + docsBlock: config.docsLink + ? `## Documentation For further information, guides, and examples visit the [reference documentation](${config.docsLink}).` - : "", - featuresBlock: renderSimpleListSection( - "## Key Features", - config.features, - (feature) => `- ${feature}` - ), - footerBlock: isNonEmpty(config.footer) ? config.footer : "", - globalFlagsBlock: renderGlobalFlagsSection(config), - installationBlock: renderSimpleListSection( - "## Installation", - config.installation - ), - licenseBlock: renderLicenseBlock(config.license), - manualInstallationBlock: renderSimpleListSection( - "## Manual Installation", - config.manualInstallation - ), - prerequisitesBlock: addSection("## Prerequisites", config.prerequisites), - quickStartBlock: renderHeadingBlock("## Quick Start", config.quickStart), - quickStartLinkBlock: config.quickStartLink - ? `## Additional Quick Start + : "", + featuresBlock: renderSimpleListSection( + "## Key Features", + config.features, + (feature) => `- ${feature}` + ), + footerBlock: isNonEmpty(config.footer) ? config.footer : "", + globalFlagsBlock: renderGlobalFlagsSection(config), + installationBlock: renderSimpleListSection( + "## Installation", + config.installation + ), + licenseBlock: renderLicenseBlock(config.license), + manualInstallationBlock: renderSimpleListSection( + "## Manual Installation", + config.manualInstallation + ), + prerequisitesBlock: addSection("## Prerequisites", config.prerequisites), + quickStartBlock: renderHeadingBlock("## Quick Start", config.quickStart), + quickStartLinkBlock: config.quickStartLink + ? `## Additional Quick Start See the full quick start guide [here](${config.quickStartLink}).` - : "", - securityBlock: renderHeadingBlock("## Security", config.security), - supportBlock: addSection("## Support", config.support), - titleBlock, - usageBlock: config.usage - ? `## Usage\n\n${renderNumberedWithCodeBlocks(config.usage)}` - : "", - }; + : "", + securityBlock: renderHeadingBlock("## Security", config.security), + supportBlock: addSection("## Support", config.support), + titleBlock, + usageBlock: config.usage + ? `## Usage\n\n${renderNumberedWithCodeBlocks(config.usage)}` + : "", + }; }; const slugifyHeading = (title: string) => - title - .toLowerCase() - .replaceAll(/[^a-z0-9 -]/g, "") - .trim() - .replaceAll(/\s+/g, "-"); + title + .toLowerCase() + .replaceAll(/[^a-z0-9 -]/g, "") + .trim() + .replaceAll(/\s+/g, "-"); const createTocTitles = (config: ReadieConfig, sections: ReadmeSections) => { - const tocSectionTitles = [ - ["Key Features", sections.featuresBlock], - ["Prerequisites", sections.prerequisitesBlock], - ["Quick Start", sections.quickStartBlock], - ["Installation", sections.installationBlock], - ["Manual Installation", sections.manualInstallationBlock], - ["Usage", sections.usageBlock], - ["Available Commands", sections.commandsBlock], - ["Global Flags", sections.globalFlagsBlock], - ["Documentation", sections.docsBlock], - ["Additional Quick Start", sections.quickStartLinkBlock], - ["Support", sections.supportBlock], - ["Contributing", sections.contributingBlock], - ["Security", sections.securityBlock], - ["License", sections.licenseBlock], - ].filter(([, section]) => isNonEmpty(section)); + const tocSectionTitles = [ + ["Key Features", sections.featuresBlock], + ["Prerequisites", sections.prerequisitesBlock], + ["Quick Start", sections.quickStartBlock], + ["Installation", sections.installationBlock], + ["Manual Installation", sections.manualInstallationBlock], + ["Usage", sections.usageBlock], + ["Available Commands", sections.commandsBlock], + ["Global Flags", sections.globalFlagsBlock], + ["Documentation", sections.docsBlock], + ["Additional Quick Start", sections.quickStartLinkBlock], + ["Support", sections.supportBlock], + ["Contributing", sections.contributingBlock], + ["Security", sections.securityBlock], + ["License", sections.licenseBlock], + ].filter(([, section]) => isNonEmpty(section)); - if (!isNonEmpty(sections.customSectionsBlock)) { - return tocSectionTitles; - } + if (!isNonEmpty(sections.customSectionsBlock)) { + return tocSectionTitles; + } - for (const key of Object.keys(config.customSections ?? {})) { - tocSectionTitles.push([key, `## ${key}`]); - } - return tocSectionTitles; + for (const key of Object.keys(config.customSections ?? {})) { + tocSectionTitles.push([key, `## ${key}`]); + } + return tocSectionTitles; }; const createTocBlock = ( - includeTableOfContents: boolean | undefined, - titles: string[][] + includeTableOfContents: boolean | undefined, + titles: string[][] ) => { - if (includeTableOfContents === false || titles.length === 0) { - return ""; - } - const links = titles - .map(([title]) => `- [${title}](#${slugifyHeading(title)})`) - .join("\n"); - return `## Table of Contents\n\n${links}`; + if (includeTableOfContents === false || titles.length === 0) { + return ""; + } + const links = titles + .map(([title]) => `- [${title}](#${slugifyHeading(title)})`) + .join("\n"); + return `## Table of Contents\n\n${links}`; }; export const baseReadmeTemplate = (rawConfig: ReadieConfig) => { - const config: ReadieConfig = { ...rawConfig }; - const sections = createReadmeSections(config); - const tocTitles = createTocTitles(config, sections); - const tocBlock = createTocBlock(config.includeTableOfContents, tocTitles); + const config: ReadieConfig = { ...rawConfig }; + const sections = createReadmeSections(config); + const tocTitles = createTocTitles(config, sections); + const tocBlock = createTocBlock(config.includeTableOfContents, tocTitles); - const readmeContent = [ - sections.bannerBlock, - sections.titleBlock, - sections.badgesBlock, - config.description, - tocBlock, - sections.featuresBlock, - sections.prerequisitesBlock, - sections.quickStartBlock, - sections.installationBlock, - sections.manualInstallationBlock, - sections.usageBlock, - sections.commandsBlock, - sections.globalFlagsBlock, - sections.docsBlock, - sections.quickStartLinkBlock, - sections.supportBlock, - sections.contributingBlock, - sections.securityBlock, - sections.licenseBlock, - sections.customSectionsBlock, - sections.footerBlock, - ] - .filter((section) => isNonEmpty(section)) - .join("\n\n") - .replaceAll(/\n{3,}/g, "\n\n") - .replace(/\n{2,}$/, "\n"); + const readmeContent = [ + sections.bannerBlock, + sections.titleBlock, + sections.badgesBlock, + config.description, + tocBlock, + sections.featuresBlock, + sections.prerequisitesBlock, + sections.quickStartBlock, + sections.installationBlock, + sections.manualInstallationBlock, + sections.usageBlock, + sections.commandsBlock, + sections.globalFlagsBlock, + sections.docsBlock, + sections.quickStartLinkBlock, + sections.supportBlock, + sections.contributingBlock, + sections.securityBlock, + sections.licenseBlock, + sections.customSectionsBlock, + sections.footerBlock, + ] + .filter((section) => isNonEmpty(section)) + .join("\n\n") + .replaceAll(/\n{3,}/g, "\n\n") + .replace(/\n{2,}$/, "\n"); - return `${readmeContent.trim()}\n`; + return `${readmeContent.trim()}\n`; }; diff --git a/test/generator-global-interpolation.test.ts b/test/generator-global-interpolation.test.ts index 98b9dfd..b870466 100644 --- a/test/generator-global-interpolation.test.ts +++ b/test/generator-global-interpolation.test.ts @@ -5,48 +5,48 @@ import { temporaryDirectory } from "tempy"; import { generateReadmeFromConfig } from "#src/readme-generator/generator.js"; const writeJson = async (filePath: string, value: unknown) => { - await writeFile(filePath, JSON.stringify(value, null, 2), "utf8"); + await writeFile(filePath, JSON.stringify(value, null, 2), "utf8"); }; const setupFixture = async () => { - const rootDir = temporaryDirectory(); - const packageDir = join(rootDir, "packages", "react"); - await ensureDir(packageDir); - - await writeJson(join(rootDir, "readie.global.json"), { - banner: '

{{title}}

', - footer: "Built for {{ title }} ({{packageNameEncoded}})", - }); - - await writeJson(join(packageDir, "package.json"), { - name: "@c15t/react", - version: "1.0.0", - }); - - const configPath = join(packageDir, "readie.json"); - await writeJson(configPath, { - description: "CMP for React", - title: "@c15t/react: React Consent Components", - }); - - return { configPath, rootDir }; + const rootDir = temporaryDirectory(); + const packageDir = join(rootDir, "packages", "react"); + await ensureDir(packageDir); + + await writeJson(join(rootDir, "readie.global.json"), { + banner: '

{{title}}

', + footer: "Built for {{ title }} ({{packageNameEncoded}})", + }); + + await writeJson(join(packageDir, "package.json"), { + name: "@c15t/react", + version: "1.0.0", + }); + + const configPath = join(packageDir, "readie.json"); + await writeJson(configPath, { + description: "CMP for React", + title: "@c15t/react: React Consent Components", + }); + + return { configPath, rootDir }; }; describe("generateReadmeFromConfig with global interpolation", () => { - it("injects title and package placeholders in global config", async () => { - const { configPath, rootDir } = await setupFixture(); - const result = await generateReadmeFromConfig({ - configPath, - dryRun: false, - }); - const generated = await readFile(result.outputPath, "utf8"); - - expect(generated).toContain( - '

@c15t/react: React Consent Components

' - ); - expect(generated).toContain( - "Built for @c15t/react: React Consent Components (%40c15t%2Freact)" - ); - await remove(rootDir); - }); + it("injects title and package placeholders in global config", async () => { + const { configPath, rootDir } = await setupFixture(); + const result = await generateReadmeFromConfig({ + configPath, + dryRun: false, + }); + const generated = await readFile(result.outputPath, "utf8"); + + expect(generated).toContain( + '

@c15t/react: React Consent Components

' + ); + expect(generated).toContain( + "Built for @c15t/react: React Consent Components (%40c15t%2Freact)" + ); + await remove(rootDir); + }); }); diff --git a/test/merge-config.test.ts b/test/merge-config.test.ts index c04c629..8587835 100644 --- a/test/merge-config.test.ts +++ b/test/merge-config.test.ts @@ -2,59 +2,59 @@ import { mergeConfigs } from "#src/config/load-config.js"; import type { ReadieConfig, ReadieGlobalConfig } from "#src/config/types.js"; const createProjectConfig = ( - overrides: Partial = {} + overrides: Partial = {} ): ReadieConfig => ({ - description: "Project level description.", - title: "My Package", - ...overrides, + description: "Project level description.", + title: "My Package", + ...overrides, }); describe("merge configs", () => { - it("interpolates top-level global string placeholders", () => { - const globalConfig: ReadieGlobalConfig = { - banner: '

{{title}}

', - customSections: { - Notes: "Package: {{title}}", - }, - features: ["Feature A"], - footer: "Built by {{ title }} - {{packageName}} - {{packageNameEncoded}}", - }; - - const merged = mergeConfigs(globalConfig, createProjectConfig(), { - packageName: "@c15t/react", - }); - - expect(merged.banner).toBe('

My Package

'); - expect(merged.footer).toBe( - "Built by My Package - @c15t/react - %40c15t%2Freact" - ); - expect(merged.features).toStrictEqual(["Feature A"]); - expect(merged.customSections?.Notes).toBe("Package: My Package"); - }); - - it("preserves project-over-global precedence before interpolation", () => { - const globalConfig: ReadieGlobalConfig = { - banner: "Global banner {{title}}", - quickStart: "Global quick start", - }; - - const projectConfig = createProjectConfig({ - banner: "Project banner", - quickStart: "Project quick start for {{title}}", - }); - - const merged = mergeConfigs(globalConfig, projectConfig); - - expect(merged.banner).toBe("Project banner"); - expect(merged.quickStart).toBe("Project quick start for My Package"); - }); - - it("falls back to title when packageName is unavailable", () => { - const globalConfig: ReadieGlobalConfig = { - footer: "Encoded: {{packageNameEncoded}}", - }; - - const merged = mergeConfigs(globalConfig, createProjectConfig()); - expect(merged.footer).toBe("Encoded: My%20Package"); - }); + it("interpolates top-level global string placeholders", () => { + const globalConfig: ReadieGlobalConfig = { + banner: '

{{title}}

', + customSections: { + Notes: "Package: {{title}}", + }, + features: ["Feature A"], + footer: "Built by {{ title }} - {{packageName}} - {{packageNameEncoded}}", + }; + + const merged = mergeConfigs(globalConfig, createProjectConfig(), { + packageName: "@c15t/react", + }); + + expect(merged.banner).toBe('

My Package

'); + expect(merged.footer).toBe( + "Built by My Package - @c15t/react - %40c15t%2Freact" + ); + expect(merged.features).toStrictEqual(["Feature A"]); + expect(merged.customSections?.Notes).toBe("Package: My Package"); + }); + + it("preserves project-over-global precedence before interpolation", () => { + const globalConfig: ReadieGlobalConfig = { + banner: "Global banner {{title}}", + quickStart: "Global quick start", + }; + + const projectConfig = createProjectConfig({ + banner: "Project banner", + quickStart: "Project quick start for {{title}}", + }); + + const merged = mergeConfigs(globalConfig, projectConfig); + + expect(merged.banner).toBe("Project banner"); + expect(merged.quickStart).toBe("Project quick start for My Package"); + }); + + it("falls back to title when packageName is unavailable", () => { + const globalConfig: ReadieGlobalConfig = { + footer: "Encoded: {{packageNameEncoded}}", + }; + + const merged = mergeConfigs(globalConfig, createProjectConfig()); + expect(merged.footer).toBe("Encoded: My%20Package"); + }); }); diff --git a/test/resolve-invocation.test.ts b/test/resolve-invocation.test.ts index a39cce7..7620e32 100644 --- a/test/resolve-invocation.test.ts +++ b/test/resolve-invocation.test.ts @@ -1,25 +1,25 @@ import { resolveInvocation } from "#src/cli/resolve-invocation.js"; describe("resolve invocation routing", () => { - it("defaults to generate when no args are passed", () => { - const resolved = resolveInvocation([]); - expect(resolved.mode).toBe("generate"); - expect(resolved.commandArgs).toStrictEqual([]); - }); + it("defaults to generate when no args are passed", () => { + const resolved = resolveInvocation([]); + expect(resolved.mode).toBe("generate"); + expect(resolved.commandArgs).toStrictEqual([]); + }); - it("routes workspace subcommand", () => { - const resolved = resolveInvocation([ - "generate:workspace", - "--root", - "./packages", - ]); - expect(resolved.mode).toBe("generate:workspace"); - expect(resolved.commandArgs).toStrictEqual(["--root", "./packages"]); - }); + it("routes workspace subcommand", () => { + const resolved = resolveInvocation([ + "generate:workspace", + "--root", + "./packages", + ]); + expect(resolved.mode).toBe("generate:workspace"); + expect(resolved.commandArgs).toStrictEqual(["--root", "./packages"]); + }); - it("routes init subcommand", () => { - const resolved = resolveInvocation(["init", "--force"]); - expect(resolved.mode).toBe("init"); - expect(resolved.commandArgs).toStrictEqual(["--force"]); - }); + it("routes init subcommand", () => { + const resolved = resolveInvocation(["init", "--force"]); + expect(resolved.mode).toBe("init"); + expect(resolved.commandArgs).toStrictEqual(["--force"]); + }); }); diff --git a/test/template.test.ts b/test/template.test.ts index b370e9f..5d8ccf6 100644 --- a/test/template.test.ts +++ b/test/template.test.ts @@ -1,26 +1,26 @@ import { baseReadmeTemplate } from "#src/readme-generator/template.js"; describe("base readme template", () => { - it("renders neutral markdown without c15t defaults", () => { - const markdown = baseReadmeTemplate({ - description: "A neutral README.", - features: ["Fast", "Simple"], - includeTableOfContents: true, - installation: ["```bash\nnpm install readie-demo\n```"], - security: "Please report issues privately.", - title: "Readie Demo", - usage: ["Run the command", "```bash\nnpx readie\n```"], - }); + it("renders neutral markdown without c15t defaults", () => { + const markdown = baseReadmeTemplate({ + description: "A neutral README.", + features: ["Fast", "Simple"], + includeTableOfContents: true, + installation: ["```bash\nnpm install readie-demo\n```"], + security: "Please report issues privately.", + title: "Readie Demo", + usage: ["Run the command", "```bash\nnpx readie\n```"], + }); - const requiredHeadings = [ - "# Readie Demo", - "## Table of Contents", - "## Key Features", - "## Security", - ]; - expect( - requiredHeadings.every((heading) => markdown.includes(heading)) - ).toBeTruthy(); - expect(markdown).not.toMatch(/c15t|consent\.io/); - }); + const requiredHeadings = [ + "# Readie Demo", + "## Table of Contents", + "## Key Features", + "## Security", + ]; + expect( + requiredHeadings.every((heading) => markdown.includes(heading)) + ).toBeTruthy(); + expect(markdown).not.toMatch(/c15t|consent\.io/); + }); }); diff --git a/test/validate-config.test.ts b/test/validate-config.test.ts index 0a5687d..876330c 100644 --- a/test/validate-config.test.ts +++ b/test/validate-config.test.ts @@ -5,34 +5,34 @@ import { temporaryDirectory } from "tempy"; import { loadReadieConfig } from "#src/config/load-config.js"; const createTempFile = async (contents: string) => { - const dir = temporaryDirectory(); - const filePath = join(dir, "readie.json"); - await writeFile(filePath, contents, "utf8"); - return filePath; + const dir = temporaryDirectory(); + const filePath = join(dir, "readie.json"); + await writeFile(filePath, contents, "utf8"); + return filePath; }; describe("load readie config", () => { - it("loads a valid config", async () => { - const configPath = await createTempFile( - JSON.stringify({ - description: "Config validation test.", - title: "Test Project", - }) - ); + it("loads a valid config", async () => { + const configPath = await createTempFile( + JSON.stringify({ + description: "Config validation test.", + title: "Test Project", + }) + ); - const config = await loadReadieConfig(configPath); - expect(config.title).toBe("Test Project"); - }); + const config = await loadReadieConfig(configPath); + expect(config.title).toBe("Test Project"); + }); - it("throws for invalid config", async () => { - const configPath = await createTempFile( - JSON.stringify({ - description: "Missing title should fail.", - }) - ); + it("throws for invalid config", async () => { + const configPath = await createTempFile( + JSON.stringify({ + description: "Missing title should fail.", + }) + ); - await expect(loadReadieConfig(configPath)).rejects.toThrow( - "Configuration validation failed" - ); - }); + await expect(loadReadieConfig(configPath)).rejects.toThrow( + "Configuration validation failed" + ); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 8e55e1d..fda423e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,34 +1,34 @@ { - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false, - "paths": { - "#src/*": ["./src/*"] - }, - "types": ["vitest/globals"] - }, - "include": ["src/**/*.ts"] + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "paths": { + "#src/*": ["./src/*"] + }, + "types": ["vitest/globals"] + }, + "include": ["src/**/*.ts"] } From 93ab5a3ffa868206c243d86c619480f06b435829 Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 21:23:09 +0000 Subject: [PATCH 07/12] Update package.json to enhance metadata and structure - Added missing "license" and "author" fields for better project documentation. - Reorganized properties to improve clarity, including restoring "type", "main", and "imports" fields. - Ensured consistent formatting and structure for improved readability. --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 3f1c7ac..ab6a1ca 100644 --- a/package.json +++ b/package.json @@ -13,17 +13,12 @@ "bugs": { "url": "https://github.com/consentdotio/readie/issues" }, + "license": "MIT", + "author": "Christopher Burns ", "repository": { "type": "git", "url": "git+https://github.com/consentdotio/readie.git" }, - "license": "MIT", - "author": "Christopher Burns ", - "type": "module", - "imports": { - "#src/*": "./src/*" - }, - "main": "dist/index.js", "bin": { "readie": "dist/index.js" }, @@ -37,6 +32,11 @@ "CODE_OF_CONDUCT.md", "SECURITY.md" ], + "type": "module", + "main": "dist/index.js", + "imports": { + "#src/*": "./src/*" + }, "scripts": { "build": "bun build src/index.ts --outdir dist --target node --minify", "changeset": "changeset", From 82356f4ca856ca17cfbb5d7c368450fe81ce242f Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 21:25:13 +0000 Subject: [PATCH 08/12] Refactor TOC title handling in README generator - Introduced a new type `TocTitleEntry` for better type safety in table of contents title entries. - Updated the `createTocTitles` function to filter out empty sections and return only visible titles. - Adjusted the `createTocBlock` function to accept the new type for improved clarity and maintainability. --- src/readme-generator/template.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/readme-generator/template.ts b/src/readme-generator/template.ts index b92b914..a37bc20 100644 --- a/src/readme-generator/template.ts +++ b/src/readme-generator/template.ts @@ -197,8 +197,10 @@ const slugifyHeading = (title: string) => .trim() .replaceAll(/\s+/g, "-"); +type TocTitleEntry = [title: string, section: string]; + const createTocTitles = (config: ReadieConfig, sections: ReadmeSections) => { - const tocSectionTitles = [ + const tocSectionTitles: TocTitleEntry[] = [ ["Key Features", sections.featuresBlock], ["Prerequisites", sections.prerequisitesBlock], ["Quick Start", sections.quickStartBlock], @@ -213,21 +215,24 @@ const createTocTitles = (config: ReadieConfig, sections: ReadmeSections) => { ["Contributing", sections.contributingBlock], ["Security", sections.securityBlock], ["License", sections.licenseBlock], - ].filter(([, section]) => isNonEmpty(section)); + ]; + const visibleTocSectionTitles = tocSectionTitles.filter(([, section]) => + isNonEmpty(section) + ); if (!isNonEmpty(sections.customSectionsBlock)) { - return tocSectionTitles; + return visibleTocSectionTitles; } for (const key of Object.keys(config.customSections ?? {})) { - tocSectionTitles.push([key, `## ${key}`]); + visibleTocSectionTitles.push([key, `## ${key}`]); } - return tocSectionTitles; + return visibleTocSectionTitles; }; const createTocBlock = ( includeTableOfContents: boolean | undefined, - titles: string[][] + titles: TocTitleEntry[] ) => { if (includeTableOfContents === false || titles.length === 0) { return ""; From e9ca159fa2c176a6e48281c6e14d47934986e2de Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 21:28:08 +0000 Subject: [PATCH 09/12] Add Vitest configuration file for testing setup - Introduced vitest.config.ts to define testing configuration. - Enabled global test options for improved test management and consistency. --- vitest.config.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 vitest.config.ts diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..076c92f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + }, +}); From 80e6f26a31fd9fbd5982508f00777f3d4d2e05cc Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 21:39:51 +0000 Subject: [PATCH 10/12] Update package.json, tsconfig.json, and README examples for improved structure and functionality - Changed ultracite version in package.json to use caret notation for better compatibility. - Expanded tsconfig.json to include test files for comprehensive type checking. - Updated README example code for consistent formatting and clarity. - Refactored command handling in index.ts to improve error handling and maintainability. - Adjusted default root directory in generate-workspace command for better usability. - Enhanced error handling in init command to ensure proper configuration writing. - Improved null checks in load-config.ts for robustness. - Modified README generator to utilize unique slugs for table of contents entries. - Updated tests to ensure proper cleanup and improved assertions for generated content. --- examples/basic/readie.json | 2 +- package.json | 2 +- src/cli/commands/generate-workspace.ts | 5 ++-- src/config/load-config.ts | 2 +- src/index.ts | 7 +++-- src/readme-generator/generator.ts | 13 ++------- src/readme-generator/template.ts | 19 +++++++++---- test/generator-global-interpolation.test.ts | 31 +++++++++++---------- test/resolve-invocation.test.ts | 19 +++++++++++++ test/template.test.ts | 7 +++-- test/validate-config.test.ts | 28 +++++++++++-------- tsconfig.json | 2 +- 12 files changed, 85 insertions(+), 52 deletions(-) diff --git a/examples/basic/readie.json b/examples/basic/readie.json index 536a122..77b9984 100644 --- a/examples/basic/readie.json +++ b/examples/basic/readie.json @@ -15,7 +15,7 @@ "installation": ["```bash\nnpm install acme-toolkit\n```"], "usage": [ "Import the toolkit and initialize your client.", - "```ts\nimport { createClient } from 'acme-toolkit'\n\nconst client = createClient()\n```" + "```ts\nimport { createClient } from \"acme-toolkit\";\n\nconst client = createClient();\n```" ], "support": ["Open an issue in your repository tracker."] } diff --git a/package.json b/package.json index ab6a1ca..51c1fc1 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "pathe": "^2.0.3", "tempy": "^3.2.0", "typescript": "^5.9.3", - "ultracite": "7.2.3", + "ultracite": "^7.2.3", "vitest": "^4.0.18" } } diff --git a/src/cli/commands/generate-workspace.ts b/src/cli/commands/generate-workspace.ts index 5764cbd..833bddc 100644 --- a/src/cli/commands/generate-workspace.ts +++ b/src/cli/commands/generate-workspace.ts @@ -39,7 +39,7 @@ export const generateWorkspaceCommand = Command.make( root: Options.directory("root").pipe( Options.withAlias("r"), Options.withDescription("Workspace root directory"), - Options.withDefault(resolve("./packages")) + Options.withDefault("./packages") ), strict: Options.boolean("strict").pipe( Options.withDescription("Exit with code 1 if any project fails") @@ -54,6 +54,7 @@ export const generateWorkspaceCommand = Command.make( noGlobal, }: GenerateWorkspaceCommandArgs) => Effect.gen(function* runGenerateWorkspaceCommand() { + const resolvedRoot = root ? resolve(root) : resolve("./packages"); const result = yield* Effect.tryPromise({ catch: (error: unknown) => error instanceof Error @@ -64,7 +65,7 @@ export const generateWorkspaceCommand = Command.make( configName, dryRun, packageFilter: parsePackageList(packageValues), - rootDir: root, + rootDir: resolvedRoot, useGlobalConfig: !noGlobal, }), }); diff --git a/src/config/load-config.ts b/src/config/load-config.ts index 9b80e04..fbf98e5 100644 --- a/src/config/load-config.ts +++ b/src/config/load-config.ts @@ -280,7 +280,7 @@ const resolveMergedCustomSections = ( } const projectCustomSections = readProjectCustomSections(project); - if (projectCustomSections === null || !projectCustomSections) { + if (projectCustomSections === null || projectCustomSections === undefined) { return; } diff --git a/src/index.ts b/src/index.ts index 96a2844..c9b2e24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { initCommand } from "./cli/commands/init"; import { printRootHelp } from "./cli/help"; import { resolveInvocation } from "./cli/resolve-invocation"; -const version = "0.1.0"; +const version = "0.0.1"; const runGenerate = (args: string[]) => Command.run(generateCommand, { @@ -39,7 +39,10 @@ const selectCommandEffect = ( if (resolved.mode === "generate:workspace") { return runGenerateWorkspace(resolved.commandArgs); } - return runInit(resolved.commandArgs); + if (resolved.mode === "init") { + return runInit(resolved.commandArgs); + } + throw new Error(`Unsupported invocation mode: ${resolved.mode}`); }; const handleError = (error: unknown) => { diff --git a/src/readme-generator/generator.ts b/src/readme-generator/generator.ts index 2e42b31..1634d00 100644 --- a/src/readme-generator/generator.ts +++ b/src/readme-generator/generator.ts @@ -1,11 +1,4 @@ -import { - existsSync, - pathExists, - readdir, - readFile, - readJson, - writeFile, -} from "fs-extra"; +import { pathExists, readdir, readFile, readJson, writeFile } from "fs-extra"; import { basename, dirname, join, resolve } from "pathe"; import { @@ -190,7 +183,7 @@ const processWorkspaceProject = async ( }); pushProjectResult(result, projectName, singleResult.updated, dryRun); } catch (error) { - result.failed.push({ error, projectDir: projectName }); + result.failed.push({ error, projectDir }); console.error(`Error generating README for ${projectName}:`, error); } }; @@ -203,7 +196,7 @@ export const generateWorkspaceReadmes = async ({ useGlobalConfig = true, }: GenerateWorkspaceOptions): Promise => { const absoluteRootDir = resolve(rootDir); - if (!existsSync(absoluteRootDir)) { + if (!(await pathExists(absoluteRootDir))) { throw new Error(`Workspace root not found at ${absoluteRootDir}`); } diff --git a/src/readme-generator/template.ts b/src/readme-generator/template.ts index a37bc20..1e0ebaf 100644 --- a/src/readme-generator/template.ts +++ b/src/readme-generator/template.ts @@ -197,6 +197,13 @@ const slugifyHeading = (title: string) => .trim() .replaceAll(/\s+/g, "-"); +const createUniqueSlug = (title: string, seenSlugs: Map) => { + const baseSlug = slugifyHeading(title); + const count = seenSlugs.get(baseSlug) ?? 0; + seenSlugs.set(baseSlug, count + 1); + return count === 0 ? baseSlug : `${baseSlug}-${count}`; +}; + type TocTitleEntry = [title: string, section: string]; const createTocTitles = (config: ReadieConfig, sections: ReadmeSections) => { @@ -237,23 +244,23 @@ const createTocBlock = ( if (includeTableOfContents === false || titles.length === 0) { return ""; } + const seenSlugs = new Map(); const links = titles - .map(([title]) => `- [${title}](#${slugifyHeading(title)})`) + .map(([title]) => `- [${title}](#${createUniqueSlug(title, seenSlugs)})`) .join("\n"); return `## Table of Contents\n\n${links}`; }; export const baseReadmeTemplate = (rawConfig: ReadieConfig) => { - const config: ReadieConfig = { ...rawConfig }; - const sections = createReadmeSections(config); - const tocTitles = createTocTitles(config, sections); - const tocBlock = createTocBlock(config.includeTableOfContents, tocTitles); + const sections = createReadmeSections(rawConfig); + const tocTitles = createTocTitles(rawConfig, sections); + const tocBlock = createTocBlock(rawConfig.includeTableOfContents, tocTitles); const readmeContent = [ sections.bannerBlock, sections.titleBlock, sections.badgesBlock, - config.description, + rawConfig.description, tocBlock, sections.featuresBlock, sections.prerequisitesBlock, diff --git a/test/generator-global-interpolation.test.ts b/test/generator-global-interpolation.test.ts index b870466..3cd6f9b 100644 --- a/test/generator-global-interpolation.test.ts +++ b/test/generator-global-interpolation.test.ts @@ -34,19 +34,22 @@ const setupFixture = async () => { describe("generateReadmeFromConfig with global interpolation", () => { it("injects title and package placeholders in global config", async () => { - const { configPath, rootDir } = await setupFixture(); - const result = await generateReadmeFromConfig({ - configPath, - dryRun: false, - }); - const generated = await readFile(result.outputPath, "utf8"); - - expect(generated).toContain( - '

@c15t/react: React Consent Components

' - ); - expect(generated).toContain( - "Built for @c15t/react: React Consent Components (%40c15t%2Freact)" - ); - await remove(rootDir); + const fixture = await setupFixture(); + try { + const result = await generateReadmeFromConfig({ + configPath: fixture.configPath, + dryRun: false, + }); + const generated = await readFile(result.outputPath, "utf8"); + + expect(generated).toContain( + '

@c15t/react: React Consent Components

' + ); + expect(generated).toContain( + "Built for @c15t/react: React Consent Components (%40c15t%2Freact)" + ); + } finally { + await remove(fixture.rootDir); + } }); }); diff --git a/test/resolve-invocation.test.ts b/test/resolve-invocation.test.ts index 7620e32..6c8f805 100644 --- a/test/resolve-invocation.test.ts +++ b/test/resolve-invocation.test.ts @@ -22,4 +22,23 @@ describe("resolve invocation routing", () => { expect(resolved.mode).toBe("init"); expect(resolved.commandArgs).toStrictEqual(["--force"]); }); + + it("routes --help to help mode", () => { + const resolved = resolveInvocation(["--help"]); + expect(resolved.mode).toBe("help"); + expect(resolved.commandArgs).toStrictEqual([]); + }); + + it("routes help command to help mode", () => { + const resolved = resolveInvocation(["help"]); + expect(resolved.mode).toBe("help"); + expect(resolved.commandArgs).toStrictEqual([]); + }); + + it("routes unknown commands to unknown mode", () => { + const args = ["invalid-command", "--flag"]; + const resolved = resolveInvocation(args); + expect(resolved.mode).toBe("unknown"); + expect(resolved.commandArgs).toStrictEqual(args); + }); }); diff --git a/test/template.test.ts b/test/template.test.ts index 5d8ccf6..5bf1218 100644 --- a/test/template.test.ts +++ b/test/template.test.ts @@ -18,9 +18,10 @@ describe("base readme template", () => { "## Key Features", "## Security", ]; - expect( - requiredHeadings.every((heading) => markdown.includes(heading)) - ).toBeTruthy(); + const missingHeadings = requiredHeadings.filter( + (heading) => !markdown.includes(heading) + ); + expect(missingHeadings).toStrictEqual([]); expect(markdown).not.toMatch(/c15t|consent\.io/); }); }); diff --git a/test/validate-config.test.ts b/test/validate-config.test.ts index 876330c..cdb793a 100644 --- a/test/validate-config.test.ts +++ b/test/validate-config.test.ts @@ -1,4 +1,4 @@ -import { writeFile } from "fs-extra"; +import { remove, writeFile } from "fs-extra"; import { join } from "pathe"; import { temporaryDirectory } from "tempy"; @@ -8,31 +8,37 @@ const createTempFile = async (contents: string) => { const dir = temporaryDirectory(); const filePath = join(dir, "readie.json"); await writeFile(filePath, contents, "utf8"); - return filePath; + return { dir, filePath }; }; describe("load readie config", () => { it("loads a valid config", async () => { - const configPath = await createTempFile( + const { dir, filePath } = await createTempFile( JSON.stringify({ description: "Config validation test.", title: "Test Project", }) ); - - const config = await loadReadieConfig(configPath); - expect(config.title).toBe("Test Project"); + try { + const config = await loadReadieConfig(filePath); + expect(config.title).toBe("Test Project"); + } finally { + await remove(dir); + } }); it("throws for invalid config", async () => { - const configPath = await createTempFile( + const { dir, filePath } = await createTempFile( JSON.stringify({ description: "Missing title should fail.", }) ); - - await expect(loadReadieConfig(configPath)).rejects.toThrow( - "Configuration validation failed" - ); + try { + await expect(loadReadieConfig(filePath)).rejects.toThrow( + "Configuration validation failed" + ); + } finally { + await remove(dir); + } }); }); diff --git a/tsconfig.json b/tsconfig.json index fda423e..6ae6cef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,5 +30,5 @@ }, "types": ["vitest/globals"] }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "test/**/*.ts"] } From f44d7020c89ecd706811a9fbd18b6980739b83d8 Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 22:06:56 +0000 Subject: [PATCH 11/12] Add ultracite to readie for enhanced functionality --- .changeset/neat-rabbits-clap.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/neat-rabbits-clap.md diff --git a/.changeset/neat-rabbits-clap.md b/.changeset/neat-rabbits-clap.md new file mode 100644 index 0000000..f965f9d --- /dev/null +++ b/.changeset/neat-rabbits-clap.md @@ -0,0 +1,5 @@ +--- +"readie": patch +--- + +Added ultracte to readie From 28d4b2b1287e5bef8e77ec93f118b8f8176db566 Mon Sep 17 00:00:00 2001 From: Christopher Burns Date: Thu, 19 Feb 2026 22:13:54 +0000 Subject: [PATCH 12/12] Refactor load-config.ts to improve null handling in merged custom sections - Updated the resolveMergedCustomSections function to return projectCustomSections when globalCustomSections is not defined, enhancing robustness and clarity in configuration merging. --- src/config/load-config.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/config/load-config.ts b/src/config/load-config.ts index fbf98e5..62a148a 100644 --- a/src/config/load-config.ts +++ b/src/config/load-config.ts @@ -284,10 +284,14 @@ const resolveMergedCustomSections = ( return; } - return { - ...globalCustomSections, - ...projectCustomSections, - }; + return globalCustomSections + ? { + ...globalCustomSections, + ...projectCustomSections, + } + : { + ...projectCustomSections, + }; }; export const mergeConfigs = (