@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,85 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { checkEnvModules } from "./check-env-modules.ts";
|
|
7
|
+
|
|
8
|
+
const dirs: string[] = [];
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const dir of dirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** A temp git repo holding `files` (path → content); `track` adds them all to the index. */
|
|
14
|
+
function repoWith(files: Record<string, string>, track = true): string {
|
|
15
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "check-env-module-"));
|
|
16
|
+
dirs.push(dir);
|
|
17
|
+
execFileSync("git", ["init", "-q"], { cwd: dir });
|
|
18
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
19
|
+
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
|
|
20
|
+
fs.writeFileSync(path.join(dir, rel), content);
|
|
21
|
+
}
|
|
22
|
+
if (track) execFileSync("git", ["add", "-A"], { cwd: dir });
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PKG = "{}\n";
|
|
27
|
+
const ENV = "export {};\n";
|
|
28
|
+
|
|
29
|
+
const SIBLINGS = {
|
|
30
|
+
"packages/a/package.json": PKG,
|
|
31
|
+
"packages/a/src/env.ts": ENV,
|
|
32
|
+
"packages/a/lib/env.ts": ENV,
|
|
33
|
+
};
|
|
34
|
+
const ROOT_PAIR = { "package.json": PKG, "env.ts": ENV, "src/env.ts": ENV };
|
|
35
|
+
const ROOT_LINE = "env: env.ts and src/env.ts are both env.ts in .; keep one";
|
|
36
|
+
|
|
37
|
+
describe("checkEnvModules", () => {
|
|
38
|
+
it("fails two env.ts under one package.json, naming both sorted", () => {
|
|
39
|
+
const dir = repoWith(SIBLINGS);
|
|
40
|
+
expect(checkEnvModules(dir)).toEqual([
|
|
41
|
+
"env: packages/a/lib/env.ts and packages/a/src/env.ts are both env.ts in packages/a; keep one",
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("lists the packages in sorted order", () => {
|
|
46
|
+
const dir = repoWith({
|
|
47
|
+
"packages/b/package.json": PKG,
|
|
48
|
+
"packages/b/env.ts": ENV,
|
|
49
|
+
"packages/b/src/env.ts": ENV,
|
|
50
|
+
...SIBLINGS,
|
|
51
|
+
});
|
|
52
|
+
expect(checkEnvModules(dir).map((line) => line.split(" in ")[1])).toEqual([
|
|
53
|
+
"packages/a; keep one",
|
|
54
|
+
"packages/b; keep one",
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("checkEnvModules, other layouts", () => {
|
|
60
|
+
it("passes one env.ts per package", () => {
|
|
61
|
+
const dir = repoWith({
|
|
62
|
+
"packages/a/package.json": PKG,
|
|
63
|
+
"packages/a/env.ts": ENV,
|
|
64
|
+
"packages/b/package.json": PKG,
|
|
65
|
+
"packages/b/env.ts": ENV,
|
|
66
|
+
});
|
|
67
|
+
expect(checkEnvModules(dir)).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("shows the root package as a dot", () => {
|
|
71
|
+
expect(checkEnvModules(repoWith(ROOT_PAIR))).toEqual([ROOT_LINE]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("skips a gitignored env.ts and counts an untracked one", () => {
|
|
75
|
+
const files = { ".gitignore": "ignored/\n", "package.json": PKG, "env.ts": ENV, "ignored/env.ts": ENV };
|
|
76
|
+
expect(checkEnvModules(repoWith(files))).toEqual([]);
|
|
77
|
+
expect(checkEnvModules(repoWith({ ...files, "src/env.ts": ENV }, false))).toEqual([ROOT_LINE]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("is empty outside a git repo", () => {
|
|
81
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "check-env-module-"));
|
|
82
|
+
dirs.push(dir);
|
|
83
|
+
expect(checkEnvModules(dir)).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ENV_FILE, MANIFEST } from "./env.constants.ts";
|
|
4
|
+
|
|
5
|
+
const ROOT = ".";
|
|
6
|
+
|
|
7
|
+
/** Tracked and untracked, non-ignored files under `cwd`, posix and relative to it; none outside a git repo. */
|
|
8
|
+
function listFiles(cwd: string): string[] {
|
|
9
|
+
try {
|
|
10
|
+
const out = execFileSync("git", ["ls-files", "-z", "-co", "--exclude-standard"], {
|
|
11
|
+
cwd,
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
14
|
+
});
|
|
15
|
+
return [...new Set(out.split("\0").filter((rel) => rel !== ""))];
|
|
16
|
+
} catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The folder of the nearest `package.json` at or above `rel`'s folder, `.` for the root or none. */
|
|
22
|
+
function packageOf(rel: string, manifests: ReadonlySet<string>): string {
|
|
23
|
+
let dir = path.posix.dirname(rel);
|
|
24
|
+
while (dir !== ROOT) {
|
|
25
|
+
if (manifests.has(dir)) return dir;
|
|
26
|
+
dir = path.posix.dirname(dir);
|
|
27
|
+
}
|
|
28
|
+
return ROOT;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `a, b and c`; a pair is `a and b`. */
|
|
32
|
+
function listOf(items: readonly string[]): string {
|
|
33
|
+
const rest = [...items];
|
|
34
|
+
const last = rest.pop();
|
|
35
|
+
return `${rest.join(", ")} and ${last}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One line per package with two or more `env.ts` files, sorted by package: the schema of a package's environment
|
|
40
|
+
* lives in one module. Tracked and untracked non-ignored files count.
|
|
41
|
+
*/
|
|
42
|
+
export function checkEnvModules(cwd: string): string[] {
|
|
43
|
+
const files = listFiles(cwd);
|
|
44
|
+
const manifests = new Set(
|
|
45
|
+
files.filter((rel) => path.posix.basename(rel) === MANIFEST).map((rel) => path.posix.dirname(rel)),
|
|
46
|
+
);
|
|
47
|
+
const byPackage = new Map<string, string[]>();
|
|
48
|
+
for (const rel of files.filter((file) => path.posix.basename(file) === ENV_FILE)) {
|
|
49
|
+
const pkg = packageOf(rel, manifests);
|
|
50
|
+
byPackage.set(pkg, [...(byPackage.get(pkg) ?? []), rel]);
|
|
51
|
+
}
|
|
52
|
+
return [...byPackage]
|
|
53
|
+
.filter(([, found]) => found.length > 1)
|
|
54
|
+
.toSorted(([a], [b]) => a.localeCompare(b, "en"))
|
|
55
|
+
.map(([pkg, found]) => `env: ${listOf(found.toSorted())} are both ${ENV_FILE} in ${pkg}; keep one`);
|
|
56
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { afterEach, expect, it } from "vitest";
|
|
2
|
+
import { magicDiagnostics, withoutSkipped } from "./check-magic.ts";
|
|
3
|
+
import {
|
|
4
|
+
BOUND,
|
|
5
|
+
cleanTemps,
|
|
6
|
+
COMPARED,
|
|
7
|
+
COPIES,
|
|
8
|
+
hit,
|
|
9
|
+
magicOf,
|
|
10
|
+
NAMES_NOTHING,
|
|
11
|
+
NUMBER,
|
|
12
|
+
plant,
|
|
13
|
+
SCHEMA,
|
|
14
|
+
SLOW,
|
|
15
|
+
STRING,
|
|
16
|
+
} from "./magic/fixtures/magic-repo.ts";
|
|
17
|
+
|
|
18
|
+
afterEach(cleanTemps);
|
|
19
|
+
|
|
20
|
+
const RETRY = [
|
|
21
|
+
'import { Schema } from "./schema.ts";',
|
|
22
|
+
"",
|
|
23
|
+
'const FAST = "fast";',
|
|
24
|
+
"const t = delay(go, 5000);",
|
|
25
|
+
"",
|
|
26
|
+
"declare function delay(f: () => void, ms: number): number;",
|
|
27
|
+
"declare function go(): void;",
|
|
28
|
+
"export function retry(mode: string, s: Schema): number {",
|
|
29
|
+
" void s.max(2048);",
|
|
30
|
+
" if (mode === FAST) return t;",
|
|
31
|
+
" if (t < 0) return 0;",
|
|
32
|
+
' if (mode == "fast") return 0;',
|
|
33
|
+
" return 1;",
|
|
34
|
+
"}",
|
|
35
|
+
"",
|
|
36
|
+
].join("\n");
|
|
37
|
+
|
|
38
|
+
it("reports each Usage hit at its line and column, in file and position order", () => {
|
|
39
|
+
expect(magicOf({ "src/retry.ts": RETRY, "src/schema.ts": SCHEMA })).toEqual([
|
|
40
|
+
hit("src/retry.ts:3:7", STRING, COPIES("FAST", "fast")),
|
|
41
|
+
hit("src/retry.ts:4:21", NUMBER, NAMES_NOTHING("5000")),
|
|
42
|
+
hit("src/retry.ts:9:14", NUMBER, BOUND(".max(2048)")),
|
|
43
|
+
hit("src/retry.ts:12:15", STRING, COMPARED("fast")),
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it(
|
|
48
|
+
"groups files by their nearest tsconfig.json, so a package's paths resolve and its literal union passes",
|
|
49
|
+
{ timeout: SLOW },
|
|
50
|
+
() => {
|
|
51
|
+
const api = {
|
|
52
|
+
compilerOptions: {
|
|
53
|
+
strict: true,
|
|
54
|
+
module: "preserve",
|
|
55
|
+
moduleResolution: "bundler",
|
|
56
|
+
paths: { "#modes": ["./src/modes.ts"] },
|
|
57
|
+
},
|
|
58
|
+
include: ["src"],
|
|
59
|
+
};
|
|
60
|
+
const root = plant({
|
|
61
|
+
"tsconfig.json": `${JSON.stringify({ compilerOptions: { strict: true }, include: ["src"] })}\n`,
|
|
62
|
+
"packages/api/tsconfig.json": `${JSON.stringify(api)}\n`,
|
|
63
|
+
"packages/api/src/modes.ts": 'export type Mode = "fast" | "slow";\nexport declare const current: Mode;\n',
|
|
64
|
+
"packages/api/src/use.ts": 'import { current } from "#modes";\nexport const fast = () => current === "fast";\n',
|
|
65
|
+
"src/root.ts": 'import { current } from "#modes";\nexport const fast = () => current === "fast";\n',
|
|
66
|
+
});
|
|
67
|
+
const found = magicDiagnostics(root, ["src/root.ts", "packages/api/src/use.ts", "packages/api/src/modes.ts"]);
|
|
68
|
+
expect(found).toEqual({ ok: true, value: [hit("src/root.ts:2:39", STRING, COMPARED("fast"))] });
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
it(
|
|
73
|
+
"finds a copy's comparisons in the package's other files when only the copy's file is checked",
|
|
74
|
+
{ timeout: SLOW },
|
|
75
|
+
() => {
|
|
76
|
+
const root = plant({
|
|
77
|
+
"tsconfig.json": `${JSON.stringify({ compilerOptions: { strict: true }, include: ["src"] })}\n`,
|
|
78
|
+
"src/modes.ts": 'export const SLOW = "slow";\nexport const PLAIN = 7;\n',
|
|
79
|
+
"src/use.ts": 'import { SLOW } from "./modes.ts";\nexport const is = (m: string) => m === SLOW;\n',
|
|
80
|
+
"src/other.ts": "export const other = (m: string) => m.length;\n",
|
|
81
|
+
"src/other.test.ts": 'import { SLOW } from "./modes.ts";\nexport const t = (m: string) => m === SLOW;\n',
|
|
82
|
+
});
|
|
83
|
+
expect(magicDiagnostics(root, ["src/modes.ts"])).toEqual({
|
|
84
|
+
ok: true,
|
|
85
|
+
value: [hit("src/modes.ts:1:14", STRING, COPIES("SLOW", "slow"))],
|
|
86
|
+
});
|
|
87
|
+
// The copy lands in a file this run does not check, so it is left to the run that checks it.
|
|
88
|
+
expect(magicDiagnostics(root, ["src/use.ts"])).toEqual({ ok: true, value: [] });
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
it("checks ts, tsx, mts and cts sources, never tests, declarations, fixtures, e2e or vendored code", () => {
|
|
93
|
+
const BODY = "export const f = () => 5000;\n";
|
|
94
|
+
const skipped = [
|
|
95
|
+
"src/a.test.ts",
|
|
96
|
+
"src/a.spec.tsx",
|
|
97
|
+
"src/a.e2e.ts",
|
|
98
|
+
"src/a.reject.ts",
|
|
99
|
+
"types/a.d.ts",
|
|
100
|
+
"types/b.d.mts",
|
|
101
|
+
"src/fixtures/a.ts",
|
|
102
|
+
"e2e/a.ts",
|
|
103
|
+
"lib/vendor/a.ts",
|
|
104
|
+
"src/a.js",
|
|
105
|
+
"README.md",
|
|
106
|
+
];
|
|
107
|
+
const checked = ["src/a.ts", "src/b.tsx", "src/c.mts", "src/d.cts"];
|
|
108
|
+
const files = Object.fromEntries([...skipped, ...checked].map((rel) => [rel, BODY]));
|
|
109
|
+
expect(magicOf(files).map((d) => d.file)).toEqual(checked);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("reports a tsconfig.json typescript cannot read", () => {
|
|
113
|
+
const root = plant({ "pkg/tsconfig.json": "{", "pkg/a.ts": "export const a = 1;\n" });
|
|
114
|
+
const found = magicDiagnostics(root, ["pkg/a.ts"]);
|
|
115
|
+
expect(found.ok ? [] : found.errors).toEqual([expect.stringMatching(/^pkg\/tsconfig\.json: .+/)]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("withoutSkipped drops only the hits of the rule a skip names, in the files its globs match", () => {
|
|
119
|
+
const diagnostics = [
|
|
120
|
+
hit("src/a.ts:1:1", NUMBER, NAMES_NOTHING("5")),
|
|
121
|
+
hit("src/a.ts:2:1", STRING, COMPARED("x")),
|
|
122
|
+
hit("lib/b.ts:1:1", NUMBER, NAMES_NOTHING("5")),
|
|
123
|
+
];
|
|
124
|
+
const skips = [{ files: ["src/*.ts"], rules: ["repo/magic-number"], reason: "tuning lives here" }];
|
|
125
|
+
expect(withoutSkipped(diagnostics, skips, ["src/a.ts", "lib/b.ts"])).toEqual(diagnostics.slice(1));
|
|
126
|
+
expect(withoutSkipped(diagnostics, [], ["src/a.ts", "lib/b.ts"])).toEqual(diagnostics);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const SHARDS = "export default { nightly: { shards: 8 } };\n";
|
|
130
|
+
|
|
131
|
+
it("reports no magic number in the root workerstack.config.ts, which holds values, but does in the same literal elsewhere", () => {
|
|
132
|
+
const found = magicOf({ "workerstack.config.ts": SHARDS, "src/shards.ts": SHARDS });
|
|
133
|
+
expect(found).toEqual([hit("src/shards.ts:1:37", NUMBER, NAMES_NOTHING("8"))]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("reports a magic number in a workerstack.config.ts below the root", () => {
|
|
137
|
+
const found = magicOf({ "pkg/workerstack.config.ts": SHARDS });
|
|
138
|
+
expect(found).toEqual([hit("pkg/workerstack.config.ts:1:37", NUMBER, NAMES_NOTHING("8"))]);
|
|
139
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magic numbers and strings, as lint diagnostics `repo(magic-number)` and `repo(magic-string)` that the baseline
|
|
3
|
+
* counts beside oxlint's. A script on the TypeScript compiler API, since only the type checker can tell a string
|
|
4
|
+
* compared to a plain `string` from one compared to a literal union, and no disable comment reaches it. Each file is
|
|
5
|
+
* checked in one program per nearest `tsconfig.json`, so a package's own paths and options resolve its imports.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import ts from "typescript";
|
|
10
|
+
import { constString } from "./magic/magic-nodes.ts";
|
|
11
|
+
import { magicNumbers } from "./magic/magic-numbers.ts";
|
|
12
|
+
import type { MagicHit } from "./magic/magic-hit.ts";
|
|
13
|
+
import { magicStrings } from "./magic/magic-strings.ts";
|
|
14
|
+
import { CONFIG_FILE, type LintSkip } from "./config.ts";
|
|
15
|
+
import type { Diagnostic, Result } from "./scripts/lint-baseline.ts";
|
|
16
|
+
import { codeOf, skipFiles } from "./scripts/lint-skips.ts";
|
|
17
|
+
import { RUNTIME_CONFIG } from "./tsconfig.constants.ts";
|
|
18
|
+
import { isChecked, ownerOf, posix, setupOf, type Setup } from "./ts-program.ts";
|
|
19
|
+
|
|
20
|
+
const byKey = (a: string, b: string) => Number(a > b) - Number(a < b);
|
|
21
|
+
|
|
22
|
+
/** The names of the `const` strings `file` declares, which a comparison in another file could make copies. */
|
|
23
|
+
function copyNames(file: ts.SourceFile): string[] {
|
|
24
|
+
const names: string[] = [];
|
|
25
|
+
const visit = (node: ts.Node): void => {
|
|
26
|
+
if (ts.isVariableDeclaration(node) && constString(node) !== null) names.push(node.name.getText());
|
|
27
|
+
ts.forEachChild(node, visit);
|
|
28
|
+
};
|
|
29
|
+
visit(file);
|
|
30
|
+
return names;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One group's files: its tsconfig dir (null for none), the root and the files to report in, as program paths. */
|
|
34
|
+
type Group = { root: string; dir: string | null; targets: readonly string[] };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The tsconfig's other checked files that mention a `const` string of the targets: a comparison there makes it a
|
|
38
|
+
* copy, reported in the target. Read as text first, so a run on a few files parses only the files that could.
|
|
39
|
+
*/
|
|
40
|
+
function extrasOf(group: Group, setup: Setup, program: ts.Program): string[] {
|
|
41
|
+
// The checker binds the files, which sets the parent links a declaration's const flag is read through.
|
|
42
|
+
program.getTypeChecker();
|
|
43
|
+
const names = program
|
|
44
|
+
.getSourceFiles()
|
|
45
|
+
.filter((file) => group.targets.includes(file.fileName))
|
|
46
|
+
.flatMap(copyNames);
|
|
47
|
+
if (names.length === 0) return [];
|
|
48
|
+
return setup.fileNames.filter((fileName) => {
|
|
49
|
+
const rel = path.posix.relative(group.root, fileName);
|
|
50
|
+
if (group.targets.includes(fileName) || !isChecked(rel) || ownerOf(group.root, rel) !== group.dir) return false;
|
|
51
|
+
const text = fs.readFileSync(fileName, "utf8");
|
|
52
|
+
return names.some((name) => text.includes(name));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Every hit in one group's targets, from one program over them and the files that may compare to their copies. */
|
|
57
|
+
function groupHits(group: Group): Result<MagicHit[]> {
|
|
58
|
+
const setup = setupOf(group.root, group.dir, RUNTIME_CONFIG);
|
|
59
|
+
if (!setup.ok) return setup;
|
|
60
|
+
const { options } = setup.value;
|
|
61
|
+
const first = ts.createProgram({ rootNames: group.targets, options });
|
|
62
|
+
const extras = extrasOf(group, setup.value, first);
|
|
63
|
+
const roots = [...group.targets, ...extras];
|
|
64
|
+
const program = extras.length === 0 ? first : ts.createProgram({ rootNames: roots, options, oldProgram: first });
|
|
65
|
+
const owns = (fileName: string) => isChecked(path.posix.relative(group.root, fileName));
|
|
66
|
+
const context = { checker: program.getTypeChecker(), owns };
|
|
67
|
+
// The root config file holds values by definition, as a constants file does.
|
|
68
|
+
const rootConfig = path.posix.join(group.root, CONFIG_FILE);
|
|
69
|
+
const walked = program.getSourceFiles().filter((file) => roots.includes(file.fileName));
|
|
70
|
+
const hits = walked.flatMap((file) => {
|
|
71
|
+
const numbers =
|
|
72
|
+
group.targets.includes(file.fileName) && file.fileName !== rootConfig ? magicNumbers(file, context.checker) : [];
|
|
73
|
+
return [...numbers, ...magicStrings(file, context)];
|
|
74
|
+
});
|
|
75
|
+
return { ok: true, value: hits.filter((hit) => group.targets.includes(hit.fileName)) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The hits as diagnostics, each once, in file then position order. */
|
|
79
|
+
function diagnosticsOf(root: string, hits: readonly MagicHit[]): Diagnostic[] {
|
|
80
|
+
const unique = new Map(hits.map((hit) => [`${hit.fileName}:${hit.at}:${hit.rule}`, hit]));
|
|
81
|
+
const sorted = [...unique.values()].sort((a, b) => byKey(a.fileName, b.fileName) || a.at - b.at);
|
|
82
|
+
return sorted.map((hit) => {
|
|
83
|
+
const file = path.posix.relative(root, hit.fileName);
|
|
84
|
+
const rule = codeOf(hit.rule);
|
|
85
|
+
return { file, rule, line: `${file}:${hit.line}:${hit.column}: error ${rule}: ${hit.message}` };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The magic hits in the checked files among `files` (root-relative POSIX paths), in oxlint's line form, sorted by file
|
|
91
|
+
* and position; or why a tsconfig.json could not be read.
|
|
92
|
+
*/
|
|
93
|
+
export function magicDiagnostics(root: string, files: readonly string[]): Result<Diagnostic[]> {
|
|
94
|
+
const real = posix(fs.realpathSync(root));
|
|
95
|
+
const groups = new Map<string | null, string[]>();
|
|
96
|
+
for (const rel of files.filter(isChecked)) {
|
|
97
|
+
const dir = ownerOf(real, rel);
|
|
98
|
+
groups.set(dir, [...(groups.get(dir) ?? []), path.posix.join(real, rel)]);
|
|
99
|
+
}
|
|
100
|
+
const hits: MagicHit[] = [];
|
|
101
|
+
for (const [dir, targets] of groups) {
|
|
102
|
+
const found = groupHits({ root: real, dir, targets });
|
|
103
|
+
if (!found.ok) return found;
|
|
104
|
+
hits.push(...found.value);
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, value: diagnosticsOf(real, hits) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `diagnostics` without those of a rule a skip turns off in a file of `files` its globs match. */
|
|
110
|
+
export function withoutSkipped(
|
|
111
|
+
diagnostics: readonly Diagnostic[],
|
|
112
|
+
skips: readonly LintSkip[],
|
|
113
|
+
files: readonly string[],
|
|
114
|
+
): Diagnostic[] {
|
|
115
|
+
const off = skips.flatMap((skip) => {
|
|
116
|
+
const matched = skipFiles(skip.files, files);
|
|
117
|
+
return skip.rules.flatMap((rule) => matched.map((file) => `${file} ${codeOf(rule)}`));
|
|
118
|
+
});
|
|
119
|
+
return diagnostics.filter((d) => !off.includes(`${d.file} ${d.rule}`));
|
|
120
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** A quoted span in a script; a separator inside one is an argument, not the end of a call. */
|
|
2
|
+
export const QUOTED_SPAN: RegExp = /'[^']*'|"[^"]*"/g;
|
|
3
|
+
/** The separators that end one tool call and start the next in a script. */
|
|
4
|
+
export const CALL_SEPARATOR: RegExp = /&&|;/;
|
|
@@ -0,0 +1,210 @@
|
|
|
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 { checkManifests } from "./check-manifests.ts";
|
|
6
|
+
import { readWorkspace } from "./sync/workspace.ts";
|
|
7
|
+
|
|
8
|
+
const temps: string[] = [];
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const PIN = `bun@${Bun.version}`;
|
|
14
|
+
|
|
15
|
+
/** Writes each manifest into a temp repo, reads it as `repo check` does and returns the problem lines. */
|
|
16
|
+
function problems(files: Record<string, object>): string[] {
|
|
17
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-manifests-"));
|
|
18
|
+
temps.push(root);
|
|
19
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
20
|
+
const file = path.join(root, rel);
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
fs.writeFileSync(file, JSON.stringify(content));
|
|
23
|
+
}
|
|
24
|
+
const result = readWorkspace(root);
|
|
25
|
+
if (!result.ok) throw new Error(result.errors.join("\n"));
|
|
26
|
+
return checkManifests(result.workspace);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("checkManifests", () => {
|
|
30
|
+
it("passes a private single-package root with sideEffects false and a pinned bun", () => {
|
|
31
|
+
expect(
|
|
32
|
+
problems({ "package.json": { name: "a", private: true, sideEffects: false, packageManager: "bun@1.4.4" } }),
|
|
33
|
+
).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("passes a published single-package root with a version", () => {
|
|
37
|
+
expect(
|
|
38
|
+
problems({
|
|
39
|
+
"package.json": { name: "a", version: "1.0.0", private: false, sideEffects: false, packageManager: PIN },
|
|
40
|
+
}),
|
|
41
|
+
).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("accepts a prerelease pin", () => {
|
|
45
|
+
expect(
|
|
46
|
+
problems({ "package.json": { private: true, sideEffects: false, packageManager: "bun@1.5.0-canary.3" } }),
|
|
47
|
+
).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("checkManifests missing fields", () => {
|
|
52
|
+
it("reports a missing private", () => {
|
|
53
|
+
expect(problems({ "package.json": { sideEffects: false, packageManager: PIN } })).toEqual([
|
|
54
|
+
'package.json: "private" is missing; set true, or false to publish it',
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("reports a missing sideEffects", () => {
|
|
59
|
+
const files = {
|
|
60
|
+
"package.json": { private: true, workspaces: ["packages/*"], packageManager: PIN },
|
|
61
|
+
"packages/web/package.json": { name: "web", private: true },
|
|
62
|
+
};
|
|
63
|
+
expect(problems(files)).toEqual(['packages/web/package.json: "sideEffects" is missing; set it to false']);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it.each([
|
|
67
|
+
[["*.css"], '["*.css"]'],
|
|
68
|
+
[true, "true"],
|
|
69
|
+
["false", '"false"'],
|
|
70
|
+
])("reports sideEffects %j and names the CSS fix", (value, shown) => {
|
|
71
|
+
const files = {
|
|
72
|
+
"package.json": { private: true, workspaces: ["packages/*"], packageManager: PIN },
|
|
73
|
+
"packages/ui/package.json": { private: true, sideEffects: value },
|
|
74
|
+
};
|
|
75
|
+
expect(problems(files)).toEqual([
|
|
76
|
+
`packages/ui/package.json: "sideEffects" is ${shown}; set it to false and import CSS from an app entry`,
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("checkManifests versions and packageManager", () => {
|
|
82
|
+
it("reports a version on a private package", () => {
|
|
83
|
+
const files = {
|
|
84
|
+
"package.json": { private: true, workspaces: ["packages/*"], packageManager: PIN },
|
|
85
|
+
"packages/api/package.json": { private: true, version: "1.0.0", sideEffects: false },
|
|
86
|
+
};
|
|
87
|
+
expect(problems(files)).toEqual(['packages/api/package.json: private packages have no "version"; delete it']);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("allows a version on a package that is not private", () => {
|
|
91
|
+
const files = {
|
|
92
|
+
"package.json": { private: true, workspaces: ["packages/*"], packageManager: PIN },
|
|
93
|
+
"packages/api/package.json": { private: false, version: "1.0.0", sideEffects: false },
|
|
94
|
+
};
|
|
95
|
+
expect(problems(files)).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("reports a version on a private root", () => {
|
|
99
|
+
expect(
|
|
100
|
+
problems({ "package.json": { private: true, version: "0.0.0", sideEffects: false, packageManager: PIN } }),
|
|
101
|
+
).toEqual(['package.json: private packages have no "version"; delete it']);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("checkManifests packageManager", () => {
|
|
106
|
+
it("reports a missing packageManager with the running bun version", () => {
|
|
107
|
+
expect(problems({ "package.json": { private: true, sideEffects: false } })).toEqual([
|
|
108
|
+
`package.json: "packageManager" is missing; set it to "${PIN}"`,
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it.each([["bun@^1.4"], ["bun@1.4"], ["bun@latest"], ["pnpm@9.0.0"], ["bun@1.4.4+sha"]])(
|
|
113
|
+
"reports packageManager %s as not an exact bun pin",
|
|
114
|
+
(value) => {
|
|
115
|
+
expect(problems({ "package.json": { private: true, sideEffects: false, packageManager: value } })).toEqual([
|
|
116
|
+
`package.json: "packageManager" is "${value}"; pin an exact version like "${PIN}"`,
|
|
117
|
+
]);
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
it("reports a packageManager that is not a string", () => {
|
|
122
|
+
expect(problems({ "package.json": { private: true, sideEffects: false, packageManager: 4 } })).toEqual([
|
|
123
|
+
`package.json: "packageManager" is 4; pin an exact version like "${PIN}"`,
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("checkManifests roots and packages", () => {
|
|
129
|
+
it("holds a monorepo root to private, version and packageManager but not sideEffects", () => {
|
|
130
|
+
expect(problems({ "package.json": { workspaces: ["packages/*"], version: "1.0.0", private: true } })).toEqual([
|
|
131
|
+
'package.json: private packages have no "version"; delete it',
|
|
132
|
+
`package.json: "packageManager" is missing; set it to "${PIN}"`,
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("checkManifests workspaces", () => {
|
|
138
|
+
it("passes a two-package workspace", () => {
|
|
139
|
+
const files = {
|
|
140
|
+
"package.json": { private: true, workspaces: { packages: ["packages/*"] }, packageManager: "bun@1.4.4" },
|
|
141
|
+
"packages/api/package.json": { private: true, sideEffects: false },
|
|
142
|
+
"packages/web/package.json": { private: true, sideEffects: false },
|
|
143
|
+
};
|
|
144
|
+
expect(problems(files)).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("lists the root first, then each package by directory", () => {
|
|
148
|
+
const files = {
|
|
149
|
+
"package.json": { workspaces: ["packages/*"] },
|
|
150
|
+
"packages/web/package.json": {},
|
|
151
|
+
"packages/api/package.json": {},
|
|
152
|
+
};
|
|
153
|
+
expect(problems(files)).toEqual([
|
|
154
|
+
'package.json: "private" is missing; set true, or false to publish it',
|
|
155
|
+
`package.json: "packageManager" is missing; set it to "${PIN}"`,
|
|
156
|
+
'packages/api/package.json: "private" is missing; set true, or false to publish it',
|
|
157
|
+
'packages/api/package.json: "sideEffects" is missing; set it to false',
|
|
158
|
+
'packages/web/package.json: "private" is missing; set true, or false to publish it',
|
|
159
|
+
'packages/web/package.json: "sideEffects" is missing; set it to false',
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("leaves a root without package.json alone", () => {
|
|
164
|
+
expect(problems({})).toEqual([]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("checkManifests chained root scripts", () => {
|
|
169
|
+
const root = { private: true, sideEffects: false, packageManager: "bun@1.4.4" };
|
|
170
|
+
const chain = (name: string, n: number) =>
|
|
171
|
+
`package.json: "${name}" chains ${n} tool calls with "&&" or ";"; make it one call, such as "repo ci"`;
|
|
172
|
+
const withScripts = (scripts: Record<string, string>) => problems({ "package.json": { ...root, scripts } });
|
|
173
|
+
|
|
174
|
+
it("names a script chained with &&", () => {
|
|
175
|
+
expect(withScripts({ test: "a && b" })).toEqual([chain("test", 2)]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("names a script chained with ;", () => {
|
|
179
|
+
expect(withScripts({ test: "a; b" })).toEqual([chain("test", 2)]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("counts three calls", () => {
|
|
183
|
+
expect(withScripts({ ci: "a && b ; c" })).toEqual([chain("ci", 3)]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("does not split inside quotes", () => {
|
|
187
|
+
expect(withScripts({ a: 'echo "a && b"', b: "echo 'a; b'", c: 'echo "it\'s && x" && y' })).toEqual([chain("c", 2)]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("leaves || and | alone, and ignores empty segments", () => {
|
|
191
|
+
expect(withScripts({ a: "a || b", b: "a | b", c: "a &&", d: "; a ;", e: "a" })).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("checkManifests script shapes", () => {
|
|
196
|
+
const root = { private: true, sideEffects: false, packageManager: "bun@1.4.4" };
|
|
197
|
+
|
|
198
|
+
it("skips scripts that are not strings", () => {
|
|
199
|
+
expect(problems({ "package.json": { ...root, scripts: { a: 1 } } })).toEqual([]);
|
|
200
|
+
expect(problems({ "package.json": { ...root, scripts: "a && b" } })).toEqual([]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("judges only the root's scripts", () => {
|
|
204
|
+
const files = {
|
|
205
|
+
"package.json": { private: true, workspaces: ["packages/*"], packageManager: "bun@1.4.4" },
|
|
206
|
+
"packages/a/package.json": { private: true, sideEffects: false, scripts: { test: "a && b" } },
|
|
207
|
+
};
|
|
208
|
+
expect(problems(files)).toEqual([]);
|
|
209
|
+
});
|
|
210
|
+
});
|