@jterrazz/typescript 9.2.1 → 10.0.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/README.md +20 -16
- package/bin/commands/check.sh +348 -117
- package/bin/typescript.sh +55 -0
- package/lib/check-architecture.js +89 -0
- package/lib/check-baseline.js +144 -0
- package/lib/check-docs.js +4 -3
- package/lib/check-drift.js +209 -0
- package/lib/check-gitignore.js +4 -4
- package/lib/check-markdown.js +279 -0
- package/lib/check-names.js +125 -0
- package/lib/check-publish.js +150 -0
- package/lib/check-secrets.js +115 -0
- package/lib/check-suppressions.js +355 -0
- package/lib/doctor.js +185 -0
- package/lib/merge-knip-config.js +57 -25
- package/lib/tracked-files.js +165 -0
- package/lib/workspace-members.js +5 -6
- package/package.json +19 -8
- package/presets/oxfmt/index.js +49 -5
- package/presets/oxlint/profiles/astro.js +10 -0
- package/presets/oxlint/profiles/bun.js +7 -0
- package/presets/oxlint/profiles/expo.js +7 -0
- package/presets/oxlint/profiles/library.js +16 -0
- package/presets/oxlint/profiles/next.js +7 -0
- package/presets/oxlint/profiles/node.js +7 -0
- package/presets/prettier/astro.json +6 -0
- package/presets/tsconfig/expo.json +16 -6
- package/presets/tsconfig/library.json +18 -0
- package/presets/tsconfig/next.json +12 -2
- package/presets/tsconfig/node.json +18 -4
- package/rules/README.md +23 -0
- package/rules/_contract.js +191 -0
- package/rules/_contract.test.ts +81 -0
- package/rules/a11y.js +51 -0
- package/rules/architecture/hexagonal.js +56 -0
- package/rules/architecture/layers.js +75 -0
- package/rules/astro.js +49 -0
- package/rules/catalog.js +134 -0
- package/rules/catalog.test.ts +84 -0
- package/rules/compile.js +125 -0
- package/rules/core/eslint.js +234 -0
- package/rules/core/import.js +107 -0
- package/rules/core/jsdoc.js +52 -0
- package/rules/core/node.js +36 -0
- package/rules/core/oxc.js +54 -0
- package/rules/core/promise.js +39 -0
- package/rules/core/typescript.js +204 -0
- package/rules/core/unicorn.js +200 -0
- package/rules/next.js +53 -0
- package/rules/profiles.js +89 -0
- package/rules/react-native.js +48 -0
- package/rules/react.js +148 -0
- package/rules/sorted.js +41 -0
- package/rules/vitest.js +153 -0
- package/src/docs.d.ts +4 -4
- package/src/docs.js +75 -47
- package/src/docs.test.ts +136 -29
- package/src/index.d.ts +13 -9
- package/src/index.js +15 -8
- package/src/oxfmt.d.ts +15 -2
- package/src/oxfmt.test.ts +10 -0
- package/src/oxlint.d.ts +57 -10
- package/src/oxlint.js +35 -50
- package/src/oxlint.test.ts +82 -28
- package/presets/oxlint/architectures/hexagonal-rules.js +0 -39
- package/presets/oxlint/architectures/hexagonal.js +0 -13
- package/presets/oxlint/base.js +0 -145
- package/presets/oxlint/expo.js +0 -36
- package/presets/oxlint/next.js +0 -43
- package/presets/oxlint/node.js +0 -14
- package/presets/oxlint/plugins/codestyle.js +0 -231
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @jterrazz/typescript
|
|
2
2
|
|
|
3
|
-
The complete TypeScript toolchain — build, run, check, and document with zero configuration. Powered by tsdown, Oxlint, Oxfmt, TypeScript 7, and Knip.
|
|
3
|
+
The complete TypeScript toolchain — build, run, check, and document with zero configuration. Powered by tsdown, Oxlint, Oxfmt, TypeScript 7, and Knip. Six profiles, one rulebook: every rule of every plugin it loads is decided by name.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -17,8 +17,10 @@ npx typescript start # Run the built application
|
|
|
17
17
|
npx typescript dev # Build, run, and rebuild on changes
|
|
18
18
|
npx typescript docs # Compile the committed docs/reference tree from source
|
|
19
19
|
npx typescript docs-layout . # Check a repository's docs/ against the manual spine
|
|
20
|
-
npx typescript check #
|
|
21
|
-
npx typescript fix # Auto-fix lint and
|
|
20
|
+
npx typescript check # Every quality pass, in parallel, ending on a drift report
|
|
21
|
+
npx typescript fix # Auto-fix lint, formatting, the .gitignore and suppression spellings
|
|
22
|
+
npx typescript doctor # Installed tool versions against the ranges this release declares
|
|
23
|
+
npx typescript baseline # Record today's oxlint diagnostics, so the count may only fall
|
|
22
24
|
npx typescript clean # Remove .artifacts/ (dist/ stays — it is the product)
|
|
23
25
|
```
|
|
24
26
|
|
|
@@ -26,16 +28,18 @@ npx typescript clean # Remove .artifacts/ (dist/ stays — it is the p
|
|
|
26
28
|
|
|
27
29
|
Fully compiled — no JavaScript in the hot path:
|
|
28
30
|
|
|
29
|
-
| Step | Tool
|
|
30
|
-
| ------------ |
|
|
31
|
-
| Transpile | [Oxc](https://oxc.rs) (via tsdown)
|
|
32
|
-
| Bundle | [Rolldown](https://rolldown.rs)
|
|
33
|
-
| Declarations | [tsdown](https://tsdown.dev) built-in
|
|
34
|
-
| Type check | [tsc (TypeScript 7)](https://github.com/microsoft/typescript-go)
|
|
35
|
-
| Lint | [Oxlint](https://oxc.rs/docs/guide/usage/linter)
|
|
36
|
-
| Format | [Oxfmt](https://oxc.rs/docs/guide/usage/formatter)
|
|
37
|
-
| Unused code | [Knip](https://knip.dev)
|
|
38
|
-
|
|
|
31
|
+
| Step | Tool | Language |
|
|
32
|
+
| ------------ | --------------------------------------------------------------------------- | -------- |
|
|
33
|
+
| Transpile | [Oxc](https://oxc.rs) (via tsdown) | Rust |
|
|
34
|
+
| Bundle | [Rolldown](https://rolldown.rs) | Rust |
|
|
35
|
+
| Declarations | [tsdown](https://tsdown.dev) built-in | Rust |
|
|
36
|
+
| Type check | [tsc (TypeScript 7)](https://github.com/microsoft/typescript-go) | Go |
|
|
37
|
+
| Lint | [Oxlint](https://oxc.rs/docs/guide/usage/linter) | Rust |
|
|
38
|
+
| Format | [Oxfmt](https://oxc.rs/docs/guide/usage/formatter) | Rust |
|
|
39
|
+
| Unused code | [Knip](https://knip.dev) | Node |
|
|
40
|
+
| Packaging | [publint](https://publint.dev) + [attw](https://arethetypeswrong.github.io) | Node |
|
|
41
|
+
| Layer map | [dependency-cruiser](https://github.com/sverweij/dependency-cruiser) | Node |
|
|
42
|
+
| API docs | [Typedoc](https://typedoc.org) | Node |
|
|
39
43
|
|
|
40
44
|
## Documentation
|
|
41
45
|
|
|
@@ -46,12 +50,12 @@ The full corpus lives in [`docs/`](docs/):
|
|
|
46
50
|
- [Testing](docs/03-testing.md) — how this toolchain proves itself.
|
|
47
51
|
- [Operating](docs/04-operating.md) — what publishes it, and which number moves.
|
|
48
52
|
- [Building](docs/05-building.md) — `build`, `bundle`, `start`, `dev`.
|
|
49
|
-
- [Quality checks](docs/06-quality-checks.md) — `check` / `fix` and their passes.
|
|
50
|
-
- [Lint presets](docs/07-lint-presets.md) —
|
|
53
|
+
- [Quality checks](docs/06-quality-checks.md) — `check` / `fix` and their fifteen passes.
|
|
54
|
+
- [Lint presets](docs/07-lint-presets.md) — the rulebook, the six profiles, `compose`, architecture, knip.
|
|
51
55
|
- [Docs pipeline](docs/08-docs-pipeline.md) — the `typescript docs` compiler.
|
|
52
56
|
- [Repo structure](docs/09-repo-structure.md) — pointer to the shared doctrine; what's TypeScript-specific here.
|
|
53
57
|
|
|
54
|
-
For agents: read the chapters and the generated [`docs/reference/`](docs/reference/) tree straight from the repo, plus the [`skills/jterrazz-typescript`](skills/jterrazz-typescript/SKILL.md) Claude Code skill (the toolchain). The repo-structure doctrine itself is a Claude Code skill too — `jterrazz-repo-structure`, shipped from [`jterrazz-studio`](https://github.com/jterrazz/jterrazz-studio).
|
|
58
|
+
For agents: read the chapters and the generated [`docs/reference/`](docs/reference/) tree straight from the repo, plus the [`skills/jterrazz-typescript`](skills/jterrazz-typescript/SKILL.md) Claude Code skill (the toolchain) and its generated [rule reference](skills/jterrazz-typescript/references/rules.md). The repo-structure doctrine itself is a Claude Code skill too — `jterrazz-repo-structure`, shipped from [`jterrazz-studio`](https://github.com/jterrazz/jterrazz-studio).
|
|
55
59
|
|
|
56
60
|
## License
|
|
57
61
|
|
package/bin/commands/check.sh
CHANGED
|
@@ -62,10 +62,62 @@ find_tsc() {
|
|
|
62
62
|
fi
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
# oxlint's type-aware rules run in `tsgolint`, a separate binary it looks up on
|
|
66
|
+
# PATH — and a consumer's PATH has no reason to carry this package's bin dir. It
|
|
67
|
+
# is a dependency here, so the lookup is made to succeed by putting the
|
|
68
|
+
# directory that holds it in front, for this process and its children only.
|
|
69
|
+
add_tsgolint_to_path() {
|
|
70
|
+
local tsgolint
|
|
71
|
+
tsgolint=$(find_binary tsgolint)
|
|
72
|
+
case "$tsgolint" in
|
|
73
|
+
*/*)
|
|
74
|
+
PATH="$(cd -P "$(dirname "$tsgolint")" && pwd):$PATH"
|
|
75
|
+
export PATH
|
|
76
|
+
;;
|
|
77
|
+
esac
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
add_tsgolint_to_path
|
|
81
|
+
|
|
82
|
+
# The config a consumer declares its rules in — the one thing that makes the
|
|
83
|
+
# drift report answerable. Without one there is no profile to have drifted from.
|
|
84
|
+
OXLINT_CONFIG=""
|
|
85
|
+
for candidate in oxlint.config.ts oxlint.config.mjs oxlint.config.js oxlint.config.cjs .oxlintrc.json; do
|
|
86
|
+
[ -f "$candidate" ] && { OXLINT_CONFIG="$candidate"; break; }
|
|
87
|
+
done
|
|
88
|
+
|
|
89
|
+
# The ratchet's file, at the project root. Its presence is what turns the oxlint
|
|
90
|
+
# pass from "no diagnostic at all" into "no diagnostic above what was recorded".
|
|
91
|
+
BASELINE_FILE="oxlint.baseline.json"
|
|
92
|
+
|
|
65
93
|
TSC=$(find_tsc)
|
|
66
94
|
OXLINT=$(find_binary oxlint)
|
|
67
95
|
OXFMT=$(find_binary oxfmt)
|
|
68
96
|
KNIP=$(find_binary knip)
|
|
97
|
+
DEPCRUISE=$(find_binary depcruise)
|
|
98
|
+
PUBLINT=$(find_binary publint)
|
|
99
|
+
ATTW=$(find_binary attw)
|
|
100
|
+
PRETTIER=$(find_binary prettier)
|
|
101
|
+
|
|
102
|
+
# `astro` is the CONSUMER's dependency, never this package's: the pass runs its
|
|
103
|
+
# checker, it does not ship one. So the lookup starts at the project.
|
|
104
|
+
find_project_binary() {
|
|
105
|
+
local name="$1"
|
|
106
|
+
if [ -x "node_modules/.bin/$name" ]; then
|
|
107
|
+
echo "$PWD/node_modules/.bin/$name"
|
|
108
|
+
else
|
|
109
|
+
find_binary "$name"
|
|
110
|
+
fi
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# The one file shape oxfmt does not parse. The values are the oxfmt values —
|
|
114
|
+
# 100 / 4 / single / all — so a consumer never declares a formatter of its own.
|
|
115
|
+
#
|
|
116
|
+
# The plugin is passed as a RESOLVED PATH, not as a name in the config: prettier
|
|
117
|
+
# resolves a plugin name from the working directory, which is the consumer's,
|
|
118
|
+
# and the consumer is precisely the project that no longer declares it.
|
|
119
|
+
PRETTIER_ASTRO_CONFIG="$PACKAGE_ROOT/presets/prettier/astro.json"
|
|
120
|
+
PRETTIER_ASTRO_PLUGIN=$(cd "$PACKAGE_ROOT" && node -e 'process.stdout.write(require.resolve("prettier-plugin-astro"))' 2>/dev/null)
|
|
69
121
|
CHECKER=$(find_binary jterrazz-test-check)
|
|
70
122
|
|
|
71
123
|
# ── The unit is the workspace package, not the repository ────────────────────
|
|
@@ -106,15 +158,22 @@ project_uses_jterrazz_test() {
|
|
|
106
158
|
node -e 'const {readFileSync}=require("node:fs");const p=JSON.parse(readFileSync(process.argv[1],"utf8"));const d={...p.dependencies,...p.devDependencies,...p.peerDependencies};process.exit(d["@jterrazz/test"]?0:1)' "$dir/package.json" 2>/dev/null
|
|
107
159
|
}
|
|
108
160
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
local
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
161
|
+
# An Astro project, read off its manifest. `.astro` is the one file shape oxfmt
|
|
162
|
+
# does not parse and `astro check` is the only checker that reads a template's
|
|
163
|
+
# frontmatter, so the pass exists exactly where the dependency does.
|
|
164
|
+
project_uses_astro() {
|
|
165
|
+
local dir="${1:-.}"
|
|
166
|
+
[ -f "$dir/package.json" ] || return 1
|
|
167
|
+
node -e 'const {readFileSync}=require("node:fs");const p=JSON.parse(readFileSync(process.argv[1],"utf8"));const d={...p.dependencies,...p.devDependencies,...p.peerDependencies};process.exit(d["astro"]?0:1)' "$dir/package.json" 2>/dev/null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# A package the registry would accept: it names an entry (`exports`, `main`) or
|
|
171
|
+
# a publish target, and it never says it is private. A private package, and a
|
|
172
|
+
# workspace root that only holds members, have no tarball to be judged on.
|
|
173
|
+
project_is_publishable() {
|
|
174
|
+
local dir="${1:-.}"
|
|
175
|
+
[ -f "$dir/package.json" ] || return 1
|
|
176
|
+
node -e 'const {readFileSync}=require("node:fs");const p=JSON.parse(readFileSync(process.argv[1],"utf8"));process.exit(p.private!==true&&(p.exports||p.main||p.publishConfig)?0:1)' "$dir/package.json" 2>/dev/null
|
|
118
177
|
}
|
|
119
178
|
|
|
120
179
|
# A path git has been told to forget is not this workspace's source. Clones,
|
|
@@ -174,31 +233,21 @@ discover_docs_roots() {
|
|
|
174
233
|
} | LC_ALL=C sort -u
|
|
175
234
|
}
|
|
176
235
|
|
|
177
|
-
# The
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
local is_cjs=false
|
|
188
|
-
case "$cfg" in
|
|
189
|
-
*.cjs) is_cjs=true ;;
|
|
236
|
+
# The name of the oxlint config, when that config is CommonJS — and nothing
|
|
237
|
+
# otherwise. Everything this package ships is ESM, and so is every oxlint JS
|
|
238
|
+
# plugin the estate writes; a CommonJS config cannot load either, and oxlint
|
|
239
|
+
# drops what it cannot load and still exits 0. The rules a config names are the
|
|
240
|
+
# whole claim of a lint run, so the shape of the config is the oxlint pass's
|
|
241
|
+
# business ([Quality checks](../../docs/06-quality-checks.md)).
|
|
242
|
+
commonjs_oxlint_config() {
|
|
243
|
+
case "$OXLINT_CONFIG" in
|
|
244
|
+
*.cjs) printf '%s' "$OXLINT_CONFIG" ;;
|
|
190
245
|
*.js)
|
|
191
246
|
if ! node -e 'process.exit(require("./package.json").type==="module"?0:1)' 2>/dev/null; then
|
|
192
|
-
|
|
247
|
+
printf '%s' "$OXLINT_CONFIG"
|
|
193
248
|
fi
|
|
194
249
|
;;
|
|
195
250
|
esac
|
|
196
|
-
|
|
197
|
-
if [ "$is_cjs" = true ]; then
|
|
198
|
-
printf "${RED} WARNING ${NC} @jterrazz/test is installed but %s is CommonJS.\n" "$cfg"
|
|
199
|
-
printf " The @jterrazz/test oxlint plugin is ESM-only and will be SILENTLY DROPPED —\n"
|
|
200
|
-
printf " none of the jterrazz/* rules will run. Switch to an ESM config (oxlint.config.ts or .mjs).\n\n"
|
|
201
|
-
fi
|
|
202
251
|
}
|
|
203
252
|
|
|
204
253
|
# Parse command and args
|
|
@@ -226,6 +275,75 @@ while [[ $# -gt 0 ]]; do
|
|
|
226
275
|
esac
|
|
227
276
|
done
|
|
228
277
|
|
|
278
|
+
# ── How a pass speaks ────────────────────────────────────────────────────────
|
|
279
|
+
# Every pass that RAN prints the same three things, in the same order: a `RUN`
|
|
280
|
+
# header carrying its label, whatever it has to say, and one verdict line. A
|
|
281
|
+
# pass that did not apply — no Astro in the project, no declared layer map —
|
|
282
|
+
# prints nothing at all, because it answered no question.
|
|
283
|
+
#
|
|
284
|
+
# What sits between the header and the verdict is the one variable: a failing
|
|
285
|
+
# pass prints its whole captured log, and a passing one stays silent unless it
|
|
286
|
+
# WROTE something. `fix` changed a file the operator owns and silence would hide
|
|
287
|
+
# it, so a writer asks for its log with the fourth argument; a reader's success
|
|
288
|
+
# chatter — oxlint's summary, which its reporter prints on a runner and in a
|
|
289
|
+
# terminal and drops under an AI agent — never reaches the stream, so a green
|
|
290
|
+
# run is byte-identical everywhere.
|
|
291
|
+
report_pass() {
|
|
292
|
+
local label="$1" status="$2" log="$3" writer="${4:-}"
|
|
293
|
+
|
|
294
|
+
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} %s\n\n" "$label"
|
|
295
|
+
if [ "$status" -ne 0 ] || [ -n "$writer" ]; then
|
|
296
|
+
[ -s "$log" ] && cat "$log"
|
|
297
|
+
fi
|
|
298
|
+
if [ "$status" -eq 0 ]; then
|
|
299
|
+
printf "${GREEN}✓ Passed${NC}\n"
|
|
300
|
+
else
|
|
301
|
+
printf "${RED}✗ Failed with exit code %d${NC}\n" "$status"
|
|
302
|
+
fi
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Two ways a lint run says nothing about the rules it was supposed to enforce.
|
|
306
|
+
#
|
|
307
|
+
# A config oxlint cannot parse — a `jsPlugins` naming a module that is not there
|
|
308
|
+
# is the common one — makes it print `Failed to parse oxlint configuration file`
|
|
309
|
+
# and lint nothing; 1.83.0 exits 1 for it, and this names the refusal in the
|
|
310
|
+
# toolchain's own vocabulary rather than leaving a reader with the tool's text.
|
|
311
|
+
# A CommonJS config is the silent one: oxlint drops it whole, prints NOTHING and
|
|
312
|
+
# exits 0, so the run is green having enforced no rule the config named.
|
|
313
|
+
#
|
|
314
|
+
# Both are the oxlint pass refusing, so both are written into its own log and
|
|
315
|
+
# both fail it. After this, `lint_status` is the verdict on what the linter
|
|
316
|
+
# actually ran, not on what it managed to exit with.
|
|
317
|
+
judge_lint_config() {
|
|
318
|
+
local cjs
|
|
319
|
+
cjs=$(commonjs_oxlint_config)
|
|
320
|
+
local refusals=""
|
|
321
|
+
|
|
322
|
+
if grep -q 'Failed to parse oxlint configuration file' "$tmp_dir/lint.log" 2>/dev/null; then
|
|
323
|
+
refusals+="oxlint-config-unparsed ${OXLINT_CONFIG} oxlint refused this config and linted nothing — its own report is above"$'\n'
|
|
324
|
+
fi
|
|
325
|
+
if [ -n "$cjs" ]; then
|
|
326
|
+
refusals+="oxlint-config-commonjs ${cjs} a CommonJS config cannot load an ESM preset or plugin, and oxlint drops what it cannot load — write oxlint.config.ts or .mjs"$'\n'
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
[ -z "$refusals" ] && return 0
|
|
330
|
+
|
|
331
|
+
printf '%s' "$refusals" >> "$tmp_dir/lint.log"
|
|
332
|
+
lint_status=1
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# One pass, N runs: the logs of the runs that FAILED, joined into the single log
|
|
336
|
+
# the pass reports under. A green member stays silent — it is the same pass.
|
|
337
|
+
join_logs() {
|
|
338
|
+
local into="$1"
|
|
339
|
+
shift
|
|
340
|
+
: > "$into"
|
|
341
|
+
local log
|
|
342
|
+
for log in "$@"; do
|
|
343
|
+
[ -s "$log" ] && cat "$log" >> "$into"
|
|
344
|
+
done
|
|
345
|
+
}
|
|
346
|
+
|
|
229
347
|
# Create a temporary directory for log files
|
|
230
348
|
tmp_dir=$(mktemp -d)
|
|
231
349
|
cleanup() { rm -rf "$tmp_dir"; }
|
|
@@ -243,21 +361,31 @@ run_checks() {
|
|
|
243
361
|
|
|
244
362
|
printf "${CYAN_BG}${BRIGHT_WHITE} START ${NC} ${LABEL}\n"
|
|
245
363
|
|
|
246
|
-
if workspace_uses_jterrazz_test; then
|
|
247
|
-
warn_cjs_oxlint_config
|
|
248
|
-
fi
|
|
249
|
-
|
|
250
364
|
# Run all tools in parallel
|
|
251
365
|
"$TSC" --noEmit > "$tmp_dir/type.log" 2>&1 &
|
|
252
366
|
local type_pid=$!
|
|
253
367
|
|
|
368
|
+
# --type-aware is explicit and unconditional: the rules it unlocks are the
|
|
369
|
+
# ones no syntactic linter can express, and a flag that is only sometimes
|
|
370
|
+
# passed is a rule set that is only sometimes enforced. `oxlint-tsgolint` is
|
|
371
|
+
# a dependency of this package, so it is there for every consumer.
|
|
254
372
|
if [ "$FIX_MODE" = true ]; then
|
|
255
|
-
"$OXLINT" --fix "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
|
|
373
|
+
"$OXLINT" --type-aware --fix "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
|
|
256
374
|
else
|
|
257
|
-
"$OXLINT" "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
|
|
375
|
+
"$OXLINT" --type-aware "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
|
|
258
376
|
fi
|
|
259
377
|
local lint_pid=$!
|
|
260
378
|
|
|
379
|
+
# The same run, machine-readable, so the ratchet can be judged rule by rule.
|
|
380
|
+
# A second invocation rather than a reformat of the first: the human log is
|
|
381
|
+
# what a failing pass prints, and neither form can be derived from the other.
|
|
382
|
+
local lint_json_pid=""
|
|
383
|
+
if [ "$FIX_MODE" = false ] && [ -f "$BASELINE_FILE" ]; then
|
|
384
|
+
"$OXLINT" --type-aware --format json "${LINT_ARGS[@]}" \
|
|
385
|
+
> "$tmp_dir/lint.json" 2>/dev/null &
|
|
386
|
+
lint_json_pid=$!
|
|
387
|
+
fi
|
|
388
|
+
|
|
261
389
|
if [ "$FIX_MODE" = true ]; then
|
|
262
390
|
"$OXFMT" > "$tmp_dir/format.log" 2>&1 &
|
|
263
391
|
else
|
|
@@ -329,6 +457,113 @@ run_checks() {
|
|
|
329
457
|
docs_layout_pid=$!
|
|
330
458
|
fi
|
|
331
459
|
|
|
460
|
+
# Suppressions (directives): every place the project told a checker to look
|
|
461
|
+
# away is spelled in this toolchain's vocabulary, carries its reason, and
|
|
462
|
+
# names a rule that is still live. It runs in BOTH modes — `--fix` settles
|
|
463
|
+
# the two spellings a machine can settle, and never invents a reason.
|
|
464
|
+
local suppressions_pid=""
|
|
465
|
+
local suppressions_status=0
|
|
466
|
+
local suppressions_fix=()
|
|
467
|
+
[ "$FIX_MODE" = true ] && suppressions_fix=(--fix)
|
|
468
|
+
node "$PACKAGE_ROOT/lib/check-suppressions.js" . --oxlint "$OXLINT" \
|
|
469
|
+
"${suppressions_fix[@]}" "${LINT_ARGS[@]}" > "$tmp_dir/suppressions.log" 2>&1 &
|
|
470
|
+
suppressions_pid=$!
|
|
471
|
+
|
|
472
|
+
# Markdown (prose): every tracked page's coordinates resolve, and its blocks
|
|
473
|
+
# breathe. Check-only — there is no rewrite that splits a paragraph into the
|
|
474
|
+
# two ideas it was carrying. It reads the same `--ignore-pattern` globs the
|
|
475
|
+
# linter received, so one flag answers for the whole run.
|
|
476
|
+
local markdown_pid=""
|
|
477
|
+
local markdown_status=0
|
|
478
|
+
if [ "$FIX_MODE" = false ]; then
|
|
479
|
+
node "$PACKAGE_ROOT/lib/check-markdown.js" . "${LINT_ARGS[@]}" \
|
|
480
|
+
> "$tmp_dir/markdown.log" 2>&1 &
|
|
481
|
+
markdown_pid=$!
|
|
482
|
+
fi
|
|
483
|
+
|
|
484
|
+
# Names: what the project calls its own parts, under the roots where a
|
|
485
|
+
# project keeps what it wrote. Check-only — renaming a file is a move, and
|
|
486
|
+
# choosing the name it moves to is the work the rule is asking for.
|
|
487
|
+
local names_pid=""
|
|
488
|
+
local names_status=0
|
|
489
|
+
if [ "$FIX_MODE" = false ]; then
|
|
490
|
+
node "$PACKAGE_ROOT/lib/check-names.js" . "${LINT_ARGS[@]}" > "$tmp_dir/names.log" 2>&1 &
|
|
491
|
+
names_pid=$!
|
|
492
|
+
fi
|
|
493
|
+
|
|
494
|
+
# Secrets: no file the project would commit carries a live-looking
|
|
495
|
+
# credential. The gate always applies, so WHICH engine answers — gitleaks
|
|
496
|
+
# where the machine has it, the built-in patterns where it does not — is
|
|
497
|
+
# decided inside the script, not here.
|
|
498
|
+
local secrets_pid=""
|
|
499
|
+
local secrets_status=0
|
|
500
|
+
if [ "$FIX_MODE" = false ]; then
|
|
501
|
+
node "$PACKAGE_ROOT/lib/check-secrets.js" . "${LINT_ARGS[@]}" \
|
|
502
|
+
> "$tmp_dir/secrets.log" 2>&1 &
|
|
503
|
+
secrets_pid=$!
|
|
504
|
+
fi
|
|
505
|
+
|
|
506
|
+
# Astro: the consumer's own checker, plus the formatter for the one file
|
|
507
|
+
# shape oxfmt does not parse. Both halves run in fix mode too — prettier
|
|
508
|
+
# writes there, and `astro check` is read-only wherever it runs.
|
|
509
|
+
local astro_pid=""
|
|
510
|
+
local astro_status=0
|
|
511
|
+
if project_uses_astro "."; then
|
|
512
|
+
ASTRO=$(find_project_binary astro)
|
|
513
|
+
# A subshell, so both halves run and both are reported — a template that
|
|
514
|
+
# does not type-check is not a reason to stay quiet about its shape.
|
|
515
|
+
# `local` has no meaning past the `&`, hence the plain names.
|
|
516
|
+
(
|
|
517
|
+
"$ASTRO" check
|
|
518
|
+
astro_check=$?
|
|
519
|
+
if [ "$FIX_MODE" = true ]; then
|
|
520
|
+
"$PRETTIER" --write --config "$PRETTIER_ASTRO_CONFIG" \
|
|
521
|
+
--plugin "$PRETTIER_ASTRO_PLUGIN" \
|
|
522
|
+
--no-error-on-unmatched-pattern "**/*.astro"
|
|
523
|
+
else
|
|
524
|
+
"$PRETTIER" --check --config "$PRETTIER_ASTRO_CONFIG" \
|
|
525
|
+
--plugin "$PRETTIER_ASTRO_PLUGIN" \
|
|
526
|
+
--no-error-on-unmatched-pattern "**/*.astro"
|
|
527
|
+
fi
|
|
528
|
+
astro_format=$?
|
|
529
|
+
[ $astro_check -eq 0 ] && [ $astro_format -eq 0 ]
|
|
530
|
+
) > "$tmp_dir/astro.log" 2>&1 &
|
|
531
|
+
astro_pid=$!
|
|
532
|
+
fi
|
|
533
|
+
|
|
534
|
+
# Architecture (layer map): the graph a project declared, resolved. Bash
|
|
535
|
+
# asks the one question that decides whether the gate applies at all — is
|
|
536
|
+
# there a map — and the script decides what it says. Opt-in by the file's
|
|
537
|
+
# existence: a project with no declared architecture is not in breach of one.
|
|
538
|
+
local architecture_pid=""
|
|
539
|
+
local architecture_status=0
|
|
540
|
+
if [ "$FIX_MODE" = false ] &&
|
|
541
|
+
{ [ -f ".dependency-cruiser.cjs" ] || [ -f ".dependency-cruiser.js" ] ||
|
|
542
|
+
[ -f ".dependency-cruiser.mjs" ]; }; then
|
|
543
|
+
node "$PACKAGE_ROOT/lib/check-architecture.js" . --depcruise "$DEPCRUISE" \
|
|
544
|
+
> "$tmp_dir/architecture.log" 2>&1 &
|
|
545
|
+
architecture_pid=$!
|
|
546
|
+
fi
|
|
547
|
+
|
|
548
|
+
# Publish (packaging): what a published package promises, held to what the
|
|
549
|
+
# tarball will contain. Once per package the registry would accept — the
|
|
550
|
+
# unit is the workspace package, and a private one has no tarball.
|
|
551
|
+
local publish_pids=()
|
|
552
|
+
local publish_logs=()
|
|
553
|
+
local publish_status=0
|
|
554
|
+
if [ "$FIX_MODE" = false ]; then
|
|
555
|
+
local publish_index=0
|
|
556
|
+
for publish_root in "." "${WORKSPACE_MEMBERS[@]}"; do
|
|
557
|
+
project_is_publishable "$publish_root" || continue
|
|
558
|
+
node "$PACKAGE_ROOT/lib/check-publish.js" "$publish_root" \
|
|
559
|
+
--publint "$PUBLINT" --attw "$ATTW" \
|
|
560
|
+
> "$tmp_dir/publish-$publish_index.log" 2>&1 &
|
|
561
|
+
publish_pids+=($!)
|
|
562
|
+
publish_logs+=("$tmp_dir/publish-$publish_index.log")
|
|
563
|
+
publish_index=$((publish_index + 1))
|
|
564
|
+
done
|
|
565
|
+
fi
|
|
566
|
+
|
|
332
567
|
# Conventions checker: only in check mode, once per specs root the workspace
|
|
333
568
|
# owns, gated by the package that OWNS that root — a member may depend on
|
|
334
569
|
# @jterrazz/test while the root does not, and the reverse.
|
|
@@ -371,10 +606,27 @@ run_checks() {
|
|
|
371
606
|
# Wait and collect statuses
|
|
372
607
|
wait $type_pid; local type_status=$?
|
|
373
608
|
wait $lint_pid; local lint_status=$?
|
|
609
|
+
|
|
610
|
+
# The ratchet, where the project keeps one: the pass is judged by what the
|
|
611
|
+
# baseline tolerates, not by oxlint's exit code. Bash decides whether the
|
|
612
|
+
# file is there; the script decides what it says.
|
|
613
|
+
if [ -n "$lint_json_pid" ]; then
|
|
614
|
+
wait $lint_json_pid
|
|
615
|
+
node "$PACKAGE_ROOT/lib/check-baseline.js" "$tmp_dir/lint.json" . \
|
|
616
|
+
>> "$tmp_dir/lint.log" 2>&1
|
|
617
|
+
lint_status=$?
|
|
618
|
+
fi
|
|
619
|
+
|
|
374
620
|
wait $format_pid; local format_status=$?
|
|
375
621
|
[ -n "$knip_pid" ] && { wait $knip_pid; knip_status=$?; }
|
|
376
622
|
[ -n "$gitignore_pid" ] && { wait $gitignore_pid; gitignore_status=$?; }
|
|
377
623
|
[ -n "$docs_layout_pid" ] && { wait $docs_layout_pid; docs_layout_status=$?; }
|
|
624
|
+
wait $suppressions_pid; suppressions_status=$?
|
|
625
|
+
[ -n "$markdown_pid" ] && { wait $markdown_pid; markdown_status=$?; }
|
|
626
|
+
[ -n "$architecture_pid" ] && { wait $architecture_pid; architecture_status=$?; }
|
|
627
|
+
[ -n "$astro_pid" ] && { wait $astro_pid; astro_status=$?; }
|
|
628
|
+
[ -n "$names_pid" ] && { wait $names_pid; names_status=$?; }
|
|
629
|
+
[ -n "$secrets_pid" ] && { wait $secrets_pid; secrets_status=$?; }
|
|
378
630
|
|
|
379
631
|
# One pass, N runs: the pass fails if any run failed, and only the logs of
|
|
380
632
|
# the runs that FAILED are printed — a green member stays silent.
|
|
@@ -388,6 +640,16 @@ run_checks() {
|
|
|
388
640
|
index=$((index + 1))
|
|
389
641
|
done
|
|
390
642
|
|
|
643
|
+
local publish_failed_logs=()
|
|
644
|
+
index=0
|
|
645
|
+
for pid in "${publish_pids[@]}"; do
|
|
646
|
+
if ! wait "$pid"; then
|
|
647
|
+
publish_status=1
|
|
648
|
+
publish_failed_logs+=("${publish_logs[$index]}")
|
|
649
|
+
fi
|
|
650
|
+
index=$((index + 1))
|
|
651
|
+
done
|
|
652
|
+
|
|
391
653
|
local docs_failed_logs=()
|
|
392
654
|
index=0
|
|
393
655
|
for pid in "${docs_pids[@]}"; do
|
|
@@ -398,92 +660,61 @@ run_checks() {
|
|
|
398
660
|
index=$((index + 1))
|
|
399
661
|
done
|
|
400
662
|
|
|
401
|
-
#
|
|
402
|
-
#
|
|
403
|
-
#
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
printf "${GREEN}✓ Passed${NC}\n"
|
|
410
|
-
fi
|
|
663
|
+
# ── The report ───────────────────────────────────────────────────────────
|
|
664
|
+
# One order, and it is the chapter's: the three tools, the artefact gate,
|
|
665
|
+
# knip, the conventions checker, the two Docs passes, then the gates a
|
|
666
|
+
# project opts into and the four that read its tree on every run. A pass
|
|
667
|
+
# that did not apply is absent; every pass that ran prints the same block.
|
|
668
|
+
join_logs "$tmp_dir/checker.log" "${checker_failed_logs[@]}"
|
|
669
|
+
join_logs "$tmp_dir/docs.log" "${docs_failed_logs[@]}"
|
|
670
|
+
join_logs "$tmp_dir/publish.log" "${publish_failed_logs[@]}"
|
|
411
671
|
|
|
412
672
|
local lint_label="Oxlint Check"
|
|
413
|
-
[ "$FIX_MODE" = true ] && lint_label="Oxlint Fix"
|
|
414
|
-
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} ${lint_label}\n\n"
|
|
415
|
-
if [ $lint_status -ne 0 ]; then
|
|
416
|
-
[ -s "$tmp_dir/lint.log" ] && cat "$tmp_dir/lint.log"
|
|
417
|
-
printf "${RED}✗ Failed with exit code %d${NC}\n" $lint_status
|
|
418
|
-
else
|
|
419
|
-
printf "${GREEN}✓ Passed${NC}\n"
|
|
420
|
-
fi
|
|
421
|
-
|
|
422
673
|
local format_label="Oxfmt Check"
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
else
|
|
429
|
-
printf "${GREEN}✓ Passed${NC}\n"
|
|
430
|
-
fi
|
|
431
|
-
|
|
432
|
-
# The one pass that speaks on success: a rewrite changed a file the operator
|
|
433
|
-
# owns, and silence would hide it. In check mode a green gate writes nothing,
|
|
434
|
-
# so the green output stays byte-identical with the others.
|
|
435
|
-
if [ -n "$gitignore_pid" ]; then
|
|
436
|
-
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Gitignore (artefacts)\n\n"
|
|
437
|
-
[ -s "$tmp_dir/gitignore.log" ] && cat "$tmp_dir/gitignore.log"
|
|
438
|
-
if [ $gitignore_status -ne 0 ]; then
|
|
439
|
-
printf "${RED}✗ Failed with exit code %d${NC}\n" $gitignore_status
|
|
440
|
-
else
|
|
441
|
-
printf "${GREEN}✓ Passed${NC}\n"
|
|
442
|
-
fi
|
|
674
|
+
local write=""
|
|
675
|
+
if [ "$FIX_MODE" = true ]; then
|
|
676
|
+
lint_label="Oxlint Fix"
|
|
677
|
+
format_label="Oxfmt Format"
|
|
678
|
+
write="writer"
|
|
443
679
|
fi
|
|
444
680
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
printf "${RED}✗ Failed with exit code %d${NC}\n" $docs_status
|
|
483
|
-
else
|
|
484
|
-
printf "${GREEN}✓ Passed${NC}\n"
|
|
485
|
-
fi
|
|
486
|
-
fi
|
|
681
|
+
judge_lint_config
|
|
682
|
+
report_pass "TypeScript Check" $type_status "$tmp_dir/type.log"
|
|
683
|
+
report_pass "$lint_label" $lint_status "$tmp_dir/lint.log"
|
|
684
|
+
report_pass "$format_label" $format_status "$tmp_dir/format.log"
|
|
685
|
+
[ -n "$gitignore_pid" ] &&
|
|
686
|
+
report_pass "Gitignore (artefacts)" $gitignore_status "$tmp_dir/gitignore.log" writer
|
|
687
|
+
[ "$FIX_MODE" = false ] &&
|
|
688
|
+
report_pass "Knip (unused code)" $knip_status "$tmp_dir/knip.log"
|
|
689
|
+
[ ${#checker_pids[@]} -gt 0 ] &&
|
|
690
|
+
report_pass "Test Conventions (@jterrazz/test)" $checker_status "$tmp_dir/checker.log"
|
|
691
|
+
[ -n "$docs_layout_pid" ] &&
|
|
692
|
+
report_pass "Docs (layout)" $docs_layout_status "$tmp_dir/docs-layout.log"
|
|
693
|
+
[ ${#docs_pids[@]} -gt 0 ] &&
|
|
694
|
+
report_pass "Docs (sync)" $docs_status "$tmp_dir/docs.log"
|
|
695
|
+
[ ${#publish_pids[@]} -gt 0 ] &&
|
|
696
|
+
report_pass "Publish (packaging)" $publish_status "$tmp_dir/publish.log"
|
|
697
|
+
[ -n "$architecture_pid" ] &&
|
|
698
|
+
report_pass "Architecture (layer map)" $architecture_status "$tmp_dir/architecture.log"
|
|
699
|
+
[ -n "$astro_pid" ] &&
|
|
700
|
+
report_pass "Astro (check + format)" $astro_status "$tmp_dir/astro.log" "$write"
|
|
701
|
+
report_pass "Suppressions (directives)" $suppressions_status "$tmp_dir/suppressions.log" "$write"
|
|
702
|
+
[ -n "$markdown_pid" ] &&
|
|
703
|
+
report_pass "Markdown (prose)" $markdown_status "$tmp_dir/markdown.log"
|
|
704
|
+
[ -n "$names_pid" ] && report_pass "Names (tree)" $names_status "$tmp_dir/names.log"
|
|
705
|
+
[ -n "$secrets_pid" ] && report_pass "Secrets (credentials)" $secrets_status "$tmp_dir/secrets.log"
|
|
706
|
+
|
|
707
|
+
# Drift: the report, not a gate — how far this project stands from the
|
|
708
|
+
# profile it says it extends, in four numbers. It runs last and it speaks on
|
|
709
|
+
# every check, because the alternative is what the estate had: every
|
|
710
|
+
# repository quietly a little further from the shared rulebook, and nobody
|
|
711
|
+
# able to say by how much without opening every config. Only a rule turned
|
|
712
|
+
# off with no reason beside it actually fails the run.
|
|
713
|
+
local drift_status=0
|
|
714
|
+
if [ "$FIX_MODE" = false ] && [ -n "$OXLINT_CONFIG" ]; then
|
|
715
|
+
printf "\n${CYAN_BG}${BRIGHT_WHITE} DRIFT ${NC} Deviations from the profile\n\n"
|
|
716
|
+
node "$PACKAGE_ROOT/lib/check-drift.js" . --oxlint "$OXLINT" "${LINT_ARGS[@]}"
|
|
717
|
+
drift_status=$?
|
|
487
718
|
fi
|
|
488
719
|
|
|
489
720
|
# Summary
|
|
@@ -493,7 +724,7 @@ run_checks() {
|
|
|
493
724
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} END ${NC} Finalizing quality checks\n\n"
|
|
494
725
|
fi
|
|
495
726
|
|
|
496
|
-
if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ] && [ $gitignore_status -eq 0 ] && [ $checker_status -eq 0 ] && [ $docs_layout_status -eq 0 ] && [ $docs_status -eq 0 ]; then
|
|
727
|
+
if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ] && [ $gitignore_status -eq 0 ] && [ $checker_status -eq 0 ] && [ $docs_layout_status -eq 0 ] && [ $docs_status -eq 0 ] && [ $markdown_status -eq 0 ] && [ $names_status -eq 0 ] && [ $secrets_status -eq 0 ] && [ $suppressions_status -eq 0 ] && [ $publish_status -eq 0 ] && [ $architecture_status -eq 0 ] && [ $astro_status -eq 0 ] && [ $drift_status -eq 0 ]; then
|
|
497
728
|
printf "${GREEN}✓ All checks passed${NC}\n"
|
|
498
729
|
exit 0
|
|
499
730
|
else
|