From 499520238093935eb1e2d2f25c8506946dd25c76 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 3 Oct 2026 09:26:39 +0100 Subject: [PATCH 1/4] build: run publint and attw before publishing and make test delegate to turbo The package shape was never checked before a release. prepublishOnly now runs publint and attw, and the repository URL uses the git+ form publint asks for. The public test script now delegates to turbo like the others, the redundant test:run script is gone, and vitest in watch mode moves to test:watch, which CONTRIBUTING already referred to. --- .github/workflows/ci.yml | 2 +- .github/workflows/cross-platform-tests.yml | 2 +- .husky/pre-push | 2 +- docs-src/guides/cross-platform-testing.md | 6 +- package.json | 10 +- pnpm-lock.yaml | 179 +++++++++++++++++++++ scripts/test-cross-platform.js | 2 +- 7 files changed, 192 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac82a7e..25782e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -286,7 +286,7 @@ jobs: env: MARKMV_TEST_FILESYSTEM_CASE_SENSITIVE: ${{ env.FILESYSTEM_IS_CASE_SENSITIVE }} MARKMV_TEST_SUPPORTS_SYMLINKS: ${{ env.FILESYSTEM_SUPPORTS_SYMLINKS }} - run: pnpm run test:run + run: pnpm run test - name: Report filesystem test results run: | diff --git a/.github/workflows/cross-platform-tests.yml b/.github/workflows/cross-platform-tests.yml index 77595cc..cce642e 100644 --- a/.github/workflows/cross-platform-tests.yml +++ b/.github/workflows/cross-platform-tests.yml @@ -167,7 +167,7 @@ jobs: echo "Symlinks: ${{ steps.filesystem.outputs.symlinks }}" # Run tests with cross-platform environment - pnpm run test:run + pnpm run test env: MARKMV_TEST_CROSS_PLATFORM: "true" diff --git a/.husky/pre-push b/.husky/pre-push index 6f2deaa..cf53f02 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,6 +1,6 @@ # Typecheck and unit tests before push, to catch breakage before it reaches CI. pnpm run typecheck -pnpm run test:run +pnpm run test # actionlint gates CI workflows; warn-and-continue when the binary is missing locally (CI always runs it), so contributors without it are not blocked. if command -v actionlint >/dev/null 2>&1; then diff --git a/docs-src/guides/cross-platform-testing.md b/docs-src/guides/cross-platform-testing.md index ce7da77..533697b 100644 --- a/docs-src/guides/cross-platform-testing.md +++ b/docs-src/guides/cross-platform-testing.md @@ -137,7 +137,7 @@ npm run test:cross-platform:data npm run test:cross-platform:cli # Run with specific environment -MARKMV_TEST_CASE_SENSITIVE=false npm run test:run +MARKMV_TEST_CASE_SENSITIVE=false npm test ``` ### Manual Testing @@ -210,10 +210,10 @@ Enable debug output in tests: ```bash # Enable verbose test output -npm run test:run -- --reporter=verbose +npx vitest run --reporter=verbose # Run specific test file -npm run test:run -- src/utils/test-helpers.test.ts +npx vitest run src/utils/test-helpers.test.ts # Check filesystem capabilities node scripts/test-cross-platform.js --test-data-only diff --git a/package.json b/package.json index c4ab203..b3eb80d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "zod": "4.6.5" }, "devDependencies": { + "@arethetypeswrong/cli": "0.18.5", "@commitlint/cli": "^21.2.3", "@commitlint/config-conventional": "^21.2.3", "@commitlint/types": "^21.2.3", @@ -59,6 +60,7 @@ "knip": "^6.38.0", "lint-staged": "^17.6.0", "prettier": "^3.9.9", + "publint": "0.3.24", "semantic-release": "^25.0.9", "semver": "7.8.5", "tsx": "4.23.15", @@ -112,7 +114,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/ExaDev/markmv.git" + "url": "git+https://github.com/ExaDev/markmv.git" }, "scripts": { "_build": "tsc -p tsconfig.build.json", @@ -136,18 +138,18 @@ "lint:fix": "eslint . --fix", "mcp-server": "node dist/cli.js mcp", "prepare": "husky", - "prepublishOnly": "pnpm run build && pnpm run test:run", + "prepublishOnly": "pnpm run build && pnpm run test && publint && attw --pack --profile esm-only", "publish:dry": "npm pack --dry-run", "publish:npm": "npm publish --access public", "release": "semantic-release", "release:dry": "semantic-release --dry-run", "start": "node dist/cli.js", - "test": "vitest", + "test": "turbo run _test", "test:coverage": "vitest run --coverage", "test:cross-platform": "node scripts/test-cross-platform.js", "test:cross-platform:cli": "node scripts/test-cross-platform.js --cli-only", "test:cross-platform:data": "node scripts/test-cross-platform.js --test-data-only", - "test:run": "turbo run _test", + "test:watch": "vitest", "typecheck": "turbo run _typecheck" }, "type": "module", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fe0f29..5f202e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: specifier: 4.6.5 version: 4.6.5 devDependencies: + "@arethetypeswrong/cli": + specifier: 0.18.5 + version: 0.18.5 "@commitlint/cli": specifier: ^21.2.3 version: 21.2.3(@types/node@26.6.3)(@typescript/typescript6@6.0.2)(conventional-commits-parser@7.1.2) @@ -147,6 +150,9 @@ importers: prettier: specifier: ^3.9.9 version: 3.9.9 + publint: + specifier: 0.3.24 + version: 0.3.24 semantic-release: specifier: ^25.0.9 version: 25.0.9(@typescript/typescript6@6.0.2)(supports-color@7.2.0) @@ -203,6 +209,27 @@ packages: integrity: sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==, } + "@andrewbranch/untar.js@1.0.4": + resolution: + { + integrity: sha512-pVXSwPsLuw8IGLo2Di0EaOfsk+ntVvpkk942J/sHYIkwvtKUakEcPh7HBgZ6tuimgzKSEHgCvO4XgQ05DEbwDw==, + } + + "@arethetypeswrong/cli@0.18.5": + resolution: + { + integrity: sha512-gM+8vRsQOD/Uc7EnBedUhkG5OCsDWE4uoak5QvomGpMpaky0Eh41p04nIMgrWb8EOmqZUJGc6zz9hsP6E56R7g==, + } + engines: { node: ">=20" } + hasBin: true + + "@arethetypeswrong/core@0.18.5": + resolution: + { + integrity: sha512-9ytjzGwxjm9Uz7I9avfbt5vlQt6uk9uRRESzJjqrznl6WKvI6dwYTo+vJ3U02Wrq/mR3iql/PzhvHhKdJIAjDQ==, + } + engines: { node: ">=20" } + "@asamuzakjp/css-color@7.1.0": resolution: { @@ -260,6 +287,12 @@ packages: } engines: { node: ">=18" } + "@braidai/lang@1.1.2": + resolution: + { + integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==, + } + "@bramus/specificity@2.4.2": resolution: { @@ -976,6 +1009,12 @@ packages: integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==, } + "@loaderkit/resolve@1.0.6": + resolution: + { + integrity: sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg==, + } + "@microsoft/tsdoc-config@0.18.2": resolution: { @@ -1500,6 +1539,13 @@ packages: } engines: { node: ">=12" } + "@publint/pack@0.1.7": + resolution: + { + integrity: sha512-4EDEmvxWtgsCnnVeBvtFIFZtUhPPt1+bA9JrSwU4Sa//6oKtzCSlGGXYJr44OD9aGISymbieJ4mCKHUygUDU+g==, + } + engines: { node: ">=18" } + "@rollup/rollup-android-arm-eabi@4.62.4": resolution: { @@ -2804,6 +2850,12 @@ packages: integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==, } + cjs-module-lexer@1.4.3: + resolution: + { + integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==, + } + clean-stack@5.3.0: resolution: { @@ -2892,6 +2944,13 @@ packages: integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, } + commander@10.0.1: + resolution: + { + integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==, + } + engines: { node: ">=14" } + commander@15.0.0: resolution: { @@ -3684,6 +3743,12 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: + { + integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==, + } + figures@2.0.0: resolution: { @@ -4714,6 +4779,14 @@ packages: engines: { node: ">= 18" } hasBin: true + marked@9.1.6: + resolution: + { + integrity: sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==, + } + engines: { node: ">= 16" } + hasBin: true + math-intrinsics@1.1.0: resolution: { @@ -5099,6 +5172,13 @@ packages: integrity: sha512-j5sNQm3VCpQQ7nTqGeOZtoJtV3uKERgCBm9QRhmGRiXiqkf7iRFOkfxdJRZWLkqYY8PNf4cDQF/WfXUYLENrRA==, } + mri@1.2.0: + resolution: + { + integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==, + } + engines: { node: ">=4" } + ms@2.1.3: resolution: { @@ -5696,6 +5776,14 @@ packages: kerberos: optional: true + publint@0.3.24: + resolution: + { + integrity: sha512-9zS56KrKBoqi5Qt8h92uMP8TTM9AYZSgnmCo4u2priMqkOZvQnTsziZ2p5LJ2ywbYkAjoCDp2jda9u4cgFefIw==, + } + engines: { node: ">=18" } + hasBin: true + punycode.js@2.3.1: resolution: { @@ -5896,6 +5984,13 @@ packages: integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==, } + sade@1.8.1: + resolution: + { + integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==, + } + engines: { node: ">=6" } + safe-buffer@5.1.2: resolution: { @@ -6515,6 +6610,14 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" + typescript@5.6.1-rc: + resolution: + { + integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==, + } + engines: { node: ">=14.17" } + hasBin: true + typescript@6.0.3: resolution: { @@ -6687,6 +6790,13 @@ packages: integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==, } + validate-npm-package-name@5.0.1: + resolution: + { + integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==, + } + engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } + vary@1.1.2: resolution: { @@ -7039,6 +7149,29 @@ snapshots: "@actions/io@3.0.2": {} + "@andrewbranch/untar.js@1.0.4": {} + + "@arethetypeswrong/cli@0.18.5": + dependencies: + "@arethetypeswrong/core": 0.18.5 + chalk: 4.1.2 + cli-table3: 0.6.5 + commander: 10.0.1 + marked: 9.1.6 + marked-terminal: 7.3.0(marked@9.1.6) + semver: 7.8.5 + + "@arethetypeswrong/core@0.18.5": + dependencies: + "@andrewbranch/untar.js": 1.0.4 + "@loaderkit/resolve": 1.0.6 + cjs-module-lexer: 1.4.3 + fflate: 0.8.3 + lru-cache: 11.5.3 + semver: 7.8.5 + typescript: 5.6.1-rc + validate-npm-package-name: 5.0.1 + "@asamuzakjp/css-color@7.1.0": dependencies: "@csstools/css-calc": 3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) @@ -7075,6 +7208,8 @@ snapshots: "@bcoe/v8-coverage@1.0.2": {} + "@braidai/lang@1.1.2": {} + "@bramus/specificity@2.4.2": dependencies: css-tree: 3.2.1 @@ -7475,6 +7610,10 @@ snapshots: "@keyv/serialize@1.1.1": {} + "@loaderkit/resolve@1.0.6": + dependencies: + "@braidai/lang": 1.1.2 + "@microsoft/tsdoc-config@0.18.2": dependencies: "@microsoft/tsdoc": 0.17.0 @@ -7720,6 +7859,10 @@ snapshots: "@pnpm/network.ca-file": 1.0.2 config-chain: 1.1.13 + "@publint/pack@0.1.7": + dependencies: + tinyexec: 1.3.1 + "@rollup/rollup-android-arm-eabi@4.62.4": optional: true @@ -8477,6 +8620,8 @@ snapshots: chardet@2.2.0: {} + cjs-module-lexer@1.4.3: {} + clean-stack@5.3.0: dependencies: escape-string-regexp: 5.0.0 @@ -8530,6 +8675,8 @@ snapshots: color-name@1.1.4: {} + commander@10.0.1: {} + commander@15.0.0: {} commander@8.3.0: {} @@ -9082,6 +9229,8 @@ snapshots: optionalDependencies: picomatch: 4.0.7 + fflate@0.8.3: {} + figures@2.0.0: dependencies: escape-string-regexp: 1.0.5 @@ -9671,8 +9820,21 @@ snapshots: node-emoji: 2.2.0 supports-hyperlinks: 3.2.0 + marked-terminal@7.3.0(marked@9.1.6): + dependencies: + ansi-escapes: 7.3.0 + ansi-regex: 6.3.0 + chalk: 5.6.2 + cli-highlight: 2.1.11 + cli-table3: 0.6.5 + marked: 9.1.6 + node-emoji: 2.2.0 + supports-hyperlinks: 3.2.0 + marked@15.0.12: {} + marked@9.1.6: {} + math-intrinsics@1.1.0: {} mdast-util-find-and-replace@3.0.2: @@ -10053,6 +10215,8 @@ snapshots: module-replacements@2.11.0: {} + mri@1.2.0: {} + ms@2.1.3: {} mute-stream@0.0.8: {} @@ -10354,6 +10518,13 @@ snapshots: proxy-agent-negotiate@1.1.0: {} + publint@0.3.24: + dependencies: + "@publint/pack": 0.1.7 + package-manager-detector: 1.8.0 + picocolors: 1.1.1 + sade: 1.8.1 + punycode.js@2.3.1: {} punycode@2.3.1: {} @@ -10523,6 +10694,10 @@ snapshots: dependencies: tslib: 2.8.1 + sade@1.8.1: + dependencies: + mri: 1.2.0 + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -10900,6 +11075,8 @@ snapshots: transitivePeerDependencies: - supports-color + typescript@5.6.1-rc: {} + typescript@6.0.3: {} typescript@7.0.2: @@ -11003,6 +11180,8 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + validate-npm-package-name@5.0.1: {} + vary@1.1.2: {} vfile-message@4.0.3: diff --git a/scripts/test-cross-platform.js b/scripts/test-cross-platform.js index 94950d6..d496384 100644 --- a/scripts/test-cross-platform.js +++ b/scripts/test-cross-platform.js @@ -106,7 +106,7 @@ function runTests() { try { // Run the test suite with our environment - execSync("npm run test:run", { + execSync("npm test", { env: testEnv, stdio: "inherit", }); From cfe01c60ff3035715cc04605ef665c0af4d46ae5 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 3 Oct 2026 09:08:59 +0100 Subject: [PATCH 2/4] build: key the build and typecheck caches on every tsconfig The tasks read tsconfig.build.json and tsconfig.lint.json through tsc but listed only tsconfig.json as an input, so editing either restored a stale cached result. --- turbo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/turbo.json b/turbo.json index 659b944..211209c 100644 --- a/turbo.json +++ b/turbo.json @@ -2,7 +2,7 @@ "$schema": "https://turborepo.dev/schema.json", "tasks": { "_build": { - "inputs": ["src/**/*.ts", "tsconfig.json", "tsconfig.build.json"], + "inputs": ["src/**/*.ts", "tsconfig*.json"], "outputs": ["dist/**", "tsconfig.tsbuildinfo"] }, "_check": { @@ -23,7 +23,7 @@ "outputs": ["coverage/**"] }, "_typecheck": { - "inputs": ["src/**/*.ts", "tsconfig.json"], + "inputs": ["src/**/*.ts", "tsconfig*.json"], "outputs": [] } } From 7f3f836d209834f64490bfa1d7f513dc809743b9 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 3 Oct 2026 09:09:10 +0100 Subject: [PATCH 3/4] test: enforce coverage thresholds that vitest actually reads The thresholds sat under a global key vitest does not read, so they enforced nothing, and the exclude list used bare directory names that miss nested copies. The floors are the measured coverage rounded down, and the excludes match at any depth. --- vitest.config.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/vitest.config.ts b/vitest.config.ts index cc05625..86366df 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -64,13 +64,12 @@ export default defineConfig({ "release.config.*", ], include: ["src/**/*.ts"], + // Floors set at the coverage measured when they were introduced, rounded down, so coverage can only be held or raised. A `global` key here is not one Vitest reads, which is how the earlier thresholds came to enforce nothing. thresholds: { - global: { - branches: 40, - functions: 40, - lines: 40, - statements: 40, - }, + branches: 69, + functions: 86, + lines: 80, + statements: 80, }, }, include: [ @@ -79,6 +78,6 @@ export default defineConfig({ ".github/scripts/**/*.test.ts", "scripts/**/*.test.ts", ], - exclude: ["node_modules/", "dist/", "coverage/"], + exclude: ["**/node_modules/**", "**/dist/**", "**/coverage/**"], }, }); From 281754003dd2aff89d3c4a8bc3fd771e4c93984e Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 3 Oct 2026 09:09:27 +0100 Subject: [PATCH 4/4] chore(lint): use the shared config's turbo, tooling wiring and import policy Turns on the turbo script and cache rules, the publish-time tooling checks and hooks check, and an import policy that keeps test helpers out of shipped code. The root tooling check is off because it cannot hold together with the turbo convention for knip. --- eslint.config.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/eslint.config.ts b/eslint.config.ts index cdb833b..255c244 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -22,7 +22,29 @@ const publicModules = [ ]; export default defineConfig( - ...exadevConfig(), + ...exadevConfig({ + turbo: {}, + // The `root` section is switched off on purpose: its knip check expects a script that runs knip itself, which the turbo convention (a public script delegating to `_knip`) rules out, so both cannot hold in a turbo repository (ExaDev/eslint-config#110). + toolingWiring: { + root: false, + hooks: true, + publish: { tools: ["publint", "attw"] }, + }, + importPolicies: [ + { + files: ["src/**"], + ignores: ["**/*.test.ts", "src/test-support/**"], + deny: [ + { + specifiers: ["src/test-support", "src/utils/test-helpers"], + message: + "test doubles and helpers stay out of shipped code; import them from tests only", + }, + ], + computedSpecifiers: "report", + }, + ], + }), { // Tracked but still not meant to be linted or reformatted: .d.ts is generated at build time, both lockfiles are machine-written, and CHANGELOG.md is entirely semantic-release output rewritten wholesale on every release -- all three are tracked, so .gitignore doesn't exclude them, and reformatting CHANGELOG.md by hand here would just be undone (noisily) by the next release anyway. ignores: [