@workerstack/repo 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +0 -3
- package/agents/global-settings/antigravity.json +3 -0
- package/agents/global-settings/claude.json +6 -0
- package/agents/global-settings/codex.toml +2 -0
- package/agents/global-settings/cursor.json +3 -0
- package/agents/global-settings/grok.toml +36 -0
- package/agents/global-settings/manifest.json +36 -0
- package/agents/grok/hooks/orchestrate-parent.json +60 -0
- package/agents/grok/hooks/rtk.json +16 -0
- package/agents/grok/hooks/rulesync.json +3 -0
- package/agents/grok/roles/scout.toml +5 -0
- package/agents/hooks.jsonc +67 -0
- package/agents/mcp-policy.jsonc +16 -0
- package/agents/rules/overview.md +21 -0
- package/agents/settings/claude.json +5 -0
- package/agents/skills/ask/SKILL.md +13 -0
- package/agents/skills/auto/SKILL.md +13 -0
- package/agents/skills/away/SKILL.md +19 -0
- package/agents/skills/blast-radius/SKILL.md +54 -0
- package/agents/skills/bro/SKILL.md +9 -0
- package/agents/skills/browser/SKILL.md +16 -0
- package/agents/skills/clasp/SKILL.md +11 -0
- package/agents/skills/commit/SKILL.md +35 -0
- package/agents/skills/commit-and-push/SKILL.md +9 -0
- package/agents/skills/commit-push/SKILL.md +59 -0
- package/agents/skills/commit-push-deploy/SKILL.md +74 -0
- package/agents/skills/commit-push-release/SKILL.md +75 -0
- package/agents/skills/correct/SKILL.md +39 -0
- package/agents/skills/do/SKILL.md +21 -0
- package/agents/skills/explain/SKILL.md +13 -0
- package/agents/skills/google-workspace/SKILL.md +12 -0
- package/agents/skills/handoff/SKILL.md +81 -0
- package/agents/skills/implement-plan/SKILL.md +29 -0
- package/agents/skills/implement-roadmap/SKILL.md +27 -0
- package/agents/skills/jev/SKILL.md +17 -0
- package/agents/skills/listen/SKILL.md +15 -0
- package/agents/skills/pickup/SKILL.md +58 -0
- package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
- package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
- package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
- package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
- package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
- package/agents/skills/principle-experience-first/SKILL.md +22 -0
- package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
- package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
- package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
- package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
- package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
- package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
- package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
- package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
- package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
- package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
- package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
- package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
- package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
- package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
- package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
- package/agents/skills/review-plan/SKILL.md +28 -0
- package/agents/skills/review-roadmap/SKILL.md +28 -0
- package/agents/skills/show-me/SKILL.md +21 -0
- package/agents/skills/speak/SKILL.md +15 -0
- package/agents/skills/su/SKILL.md +11 -0
- package/agents/skills/teach/SKILL.md +25 -0
- package/agents/skills/tmp/SKILL.md +54 -0
- package/agents/skills/typescript-best-practices/SKILL.md +32 -0
- package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
- package/agents/skills/unslop/SKILL.md +62 -0
- package/agents/skills/write-plan/SKILL.md +33 -0
- package/agents/skills/write-roadmap/SKILL.md +66 -0
- package/agents/subagents/implementer-deep.md +57 -0
- package/agents/subagents/implementer-fast.md +57 -0
- package/agents/subagents/orchestrator.md +43 -0
- package/agents/subagents/researcher.md +47 -0
- package/agents/subagents/scout.md +44 -0
- package/bin/repo.ts +4 -0
- package/cli/actionlint-default.test.ts +38 -0
- package/cli/actionlint-download.test.ts +162 -0
- package/cli/actionlint-real.test.ts +33 -0
- package/cli/actionlint.constants.ts +43 -0
- package/cli/actionlint.test.ts +108 -0
- package/cli/actionlint.ts +178 -0
- package/cli/bin.test.ts +54 -0
- package/cli/bin.ts +40 -0
- package/cli/check-changelog.test.ts +40 -0
- package/cli/check-changelog.ts +12 -0
- package/cli/check-comments.test.ts +157 -0
- package/cli/check-comments.ts +123 -0
- package/cli/check-env-modules.test.ts +85 -0
- package/cli/check-env-modules.ts +56 -0
- package/cli/check-magic.test.ts +139 -0
- package/cli/check-magic.ts +120 -0
- package/cli/check-manifests.constants.ts +4 -0
- package/cli/check-manifests.test.ts +210 -0
- package/cli/check-manifests.ts +71 -0
- package/cli/check-nightly.test.ts +103 -0
- package/cli/check-nightly.ts +37 -0
- package/cli/check-ratchets-missing.test.ts +46 -0
- package/cli/check-ratchets-mutation.test.ts +49 -0
- package/cli/check-ratchets.constants.ts +20 -0
- package/cli/check-ratchets.test.ts +190 -0
- package/cli/check-ratchets.ts +193 -0
- package/cli/check-rejections.test.ts +220 -0
- package/cli/check-rejections.ts +153 -0
- package/cli/check-reports.ts +37 -0
- package/cli/check-tsconfig.test.ts +507 -0
- package/cli/check-tsconfig.ts +205 -0
- package/cli/commands.test.ts +114 -0
- package/cli/commands.ts +218 -0
- package/cli/commit-changelog.ts +27 -0
- package/cli/commit-msg-changelog.test.ts +93 -0
- package/cli/commit-msg-range.test.ts +126 -0
- package/cli/commit-msg-red-proof.test.ts +147 -0
- package/cli/commit-msg-repo.test.ts +153 -0
- package/cli/commit-msg.test.ts +101 -0
- package/cli/commit-msg.ts +194 -0
- package/cli/commit-range.constants.ts +17 -0
- package/cli/commit-range.ts +57 -0
- package/cli/config-knip.test.ts +70 -0
- package/cli/config-nightly.test.ts +84 -0
- package/cli/config-tdd.test.ts +46 -0
- package/cli/config.test.ts +576 -0
- package/cli/config.ts +261 -0
- package/cli/conventional.ts +9 -0
- package/cli/coverage-config.ts +62 -0
- package/cli/coverage.constants.ts +6 -0
- package/cli/coverage.test.ts +199 -0
- package/cli/coverage.ts +127 -0
- package/cli/env.constants.ts +10 -0
- package/cli/env.test.ts +56 -0
- package/cli/env.ts +72 -0
- package/cli/fixtures/base-config.ts +13 -0
- package/cli/fixtures/gate-world.ts +48 -0
- package/cli/fixtures/knip-run.ts +38 -0
- package/cli/fixtures/mutate-fakes.ts +137 -0
- package/cli/fixtures/planted-repo.ts +50 -0
- package/cli/fixtures/publish-world.ts +71 -0
- package/cli/fixtures/red-proof-repo.ts +72 -0
- package/cli/fixtures/release-context.ts +15 -0
- package/cli/fixtures/release-world.ts +111 -0
- package/cli/fixtures/stub-bin.ts +40 -0
- package/cli/guard-diagnostics.test.ts +157 -0
- package/cli/guard-diagnostics.ts +195 -0
- package/cli/init.test.ts +487 -0
- package/cli/init.ts +275 -0
- package/cli/knip-check-ignores.test.ts +106 -0
- package/cli/knip-check-real.test.ts +65 -0
- package/cli/knip-check.constants.ts +46 -0
- package/cli/knip-check.test.ts +114 -0
- package/cli/knip-check.ts +169 -0
- package/cli/knip-schema.ts +67 -0
- package/cli/known-failures.constants.ts +22 -0
- package/cli/known-failures.test.ts +138 -0
- package/cli/known-failures.ts +131 -0
- package/cli/lint/as-needs-reason.ts +41 -0
- package/cli/lint/boundaries.ts +73 -0
- package/cli/lint/class-file.ts +39 -0
- package/cli/lint/config-file.ts +32 -0
- package/cli/lint/entry-file.ts +54 -0
- package/cli/lint/env-in-config.ts +66 -0
- package/cli/lint/filename-matches-export.ts +63 -0
- package/cli/lint/index-file.ts +22 -0
- package/cli/lint/index.ts +47 -0
- package/cli/lint/is-reason.ts +6 -0
- package/cli/lint/manifest.ts +46 -0
- package/cli/lint/names.ts +59 -0
- package/cli/lint/no-commented-out-code.ts +105 -0
- package/cli/lint/no-mock-of-subject.ts +64 -0
- package/cli/lint/no-module-side-effects.ts +94 -0
- package/cli/lint/no-module-state.ts +58 -0
- package/cli/lint/no-side-effect-import.ts +17 -0
- package/cli/lint/no-v8-ignore.ts +18 -0
- package/cli/lint/script-rules.ts +9 -0
- package/cli/lint/skip-on-platform.constants.ts +8 -0
- package/cli/lint/skip-on-platform.ts +118 -0
- package/cli/lint/todo-needs-id.ts +63 -0
- package/cli/lint/types-file.ts +28 -0
- package/cli/lint-schema.ts +32 -0
- package/cli/magic/fixtures/magic-repo.ts +64 -0
- package/cli/magic/magic-hit.ts +2 -0
- package/cli/magic/magic-nodes.ts +44 -0
- package/cli/magic/magic-numbers.test.ts +99 -0
- package/cli/magic/magic-numbers.ts +117 -0
- package/cli/magic/magic-strings.test.ts +38 -0
- package/cli/magic/magic-strings.ts +81 -0
- package/cli/main.test.ts +853 -0
- package/cli/main.ts +292 -0
- package/cli/mutate-args.ts +79 -0
- package/cli/mutate-check.test.ts +147 -0
- package/cli/mutate-deps.ts +42 -0
- package/cli/mutate-edges.test.ts +150 -0
- package/cli/mutate-files.test.ts +100 -0
- package/cli/mutate-files.ts +31 -0
- package/cli/mutate-floors.test.ts +91 -0
- package/cli/mutate-floors.ts +104 -0
- package/cli/mutate-real-deps.ts +57 -0
- package/cli/mutate-real.test.ts +96 -0
- package/cli/mutate-report.ts +96 -0
- package/cli/mutate-run.ts +128 -0
- package/cli/mutate-shards.ts +64 -0
- package/cli/mutate.constants.ts +67 -0
- package/cli/mutate.test.ts +182 -0
- package/cli/mutate.ts +64 -0
- package/cli/nested-vitest-flags.ts +8 -0
- package/cli/nightly-config.constants.ts +14 -0
- package/cli/nightly-config.ts +62 -0
- package/cli/prompt.test.ts +202 -0
- package/cli/prompt.ts +1 -0
- package/cli/red-proof-edges.test.ts +78 -0
- package/cli/red-proof-failures.test.ts +82 -0
- package/cli/red-proof-tree.test.ts +157 -0
- package/cli/red-proof-verdict.test.ts +44 -0
- package/cli/red-proof.constants.ts +14 -0
- package/cli/red-proof.ts +202 -0
- package/cli/release/changelog-parse.ts +83 -0
- package/cli/release/changelog.test.ts +141 -0
- package/cli/release/changelog.ts +106 -0
- package/cli/release/gate-refusals.test.ts +85 -0
- package/cli/release/gate.test.ts +63 -0
- package/cli/release/gate.ts +70 -0
- package/cli/release/git.ts +66 -0
- package/cli/release/github.test.ts +128 -0
- package/cli/release/github.ts +46 -0
- package/cli/release/list-dir.test.ts +11 -0
- package/cli/release/list-dir.ts +6 -0
- package/cli/release/main.test.ts +48 -0
- package/cli/release/main.ts +51 -0
- package/cli/release/manifest.test.ts +24 -0
- package/cli/release/manifest.ts +34 -0
- package/cli/release/pack.test.ts +107 -0
- package/cli/release/pack.ts +76 -0
- package/cli/release/plan.ts +127 -0
- package/cli/release/publish-run.test.ts +98 -0
- package/cli/release/publish.test.ts +127 -0
- package/cli/release/publish.ts +125 -0
- package/cli/release/refuse.ts +5 -0
- package/cli/release/release-messages.constants.ts +96 -0
- package/cli/release/release-refusals.test.ts +149 -0
- package/cli/release/release.constants.ts +128 -0
- package/cli/release/release.test.ts +199 -0
- package/cli/release/release.ts +145 -0
- package/cli/release/version.test.ts +118 -0
- package/cli/release/version.ts +69 -0
- package/cli/release/wait.test.ts +101 -0
- package/cli/release/wait.ts +95 -0
- package/cli/script-diagnostics.test.ts +76 -0
- package/cli/script-diagnostics.ts +62 -0
- package/cli/scripts/board.ts +519 -0
- package/cli/scripts/browser.ts +994 -0
- package/cli/scripts/change-set.ts +105 -0
- package/cli/scripts/check-globals.ts +199 -0
- package/cli/scripts/ci-skips.constants.ts +17 -0
- package/cli/scripts/ci-skips.ts +118 -0
- package/cli/scripts/ci-wait-run.ts +28 -0
- package/cli/scripts/ci-wait.constants.ts +44 -0
- package/cli/scripts/ci-wait.ts +175 -0
- package/cli/scripts/ci.constants.ts +15 -0
- package/cli/scripts/ci.ts +208 -0
- package/cli/scripts/clean.ts +363 -0
- package/cli/scripts/comments-step.ts +15 -0
- package/cli/scripts/doctor.ts +828 -0
- package/cli/scripts/emit-agent-shell-tools.ts +91 -0
- package/cli/scripts/emit-mcp-policy.ts +402 -0
- package/cli/scripts/folder-map.ts +353 -0
- package/cli/scripts/gate-fat-tools.ts +1095 -0
- package/cli/scripts/gitignore-guard.ts +338 -0
- package/cli/scripts/google-workspace.ts +779 -0
- package/cli/scripts/hook-command.ts +72 -0
- package/cli/scripts/hook-input.ts +178 -0
- package/cli/scripts/hook-io.constants.ts +5 -0
- package/cli/scripts/hook-io.ts +46 -0
- package/cli/scripts/hooks-union.ts +469 -0
- package/cli/scripts/interactive.ts +30 -0
- package/cli/scripts/jev.ts +420 -0
- package/cli/scripts/job.ts +934 -0
- package/cli/scripts/jsonc.ts +9 -0
- package/cli/scripts/lessons.ts +564 -0
- package/cli/scripts/lint-baseline.ts +233 -0
- package/cli/scripts/lint-main.ts +24 -0
- package/cli/scripts/lint-skips.ts +139 -0
- package/cli/scripts/lint.constants.ts +6 -0
- package/cli/scripts/lint.ts +142 -0
- package/cli/scripts/listen.ts +962 -0
- package/cli/scripts/managed-text.ts +47 -0
- package/cli/scripts/orchestrate-parent.ts +1155 -0
- package/cli/scripts/parse-need.ts +81 -0
- package/cli/scripts/persist-handoff.ts +228 -0
- package/cli/scripts/persist-implementation.ts +276 -0
- package/cli/scripts/persist-plan.ts +888 -0
- package/cli/scripts/persist-roadmap.ts +478 -0
- package/cli/scripts/persist-tmp.ts +177 -0
- package/cli/scripts/persist.ts +54 -0
- package/cli/scripts/phase-files.ts +121 -0
- package/cli/scripts/prompt.ts +38 -0
- package/cli/scripts/purge-global-mcps.ts +313 -0
- package/cli/scripts/rejections-step.ts +37 -0
- package/cli/scripts/rtk-grok.ts +102 -0
- package/cli/scripts/scrub-spawn-prompt.ts +103 -0
- package/cli/scripts/scrub-text-lib.ts +58 -0
- package/cli/scripts/session-map.ts +69 -0
- package/cli/scripts/setup.ts +862 -0
- package/cli/scripts/skill-conflicts.ts +82 -0
- package/cli/scripts/speak.ts +370 -0
- package/cli/scripts/strategy.ts +168 -0
- package/cli/scripts/sync-global-settings.ts +316 -0
- package/cli/scripts/turn-steps.ts +249 -0
- package/cli/scripts/turn.ts +157 -0
- package/cli/scripts/typecheck-step.ts +43 -0
- package/cli/scripts/vscode-tasks.ts +175 -0
- package/cli/scripts/workspace-root.ts +160 -0
- package/cli/scripts/worktree.ts +806 -0
- package/cli/spawn.test.ts +34 -0
- package/cli/spawn.ts +27 -0
- package/cli/staged-change.constants.ts +11 -0
- package/cli/staged-change.ts +79 -0
- package/cli/stryker-preload.test.ts +50 -0
- package/cli/stryker-preload.ts +9 -0
- package/cli/sync/agents/frontmatter.test.ts +197 -0
- package/cli/sync/agents/frontmatter.ts +140 -0
- package/cli/sync/agents/hooks.test.ts +517 -0
- package/cli/sync/agents/hooks.ts +401 -0
- package/cli/sync/agents/rules.test.ts +229 -0
- package/cli/sync/agents/rules.ts +84 -0
- package/cli/sync/agents/sources.test.ts +375 -0
- package/cli/sync/agents/sources.ts +244 -0
- package/cli/sync/agents/subagents.test.ts +149 -0
- package/cli/sync/agents/subagents.ts +158 -0
- package/cli/sync/agents-text.test.ts +31 -0
- package/cli/sync/agents-text.ts +36 -0
- package/cli/sync/fixtures/copied.README.md +35 -0
- package/cli/sync/fixtures/copied.tgz +0 -0
- package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
- package/cli/sync/fixtures/maps-ws.tgz +0 -0
- package/cli/sync/git-files.test.ts +340 -0
- package/cli/sync/git-files.ts +197 -0
- package/cli/sync/git-hooks.test.ts +94 -0
- package/cli/sync/golden.test.ts +15 -0
- package/cli/sync/hooks-mode.test.ts +99 -0
- package/cli/sync/hosts.ts +28 -0
- package/cli/sync/knip-config.constants.ts +4 -0
- package/cli/sync/knip-config.test.ts +147 -0
- package/cli/sync/knip-config.ts +108 -0
- package/cli/sync/manifest.test.ts +78 -0
- package/cli/sync/manifest.ts +60 -0
- package/cli/sync/maps.test.ts +304 -0
- package/cli/sync/maps.ts +297 -0
- package/cli/sync/migrate.test.ts +2000 -0
- package/cli/sync/migrate.ts +1104 -0
- package/cli/sync/nightly-jobs.test.ts +63 -0
- package/cli/sync/nightly.constants.ts +48 -0
- package/cli/sync/nightly.test.ts +100 -0
- package/cli/sync/nightly.ts +75 -0
- package/cli/sync/outputs.test.ts +351 -0
- package/cli/sync/outputs.ts +168 -0
- package/cli/sync/oxlint-config.test.ts +178 -0
- package/cli/sync/oxlint-config.ts +168 -0
- package/cli/sync/release-jobs.test.ts +29 -0
- package/cli/sync/release-reports.test.ts +31 -0
- package/cli/sync/release-workflow.constants.ts +61 -0
- package/cli/sync/release-workflow.test.ts +192 -0
- package/cli/sync/release-workflow.ts +86 -0
- package/cli/sync/release.constants.ts +93 -0
- package/cli/sync/release.test.ts +187 -0
- package/cli/sync/release.ts +112 -0
- package/cli/sync/repo-dir.test.ts +371 -0
- package/cli/sync/repo-dir.ts +205 -0
- package/cli/sync/standards.constants.ts +8 -0
- package/cli/sync/standards.test.ts +206 -0
- package/cli/sync/standards.ts +179 -0
- package/cli/sync/stock-history.json +2796 -0
- package/cli/sync/stock-history.test.ts +66 -0
- package/cli/sync/stock-history.ts +84 -0
- package/cli/sync/stock.test.ts +47 -0
- package/cli/sync/stock.ts +24 -0
- package/cli/sync/trees.test.ts +621 -0
- package/cli/sync/trees.ts +280 -0
- package/cli/sync/validate.test.ts +299 -0
- package/cli/sync/validate.ts +402 -0
- package/cli/sync/workspace.test.ts +180 -0
- package/cli/sync/workspace.ts +138 -0
- package/cli/sync/write.test.ts +341 -0
- package/cli/sync/write.ts +231 -0
- package/cli/sync.test.ts +1495 -0
- package/cli/sync.ts +360 -0
- package/cli/tdd-scope.test.ts +36 -0
- package/cli/tdd-scope.ts +41 -0
- package/cli/test-files.ts +16 -0
- package/cli/ts-program.ts +69 -0
- package/cli/tsconfig.constants.ts +8 -0
- package/cli/workerstack-config.types.ts +21 -0
- package/dist/cli/env.d.ts +10 -0
- package/dist/cli/env.js +64 -0
- package/dist/presets/temp-root-env.d.ts +10 -0
- package/dist/presets/temp-root-env.js +20 -0
- package/dist/presets/temp-root.constants.d.ts +6 -0
- package/dist/presets/temp-root.constants.js +6 -0
- package/dist/presets/temp-root.d.ts +2 -0
- package/dist/presets/temp-root.js +60 -0
- package/dist/presets/vitest.constants.d.ts +16 -0
- package/dist/presets/vitest.constants.js +16 -0
- package/dist/presets/vitest.d.ts +11 -0
- package/dist/presets/vitest.js +67 -0
- package/dist/presets/workers-plugin.d.ts +10 -0
- package/dist/presets/workers-plugin.js +54 -0
- package/package.json +141 -4
- package/references/INDEX.md +100 -0
- package/references/browser.md +66 -0
- package/references/cascade.md +15 -0
- package/references/clasp.md +21 -0
- package/references/cloudflare.md +36 -0
- package/references/google-workspace.md +79 -0
- package/references/hooks.md +103 -0
- package/references/implementation-format.md +119 -0
- package/references/jev.md +56 -0
- package/references/lessons.md +73 -0
- package/references/listen.md +80 -0
- package/references/map.md +107 -0
- package/references/output.md +47 -0
- package/references/plan-format.md +264 -0
- package/references/review-loop.md +48 -0
- package/references/roadmap-format.md +104 -0
- package/references/rtk.md +27 -0
- package/references/scope.md +76 -0
- package/references/setup.md +42 -0
- package/references/speak.md +59 -0
- package/tsconfig/base.json +28 -0
- package/tsconfig/bun.d.ts +1 -0
- package/tsconfig/bun.json +8 -0
- package/tsconfig/reset.d.ts +1 -0
- package/tsconfig/strict.json +12 -0
- package/tsconfig/web.json +7 -0
- package/tsconfig/worker.d.ts +1 -0
- package/tsconfig/worker.json +8 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `repo check`'s knip lines: one knip run against the generated config without its ignores, every issue it finds a
|
|
3
|
+
* problem unless a `knip.ignore` entry covers it, each entry reported with its reason, and an entry that matches
|
|
4
|
+
* nothing (before or after the run) refused. The config is written beside `knip.json` for the run and named for the
|
|
5
|
+
* process, so two checks never remove each other's copy.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { findRoot, loadConfig } from "./config.ts";
|
|
11
|
+
import {
|
|
12
|
+
BINARIES_TYPE,
|
|
13
|
+
DEPENDENCY_TYPES,
|
|
14
|
+
EXIT_BAD_CONFIG,
|
|
15
|
+
FILES_TYPE,
|
|
16
|
+
MANIFEST_FIELDS,
|
|
17
|
+
STDERR_LINES,
|
|
18
|
+
WORDINGS,
|
|
19
|
+
type Wording,
|
|
20
|
+
} from "./knip-check.constants.ts";
|
|
21
|
+
import type { KnipIgnore } from "./knip-schema.ts";
|
|
22
|
+
import { captureBun, type Captured } from "./scripts/lint.ts";
|
|
23
|
+
import { knipConfig } from "./sync/knip-config.ts";
|
|
24
|
+
import { readWorkspace, type Workspace } from "./sync/workspace.ts";
|
|
25
|
+
|
|
26
|
+
/** What `repo check` prints of knip: report lines that pass, problem lines that fail it. */
|
|
27
|
+
export type KnipCheck = { report: string[]; problems: string[] };
|
|
28
|
+
/** Runs knip with `args` in `cwd`; the default spawns the knip this package installs. */
|
|
29
|
+
export type KnipRun = (args: string[], cwd: string) => Captured;
|
|
30
|
+
/** The knip runner `checkKnipAt` uses unless a test injects another. */
|
|
31
|
+
export type KnipDeps = { run?: KnipRun };
|
|
32
|
+
/** One knip finding: its file, how its type is worded, name and namespace (an enum's, say). */
|
|
33
|
+
type Issue = { file: string; wording: Wording; name: string; namespace: string | undefined };
|
|
34
|
+
|
|
35
|
+
const NONE: KnipCheck = { report: [], problems: [] };
|
|
36
|
+
const FIX_CONFIG = "run `bun repo sync` and check knip.json";
|
|
37
|
+
const entrySchema = () => z.object({ name: z.string(), namespace: z.string().optional() });
|
|
38
|
+
const reportSchema = () => z.object({ issues: z.array(z.looseObject({ file: z.string() })) });
|
|
39
|
+
const manifestSchema = () => z.object({ bin: z.object({ knip: z.string() }) });
|
|
40
|
+
|
|
41
|
+
const unignoredPath = (): string => `knip.unignored-${process.pid}.json`;
|
|
42
|
+
|
|
43
|
+
/** The path of the knip bin in this package's install. */
|
|
44
|
+
function knipBin(): string {
|
|
45
|
+
const manifest = Bun.resolveSync("knip/package.json", import.meta.dirname);
|
|
46
|
+
const { bin } = manifestSchema().parse(JSON.parse(fs.readFileSync(manifest, "utf8")));
|
|
47
|
+
return path.join(path.dirname(manifest), bin.knip);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The knip runner for real: the resolved bin under bun, with no `GIT_*` variable. */
|
|
51
|
+
const runKnip: KnipRun = (args, cwd) => captureBun([knipBin(), ...args], cwd);
|
|
52
|
+
|
|
53
|
+
/** The issues in knip's json report, in its order; null when `stdout` is not that report. */
|
|
54
|
+
function issuesOf(stdout: string): Issue[] | null {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = reportSchema().parse(JSON.parse(stdout));
|
|
57
|
+
return parsed.issues.flatMap((entry) =>
|
|
58
|
+
WORDINGS.flatMap(
|
|
59
|
+
(wording) =>
|
|
60
|
+
z
|
|
61
|
+
.array(entrySchema())
|
|
62
|
+
.optional()
|
|
63
|
+
.parse(entry[wording.type])
|
|
64
|
+
?.map(({ name, namespace }) => ({ file: entry.file, wording, name, namespace })) ?? [],
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `knip: <file>: <what is wrong>; <the fix>`. */
|
|
73
|
+
function lineOf({ file, wording, name, namespace }: Issue): string {
|
|
74
|
+
const { type, label, fix } = wording;
|
|
75
|
+
const subject = type === FILES_TYPE ? label : `${label} "${namespace === undefined ? name : `${namespace}.${name}`}"`;
|
|
76
|
+
return `knip: ${file}: ${subject}; ${fix}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Whether `ignore` covers `issue`. */
|
|
80
|
+
function covers(ignore: KnipIgnore, issue: Issue): boolean {
|
|
81
|
+
if ("file" in ignore) return new Bun.Glob(ignore.file).match(issue.file);
|
|
82
|
+
if ("dependency" in ignore) return DEPENDENCY_TYPES.includes(issue.wording.type) && issue.name === ignore.dependency;
|
|
83
|
+
return issue.wording.type === BINARIES_TYPE && issue.name === ignore.binary;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** `file "x"`, `dependency "x"` or `binary "x"`. */
|
|
87
|
+
function kindName(ignore: KnipIgnore): [string, string] {
|
|
88
|
+
if ("file" in ignore) return ["file", ignore.file];
|
|
89
|
+
if ("dependency" in ignore) return ["dependency", ignore.dependency];
|
|
90
|
+
return ["binary", ignore.binary];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Why `ignore` is stale before knip runs, or null: its file is gone, or no package.json declares its dependency. */
|
|
94
|
+
function staleBefore(workspace: Workspace, ignore: KnipIgnore): string | null {
|
|
95
|
+
if ("file" in ignore) {
|
|
96
|
+
const found = fs.globSync(ignore.file, { cwd: workspace.root }).length > 0;
|
|
97
|
+
return found ? null : `${ignore.file} no longer exists; delete the entry`;
|
|
98
|
+
}
|
|
99
|
+
if (!("dependency" in ignore)) return null;
|
|
100
|
+
const manifests = [workspace.manifest, ...workspace.packages.map((pkg) => pkg.manifest)];
|
|
101
|
+
const declared = manifests.some((manifest) =>
|
|
102
|
+
MANIFEST_FIELDS.some((f) => manifest[f]?.[ignore.dependency] !== undefined),
|
|
103
|
+
);
|
|
104
|
+
return declared ? null : `dependency "${ignore.dependency}" is in no package.json; delete the entry`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The last lines of knip's stderr, on one line. */
|
|
108
|
+
const tail = (stderr: string): string => stderr.trim().split("\n").slice(-STDERR_LINES).join(" ");
|
|
109
|
+
|
|
110
|
+
/** Runs knip on the unignored config, written for the run only. */
|
|
111
|
+
function runUnignored(workspace: Workspace, run: KnipRun): Captured {
|
|
112
|
+
const name = unignoredPath();
|
|
113
|
+
const file = path.join(workspace.root, name);
|
|
114
|
+
try {
|
|
115
|
+
fs.writeFileSync(file, knipConfig(workspace, []));
|
|
116
|
+
return run(["--config", name, "--no-exit-code", "--reporter", "json"], workspace.root);
|
|
117
|
+
} finally {
|
|
118
|
+
fs.rmSync(file, { force: true });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The issues no entry of `ignores` covers, and how many each entry covers (an issue two cover counts for both). */
|
|
123
|
+
function applyIgnores(ignores: readonly KnipIgnore[], issues: readonly Issue[]): { open: Issue[]; hits: number[] } {
|
|
124
|
+
const hits = ignores.map((ignore) => issues.filter((issue) => covers(ignore, issue)).length);
|
|
125
|
+
return { open: issues.filter((issue) => !ignores.some((ignore) => covers(ignore, issue))), hits };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The report line for the entries that covered something, and a problem for each live entry that covered nothing. */
|
|
129
|
+
function ignoreLines(ignores: readonly KnipIgnore[], hits: readonly number[], stale: readonly (string | null)[]) {
|
|
130
|
+
const unused = ignores.flatMap((ignore, i) => {
|
|
131
|
+
const [kind, name] = kindName(ignore);
|
|
132
|
+
const gone = hits[i] === 0 && stale[i] === null;
|
|
133
|
+
return gone ? [`knip.ignore[${i}]: ${kind} "${name}" has no knip issue any more; delete the entry`] : [];
|
|
134
|
+
});
|
|
135
|
+
const used = ignores.filter((_, i) => hits[i] !== 0);
|
|
136
|
+
const shown = used.map((ignore) => `${kindName(ignore).join(" ")} (${ignore.reason})`);
|
|
137
|
+
const line = `knip: ${used.length} ${used.length === 1 ? "ignore" : "ignores"}: ${shown.join(", ")}`;
|
|
138
|
+
return { report: used.length === 0 ? [] : [line], unused };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Every issue knip finds in `workspace` is a problem unless an entry of `ignores` covers it. Each covering entry is
|
|
143
|
+
* reported with its reason; one that matches nothing, before or after the run, is a problem.
|
|
144
|
+
*/
|
|
145
|
+
export function checkKnip(workspace: Workspace, ignores: readonly KnipIgnore[], run: KnipRun): KnipCheck {
|
|
146
|
+
const before = ignores.map((ignore) => staleBefore(workspace, ignore));
|
|
147
|
+
const stale = before.flatMap((why, i) => (why === null ? [] : [`knip.ignore[${i}]: ${why}`]));
|
|
148
|
+
const ran = runUnignored(workspace, run);
|
|
149
|
+
const issues = issuesOf(ran.stdout);
|
|
150
|
+
if (ran.code === EXIT_BAD_CONFIG || issues === null)
|
|
151
|
+
return { report: [], problems: [...stale, `knip: exited ${ran.code}: ${tail(ran.stderr)}; ${FIX_CONFIG}`] };
|
|
152
|
+
const { open, hits } = applyIgnores(ignores, issues);
|
|
153
|
+
const { report, unused } = ignoreLines(ignores, hits, before);
|
|
154
|
+
return { report, problems: [...stale, ...open.map(lineOf), ...unused] };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* `repo check`'s knip lines for the repo at `cwd`: none without a config, the standards pack or a readable
|
|
159
|
+
* workspace.
|
|
160
|
+
*/
|
|
161
|
+
export async function checkKnipAt(cwd: string, deps: KnipDeps = {}): Promise<KnipCheck> {
|
|
162
|
+
const root = findRoot(cwd);
|
|
163
|
+
if (root === null) return NONE;
|
|
164
|
+
const loaded = await loadConfig(root);
|
|
165
|
+
if (!loaded.ok || !loaded.config.packs.includes("standards")) return NONE;
|
|
166
|
+
// An unreadable manifest is already reported by sync --check.
|
|
167
|
+
const read = readWorkspace(root);
|
|
168
|
+
return read.ok ? checkKnip(read.workspace, loaded.config.knip.ignore, deps.run ?? runKnip) : NONE;
|
|
169
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** The `knip` section of workerstack.config.ts: the sanctioned knip ignores. */
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/** One `knip.ignore` entry: the file, dependency or binary knip is not to report, and why it cannot see the use. */
|
|
5
|
+
export type KnipIgnore =
|
|
6
|
+
| { file: string; reason: string }
|
|
7
|
+
| { dependency: string; reason: string }
|
|
8
|
+
| { binary: string; reason: string };
|
|
9
|
+
/** A `knip.ignore` entry as written: the schema refuses any that does not name exactly one of the three. */
|
|
10
|
+
export type KnipIgnoreEntry = {
|
|
11
|
+
reason: string;
|
|
12
|
+
file?: string | undefined;
|
|
13
|
+
dependency?: string | undefined;
|
|
14
|
+
binary?: string | undefined;
|
|
15
|
+
};
|
|
16
|
+
/** What the generated knip config and `repo check` read from `knip`. */
|
|
17
|
+
export type ResolvedKnip = { readonly ignore: readonly KnipIgnore[] };
|
|
18
|
+
|
|
19
|
+
const REASON_ERROR = "say why knip cannot see the use";
|
|
20
|
+
const ONE_KEY_ERROR = "name exactly one of file, dependency or binary";
|
|
21
|
+
const IGNORE_LIST = "must be a list of { file | dependency | binary, reason } entries";
|
|
22
|
+
|
|
23
|
+
const name = (error: string): z.ZodOptional<z.ZodString> => z.string({ error }).min(1, { error }).optional();
|
|
24
|
+
|
|
25
|
+
type EntrySchema = z.ZodPipe<
|
|
26
|
+
z.ZodObject<
|
|
27
|
+
{
|
|
28
|
+
file: z.ZodOptional<z.ZodString>;
|
|
29
|
+
dependency: z.ZodOptional<z.ZodString>;
|
|
30
|
+
binary: z.ZodOptional<z.ZodString>;
|
|
31
|
+
reason: z.ZodString;
|
|
32
|
+
},
|
|
33
|
+
z.core.$strict
|
|
34
|
+
>,
|
|
35
|
+
z.ZodTransform<KnipIgnore, KnipIgnoreEntry>
|
|
36
|
+
>;
|
|
37
|
+
type KnipSchema = z.ZodObject<{ ignore: z.ZodOptional<z.ZodReadonly<z.ZodArray<EntrySchema>>> }, z.core.$strict>;
|
|
38
|
+
|
|
39
|
+
/** One ignore: exactly one of `file`, `dependency` or `binary`, and why knip cannot see the use. */
|
|
40
|
+
function entrySchema(): EntrySchema {
|
|
41
|
+
return z
|
|
42
|
+
.strictObject(
|
|
43
|
+
{
|
|
44
|
+
file: name("file must be a path"),
|
|
45
|
+
dependency: name("dependency must be a package name"),
|
|
46
|
+
binary: name("binary must be a command name"),
|
|
47
|
+
reason: z.string({ error: REASON_ERROR }).min(1, { error: REASON_ERROR }),
|
|
48
|
+
},
|
|
49
|
+
{ error: 'must be an object like { file: "src/a.ts", reason: "why" }' },
|
|
50
|
+
)
|
|
51
|
+
.transform((entry, ctx): KnipIgnore => {
|
|
52
|
+
const { file, dependency, binary, reason } = entry;
|
|
53
|
+
if (file !== undefined && dependency === undefined && binary === undefined) return { file, reason };
|
|
54
|
+
if (dependency !== undefined && file === undefined && binary === undefined) return { dependency, reason };
|
|
55
|
+
if (binary !== undefined && file === undefined && dependency === undefined) return { binary, reason };
|
|
56
|
+
ctx.issues.push({ code: "custom", message: ONE_KEY_ERROR, input: entry });
|
|
57
|
+
return z.NEVER;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `knip`: the ignores, built when the config schema is. */
|
|
62
|
+
export function knipSchema(): KnipSchema {
|
|
63
|
+
return z.strictObject(
|
|
64
|
+
{ ignore: z.array(entrySchema(), { error: IGNORE_LIST }).readonly().optional() },
|
|
65
|
+
{ error: "must be an object like { ignore: [{ file, reason }] }" },
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The names the known-failures list and its check share. */
|
|
2
|
+
|
|
3
|
+
/** Where the list lives unless the config says otherwise. */
|
|
4
|
+
export const DEFAULT_LIST = "known-failures.json";
|
|
5
|
+
/** Joins the file, the describe titles and the case title into one entry. */
|
|
6
|
+
export const SEPARATOR = " > ";
|
|
7
|
+
/** The modifier that marks a case expected to fail. */
|
|
8
|
+
export const FAILS = "fails";
|
|
9
|
+
/** The suite function whose title joins the names. */
|
|
10
|
+
export const DESCRIBE = "describe";
|
|
11
|
+
/** The test functions that carry `.fails`. */
|
|
12
|
+
export const TEST_ROOTS: readonly string[] = ["it", "test"];
|
|
13
|
+
/** Files worth parsing: ts tsx mts cts js jsx mjs cjs. */
|
|
14
|
+
export const SOURCE: RegExp = /\.(?:[jt]sx?|[cm][jt]s)$/;
|
|
15
|
+
/** The oxc node types the walk reads. */
|
|
16
|
+
export const NODE = {
|
|
17
|
+
call: "CallExpression",
|
|
18
|
+
identifier: "Identifier",
|
|
19
|
+
member: "MemberExpression",
|
|
20
|
+
literal: "Literal",
|
|
21
|
+
template: "TemplateLiteral",
|
|
22
|
+
} as const;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { knownFailures } from "./known-failures.ts";
|
|
6
|
+
|
|
7
|
+
const dirs: string[] = [];
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
for (const dir of dirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/** A temp project holding `files` (path → content); the test files are those ending in `.test.ts`. */
|
|
13
|
+
function project(files: Record<string, string>): { root: string; tests: string[] } {
|
|
14
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "known-failures-"));
|
|
15
|
+
dirs.push(root);
|
|
16
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
17
|
+
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
18
|
+
fs.writeFileSync(path.join(root, rel), content);
|
|
19
|
+
}
|
|
20
|
+
return { root, tests: Object.keys(files).filter((rel) => rel.endsWith(".test.ts")) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const list = (...entries: string[]) => JSON.stringify(entries);
|
|
24
|
+
const run = (files: Record<string, string>) => {
|
|
25
|
+
const { root, tests } = project(files);
|
|
26
|
+
return knownFailures(root, tests);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe("knownFailures, the code against the list", () => {
|
|
30
|
+
it("fails an it.fails that is not listed, at its line", () => {
|
|
31
|
+
const problems = run({ "a.test.ts": 'import { it } from "vitest";\n\nit.fails("parses", () => {});\n' });
|
|
32
|
+
expect(problems).toEqual(['a.test.ts:3: it.fails("parses") is not in known-failures.json; list it or fix it']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("fails test.fails and it.concurrent.fails the same way", () => {
|
|
36
|
+
const problems = run({ "a.test.ts": 'test.fails("one", () => {});\nit.concurrent.fails("two", () => {});\n' });
|
|
37
|
+
expect(problems).toEqual([
|
|
38
|
+
'a.test.ts:1: test.fails("one") is not in known-failures.json; list it or fix it',
|
|
39
|
+
'a.test.ts:2: it.concurrent.fails("two") is not in known-failures.json; list it or fix it',
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("knownFailures, matching the list", () => {
|
|
45
|
+
it("passes cases the list names, nested describe titles joined", () => {
|
|
46
|
+
const problems = run({
|
|
47
|
+
"known-failures.json": list("a.test.ts > dates > utc > parses", "a.test.ts > top"),
|
|
48
|
+
"a.test.ts":
|
|
49
|
+
'describe("dates", () => { describe("utc", () => { it.fails("parses", () => {}); }); });\ntest.fails("top", () => {});\n',
|
|
50
|
+
});
|
|
51
|
+
expect(problems).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("fails a case whose describe path differs from the listed entry", () => {
|
|
55
|
+
const problems = run({
|
|
56
|
+
"known-failures.json": list("a.test.ts > parses"),
|
|
57
|
+
"a.test.ts": 'describe("dates", () => { it.fails("parses", () => {}); });\n',
|
|
58
|
+
});
|
|
59
|
+
expect(problems).toEqual([
|
|
60
|
+
'a.test.ts:1: it.fails("parses") is not in known-failures.json; list it or fix it',
|
|
61
|
+
'known-failures.json lists "a.test.ts > parses", which has no it.fails; remove it from the list',
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("fails a listed entry with no such case", () => {
|
|
66
|
+
const problems = run({ "known-failures.json": list("a.test.ts > gone"), "a.test.ts": "export {};\n" });
|
|
67
|
+
expect(problems).toEqual([
|
|
68
|
+
'known-failures.json lists "a.test.ts > gone", which has no it.fails; remove it from the list',
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("knownFailures, titles and files", () => {
|
|
74
|
+
it("fails a title that is not a literal on its own line, and still lists the others", () => {
|
|
75
|
+
const problems = run({
|
|
76
|
+
"known-failures.json": list("a.test.ts > fine"),
|
|
77
|
+
"a.test.ts": 'const name = "x";\nit.fails("fine", () => {});\nit.fails(name, () => {});\n',
|
|
78
|
+
});
|
|
79
|
+
expect(problems).toHaveLength(1);
|
|
80
|
+
expect(problems[0]).toMatch(/^a\.test\.ts:3: it\.fails\(\) title is not a string literal/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("accepts a template title without placeholders", () => {
|
|
84
|
+
const problems = run({
|
|
85
|
+
"known-failures.json": list("a.test.ts > plain"),
|
|
86
|
+
"a.test.ts": "it.fails(`plain`, () => {});\n",
|
|
87
|
+
});
|
|
88
|
+
expect(problems).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("knownFailures, what it leaves alone", () => {
|
|
93
|
+
it("ignores plain cases and .fails on other objects", () => {
|
|
94
|
+
const problems = run({ "a.test.ts": 'it("ok", () => {});\nconst r = { fails: () => 1 };\nr.fails();\n' });
|
|
95
|
+
expect(problems).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("passes with no list file and no .fails", () => {
|
|
99
|
+
expect(run({ "a.test.ts": 'it("ok", () => {});\n' })).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("reads the list from the path it is given", () => {
|
|
103
|
+
const { root, tests } = project({
|
|
104
|
+
"lists/kf.json": list("a.test.ts > x"),
|
|
105
|
+
"a.test.ts": 'it.fails("x", () => {});\n',
|
|
106
|
+
});
|
|
107
|
+
expect(knownFailures(root, tests, "lists/kf.json")).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("knownFailures, odd shapes", () => {
|
|
112
|
+
it("names a template with a placeholder, a number title and a non-literal describe as non-literal", () => {
|
|
113
|
+
const problems = run({
|
|
114
|
+
"a.test.ts":
|
|
115
|
+
'it.fails(`a ${1}`, () => {});\nit.fails(5, () => {});\ndescribe(name, () => { it.fails("x", () => {}); });\nit.fails();\n',
|
|
116
|
+
});
|
|
117
|
+
expect(problems.map((line) => line.slice(0, "a.test.ts:N".length))).toEqual([
|
|
118
|
+
"a.test.ts:1",
|
|
119
|
+
"a.test.ts:2",
|
|
120
|
+
"a.test.ts:3",
|
|
121
|
+
"a.test.ts:4",
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("ignores callees that are no it or test chain", () => {
|
|
126
|
+
const problems = run({
|
|
127
|
+
"a.test.ts": 'it["fails"]("x", () => {});\nmake()("y");\nmake().fails("z");\nthis.fails("w");\nit.fails;\n',
|
|
128
|
+
});
|
|
129
|
+
expect(problems).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("reads an invalid list, a list that is no array and a missing test file as empty", () => {
|
|
133
|
+
const bad = project({ "known-failures.json": "{", "a.test.ts": "export {};\n" });
|
|
134
|
+
expect(knownFailures(bad.root, bad.tests)).toEqual([]);
|
|
135
|
+
const object = project({ "known-failures.json": "{}", "a.test.ts": "export {};\n" });
|
|
136
|
+
expect(knownFailures(object.root, [...object.tests, "gone.test.ts", "notes.md"])).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The known-failures list: every `it.fails` / `test.fails` case named `<file> > <describe titles> > <title>` in a JSON
|
|
3
|
+
* array, so a case that starts passing is the only way a listed one leaves, and a new one cannot appear unlisted.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { parseSync } from "oxc-parser";
|
|
8
|
+
import { DEFAULT_LIST, DESCRIBE, FAILS, NODE, SEPARATOR, SOURCE, TEST_ROOTS } from "./known-failures.constants.ts";
|
|
9
|
+
|
|
10
|
+
/** A `.fails` case found in a file: where, how it was called, and its name, or null for a title that is no literal. */
|
|
11
|
+
type Case = { line: number; call: string; title: string | null; name: string | null };
|
|
12
|
+
|
|
13
|
+
type Node = Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
const isNode = (value: unknown): value is Node => typeof value === "object" && value !== null;
|
|
16
|
+
const isString = (value: unknown): value is string => typeof value === "string";
|
|
17
|
+
const lineOf = (source: string, at: number) => source.slice(0, at).split("\n").length;
|
|
18
|
+
/** `value[key]`, or undefined when `value` is no node. */
|
|
19
|
+
const get = (value: unknown, key: string): unknown => (isNode(value) ? value[key] : undefined);
|
|
20
|
+
|
|
21
|
+
/** The identifiers of a callee chain `a.b.c`, root first; null when the chain holds anything else. */
|
|
22
|
+
function chainOf(callee: unknown): string[] | null {
|
|
23
|
+
const type = get(callee, "type");
|
|
24
|
+
if (type === NODE.identifier) return [String(get(callee, "name"))];
|
|
25
|
+
if (type !== NODE.member || get(callee, "computed") === true) return null;
|
|
26
|
+
const head = chainOf(get(callee, "object"));
|
|
27
|
+
return head === null ? null : [...head, String(get(get(callee, "property"), "name"))];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The text of a string literal or a template without expressions, else null. */
|
|
31
|
+
function titleOf(arg: unknown): string | null {
|
|
32
|
+
if (get(arg, "type") === NODE.literal) {
|
|
33
|
+
const value = get(arg, "value");
|
|
34
|
+
return isString(value) ? value : null;
|
|
35
|
+
}
|
|
36
|
+
const bare = get(arg, "type") === NODE.template && get(get(arg, "expressions"), "length") === 0;
|
|
37
|
+
return bare ? String(get(get(get(get(arg, "quasis"), "0"), "value"), "cooked")) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type Scope = (string | null)[];
|
|
41
|
+
|
|
42
|
+
function caseOf(node: Node, scope: Scope, source: string): Case {
|
|
43
|
+
const title = titleOf(get(get(node, "arguments"), "0"));
|
|
44
|
+
const start = Number(get(node, "start"));
|
|
45
|
+
const end = Number(get(get(node, "callee"), "end"));
|
|
46
|
+
const named = title === null || scope.includes(null) ? null : [...scope, title].join(SEPARATOR);
|
|
47
|
+
return { line: lineOf(source, start), call: source.slice(start, end), title, name: named };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The scope inside `node`: a `describe` call adds its title (null when it is no literal). */
|
|
51
|
+
function scopeOf(node: Node, chain: string[] | null, scope: Scope): Scope {
|
|
52
|
+
if (chain?.[0] !== DESCRIBE) return scope;
|
|
53
|
+
return [...scope, titleOf(get(get(node, "arguments"), "0"))];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isFailsCall = (chain: string[] | null): chain is string[] =>
|
|
57
|
+
chain !== null && TEST_ROOTS.includes(String(chain[0])) && chain.includes(FAILS);
|
|
58
|
+
|
|
59
|
+
/** What a walk carries: the source it reads and the cases it found. */
|
|
60
|
+
type Walk = { source: string; out: Case[] };
|
|
61
|
+
|
|
62
|
+
/** The cases under `node`, `scope` being the describe titles around it (null for one that is no literal). */
|
|
63
|
+
function collect(node: unknown, scope: Scope, walk: Walk): void {
|
|
64
|
+
if (Array.isArray(node)) {
|
|
65
|
+
for (const child of node) collect(child, scope, walk);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!isNode(node)) return;
|
|
69
|
+
const chain = node["type"] === NODE.call ? chainOf(node["callee"]) : null;
|
|
70
|
+
if (isFailsCall(chain)) walk.out.push(caseOf(node, scope, walk.source));
|
|
71
|
+
const inner = scopeOf(node, chain, scope);
|
|
72
|
+
for (const child of Object.values(node)) collect(child, inner, walk);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function casesOf(rel: string, source: string): Case[] {
|
|
76
|
+
if (!source.includes(`.${FAILS}`)) return [];
|
|
77
|
+
const found: Case[] = [];
|
|
78
|
+
collect(parseSync(rel, source).program, [], { source, out: found });
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readText(file: string): string | null {
|
|
83
|
+
try {
|
|
84
|
+
return fs.readFileSync(file, "utf8");
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The list's entries, or null when the file is absent; a file that is no array of strings is an empty list. */
|
|
91
|
+
function readList(file: string): string[] | null {
|
|
92
|
+
const text = readText(file);
|
|
93
|
+
if (text === null) return null;
|
|
94
|
+
try {
|
|
95
|
+
const parsed: unknown = JSON.parse(text);
|
|
96
|
+
return Array.isArray(parsed) ? parsed.filter(isString) : [];
|
|
97
|
+
} catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The problems between the `.fails` cases of the test `files` (relative to `root`) and the list at `list` (default
|
|
104
|
+
* `known-failures.json`): a case not listed, a non-literal title, a listed entry with no case. No list file counts as
|
|
105
|
+
* an empty list.
|
|
106
|
+
*/
|
|
107
|
+
export function knownFailures(root: string, files: readonly string[], list: string = DEFAULT_LIST): string[] {
|
|
108
|
+
const listed = readList(path.join(root, list)) ?? [];
|
|
109
|
+
const seen = new Set<string>();
|
|
110
|
+
const problems: string[] = [];
|
|
111
|
+
for (const rel of files.filter((f) => SOURCE.test(f))) {
|
|
112
|
+
const source = readText(path.join(root, rel));
|
|
113
|
+
if (source === null) continue;
|
|
114
|
+
for (const found of casesOf(rel, source)) {
|
|
115
|
+
const at = `${rel}:${found.line}`;
|
|
116
|
+
if (found.name === null) {
|
|
117
|
+
problems.push(
|
|
118
|
+
`${at}: ${found.call}() title is not a string literal; write the title as a literal so ${list} can name it`,
|
|
119
|
+
);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const name = `${rel}${SEPARATOR}${found.name}`;
|
|
123
|
+
seen.add(name);
|
|
124
|
+
if (!listed.includes(name))
|
|
125
|
+
problems.push(`${at}: ${found.call}("${found.title}") is not in ${list}; list it or fix it`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const entry of listed)
|
|
129
|
+
if (!seen.has(entry)) problems.push(`${list} lists "${entry}", which has no it.fails; remove it from the list`);
|
|
130
|
+
return problems;
|
|
131
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Comment, ESTree, CreateRule } from "@oxlint/plugins";
|
|
2
|
+
import { isReason } from "./is-reason.ts";
|
|
3
|
+
|
|
4
|
+
const AS_KEYWORD = /\bas\b/;
|
|
5
|
+
|
|
6
|
+
/** `as const` and `as unknown` claim nothing a reader must trust. */
|
|
7
|
+
function claimsNothing(type: ESTree.TSType): boolean {
|
|
8
|
+
if (type.type === "TSUnknownKeyword") return true;
|
|
9
|
+
return type.type === "TSTypeReference" && type.typeName.type === "Identifier" && type.typeName.name === "const";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Whether a block comment of a reason lies after the expression and before the `as` keyword. */
|
|
13
|
+
function hasReason(text: string, comments: readonly Comment[], node: ESTree.TSAsExpression): boolean {
|
|
14
|
+
let edge = node.expression.end;
|
|
15
|
+
for (const comment of comments) {
|
|
16
|
+
if (comment.start < edge || comment.end > node.typeAnnotation.start) continue;
|
|
17
|
+
if (AS_KEYWORD.test(text.slice(edge, comment.start))) return false;
|
|
18
|
+
if (comment.type === "Block" && isReason(comment.value)) return true;
|
|
19
|
+
edge = comment.end;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fails an `as` cast with no reason right before its keyword: `x as Foo` tells the checker to trust it, so the reader
|
|
26
|
+
* is told why. `as const` and `as unknown` pass; `<Foo>x` is native `typescript/consistent-type-assertions`'.
|
|
27
|
+
*/
|
|
28
|
+
export const asNeedsReason: CreateRule = {
|
|
29
|
+
create(context) {
|
|
30
|
+
return {
|
|
31
|
+
TSAsExpression(node) {
|
|
32
|
+
if (claimsNothing(node.typeAnnotation)) return;
|
|
33
|
+
const { text } = context.sourceCode;
|
|
34
|
+
if (hasReason(text, context.sourceCode.getAllComments(), node)) return;
|
|
35
|
+
const type = text.slice(node.typeAnnotation.start, node.typeAnnotation.end);
|
|
36
|
+
const message = `"as ${type}" needs a reason: write x /* why the type holds */ as ${type}, or narrow with a check or a schema parse`;
|
|
37
|
+
context.report({ message, node });
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { builtinModules } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { ESTree, CreateRule } from "@oxlint/plugins";
|
|
4
|
+
import { type Manifest, nearestManifest, readManifest } from "./manifest.ts";
|
|
5
|
+
|
|
6
|
+
/** The file being linted, its package's manifest, and the dir paths in messages are relative to. */
|
|
7
|
+
type Where = { cwd: string; filename: string; own: Manifest };
|
|
8
|
+
type Sourced =
|
|
9
|
+
| ESTree.ImportDeclaration
|
|
10
|
+
| ESTree.ExportNamedDeclaration
|
|
11
|
+
| ESTree.ExportAllDeclaration
|
|
12
|
+
| ESTree.ImportExpression;
|
|
13
|
+
|
|
14
|
+
/** `node:fs`, `bun:test`, `virtual:x`: a scheme names no installed package. */
|
|
15
|
+
const SCHEME = /^[a-z][a-z\d+.-]*:/i;
|
|
16
|
+
|
|
17
|
+
/** `@scope/pkg/sub` → `@scope/pkg`; `pkg/sub` → `pkg`. */
|
|
18
|
+
function packageName(specifier: string): string {
|
|
19
|
+
const parts = specifier.split("/");
|
|
20
|
+
return parts.slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const exempt = (specifier: string): boolean =>
|
|
24
|
+
specifier.startsWith("#") ||
|
|
25
|
+
SCHEME.test(specifier) ||
|
|
26
|
+
specifier === "bun" ||
|
|
27
|
+
builtinModules.includes(packageName(specifier));
|
|
28
|
+
|
|
29
|
+
const shown = (cwd: string, to: string): string => path.relative(cwd, to).replaceAll("\\", "/");
|
|
30
|
+
|
|
31
|
+
/** The message for a path specifier whose target belongs to another package, or null when it stays home. */
|
|
32
|
+
function reachProblem(where: Where, specifier: string): string | null {
|
|
33
|
+
const file = nearestManifest(path.resolve(path.dirname(where.filename), specifier));
|
|
34
|
+
if (file === null || file === where.own.file) return null;
|
|
35
|
+
const dir = shown(where.cwd, path.dirname(file)) || "the root package";
|
|
36
|
+
const name = readManifest(file)?.name ?? null;
|
|
37
|
+
return name === null
|
|
38
|
+
? `"${specifier}" reaches into ${dir}; name it in ${shown(where.cwd, file)} and import it by that name`
|
|
39
|
+
: `"${specifier}" reaches into ${dir}; import it as "${name}"`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function problemOf(where: Where, specifier: string): string | null {
|
|
43
|
+
if (specifier.startsWith(".") || path.isAbsolute(specifier)) return reachProblem(where, specifier);
|
|
44
|
+
const name = packageName(specifier);
|
|
45
|
+
if (exempt(specifier) || name === where.own.name || where.own.declared.includes(name)) return null;
|
|
46
|
+
return `"${name}" is not declared in ${shown(where.cwd, where.own.file)}; add it to its dependencies`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Keeps each file to its own package: a path import stays inside its nearest package.json, and a package import is
|
|
51
|
+
* the package itself or one its manifest declares. Type-only imports and `import()` count; a file whose manifest
|
|
52
|
+
* cannot be read is skipped.
|
|
53
|
+
*/
|
|
54
|
+
export const boundaries: CreateRule = {
|
|
55
|
+
create(context) {
|
|
56
|
+
const ownFile = nearestManifest(path.dirname(context.filename));
|
|
57
|
+
const own = ownFile === null ? null : readManifest(ownFile);
|
|
58
|
+
if (own === null) return {};
|
|
59
|
+
const where: Where = { cwd: context.cwd, filename: context.filename, own };
|
|
60
|
+
const check = (node: Sourced): void => {
|
|
61
|
+
const { source } = node;
|
|
62
|
+
if (source?.type !== "Literal" || typeof source.value !== "string") return;
|
|
63
|
+
const message = problemOf(where, source.value);
|
|
64
|
+
if (message !== null) context.report({ message, node });
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
ImportDeclaration: check,
|
|
68
|
+
ExportNamedDeclaration: check,
|
|
69
|
+
ExportAllDeclaration: check,
|
|
70
|
+
ImportExpression: check,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|