@avi2dg/checks 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/LICENSE +21 -0
- package/README.md +363 -0
- package/bunfig.toml +2 -0
- package/commitlint.config.js +22 -0
- package/dependency-cruiser.config.js +72 -0
- package/dist/index.js +80 -0
- package/oxlintrc.json +21 -0
- package/package.json +70 -0
- package/scripts/backtest.ts +117 -0
- package/scripts/comment-gate.ts +116 -0
- package/scripts/comments.ts +298 -0
- package/scripts/commit-identity.ts +153 -0
- package/scripts/lint-coverage.sh +27 -0
- package/scripts/test-layout.ts +316 -0
- package/tsconfig.effect.json +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 avi2d
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# checks
|
|
2
|
+
|
|
3
|
+
Deterministic checks shared across my TypeScript repos. One package,
|
|
4
|
+
`@avi2dg/checks`: the oxlint base config, the tsconfig fragment with the
|
|
5
|
+
Effect language-service block, the shared commitlint config, the shared
|
|
6
|
+
dependency-cruiser base, the test-layout check with its bunfig preset,
|
|
7
|
+
the commit-identity check with its workflow, the comment gate with its
|
|
8
|
+
workflow and backtest, and the Effect error-channel plugin compiled to
|
|
9
|
+
JavaScript.
|
|
10
|
+
|
|
11
|
+
Published as `@avi2dg/checks` on the public npm registry.
|
|
12
|
+
|
|
13
|
+
## Consume it
|
|
14
|
+
|
|
15
|
+
From the consuming repo:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
bun add -d @avi2dg/checks oxlint@1.83.0 oxlint-tsgolint@7.0.2002 @effect/tsgo@0.45.0 typescript@7.0.2 dependency-cruiser@18.4.0 @swc/core@1.16.2
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`.oxlintrc.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"extends": ["./node_modules/@avi2dg/checks/oxlintrc.json"],
|
|
26
|
+
"plugins": ["typescript", "oxc", "eslint", "import"]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`.gitignore` keeps oxlint out of `node_modules/`:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
node_modules/
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`tsconfig.json` gains one line:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"extends": "@avi2dg/checks/tsconfig.effect.json"
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`bunfig.toml` is a copy of the shipped preset:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
cp node_modules/@avi2dg/checks/bunfig.toml bunfig.toml
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`package.json` gains three scripts:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
"lint": "oxlint --type-aware && ./node_modules/@avi2dg/checks/scripts/lint-coverage.sh && bun ./node_modules/@avi2dg/checks/scripts/test-layout.ts && bun ./node_modules/@avi2dg/checks/scripts/commit-identity.ts HEAD",
|
|
54
|
+
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
55
|
+
"test": "bun test --randomize"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`lint-coverage.sh` fails when oxlint silently skips a tracked `.ts` or
|
|
59
|
+
`.tsx` file, for example through a stray `.gitignore` entry. It compares
|
|
60
|
+
`git ls-files` against oxlint's own file walk and names the missing files.
|
|
61
|
+
|
|
62
|
+
`test-layout.ts` decides the test layout described below.
|
|
63
|
+
|
|
64
|
+
`commit-identity.ts` refuses a commit with an author other than the
|
|
65
|
+
repository owner; see "Commit identity" below.
|
|
66
|
+
|
|
67
|
+
The registry version is pinned by the consumer's lockfile; bump
|
|
68
|
+
`@avi2dg/checks` to adopt a new release.
|
|
69
|
+
|
|
70
|
+
## Test layout
|
|
71
|
+
|
|
72
|
+
`bun ./node_modules/@avi2dg/checks/scripts/test-layout.ts` fails unless the
|
|
73
|
+
repo holds this shape, and names the file and the path to move it to when
|
|
74
|
+
it does not:
|
|
75
|
+
|
|
76
|
+
- Every test file is `tests/**/*.test.ts` or `.tsx`. A `*.test.ts`, `*.spec.ts` or
|
|
77
|
+
`*_test.ts` under `src/`, `test/`, `__tests__/` or the repo root fails.
|
|
78
|
+
Tracked and untracked files that `git ls-files --exclude-standard`
|
|
79
|
+
reports are scanned, so `node_modules/` and every gitignored tree are
|
|
80
|
+
out of reach, and a local run agrees with CI before `git add`.
|
|
81
|
+
- `tests/lib/**` holds helpers and `tests/fixtures/**` holds data; neither
|
|
82
|
+
may hold a test file. Every other directory directly under `tests/` is a
|
|
83
|
+
test group and may nest as deep as it likes.
|
|
84
|
+
- Two levels. A test outside `tests/e2e/` runs in-process, so it may not
|
|
85
|
+
import `node:child_process`, `net`, `http`, `https`, `http2`, `tls` or
|
|
86
|
+
`dgram`, may not import `$`, `spawn`, `spawnSync`, `connect`, `serve` or
|
|
87
|
+
`listen` from `bun`, may not touch `Bun.$` or `Bun.spawn`, and may not
|
|
88
|
+
call `fetch`. A test inside `tests/e2e/` may do all of it. Helpers in
|
|
89
|
+
`tests/lib/**` answer to the same rule, since an in-process test reaches
|
|
90
|
+
them; `tests/fixtures/**` is data and is not parsed. Detection parses
|
|
91
|
+
with swc and reads import specifiers and identifier use, so a test that
|
|
92
|
+
only carries `"node:child_process"` as a string is not a violation.
|
|
93
|
+
- `scripts.test` is exactly `bun test --randomize` and `scripts.lint` runs
|
|
94
|
+
this check.
|
|
95
|
+
- `bunfig.toml` carries every `[test]` key of the shipped preset with the
|
|
96
|
+
same value. Other tables, and extra `[test]` keys, are the repo's own.
|
|
97
|
+
|
|
98
|
+
The in-process half is what a mutation run can mutate; `tests/e2e/**` is
|
|
99
|
+
excluded from a mutate scope by construction, because a subprocess kills
|
|
100
|
+
both the speed and the coverage signal a mutant needs.
|
|
101
|
+
|
|
102
|
+
The preset also skips `tests/quarantine/**` on a default run. A test that
|
|
103
|
+
turns flaky moves there, so the suite stays trustworthy, and the flake is
|
|
104
|
+
still run on demand:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
bun test --path-ignore-patterns='' tests/quarantine
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Dependency rules
|
|
111
|
+
|
|
112
|
+
`.dependency-cruiser.cjs` extends the shared base, which carries
|
|
113
|
+
`no-circular`, `no-orphans`, `not-to-dev-dep` (shipped source importing
|
|
114
|
+
a dev-only package, which a package listed in `peerDependencies` too is
|
|
115
|
+
not), `not-to-unresolvable` (nothing installed answers the
|
|
116
|
+
specifier), and `no-deep-imports` (a subpath the package's exports map
|
|
117
|
+
does not publish):
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
module.exports = {
|
|
121
|
+
extends: "./node_modules/@avi2dg/checks/dependency-cruiser.config.js",
|
|
122
|
+
forbidden: [
|
|
123
|
+
{
|
|
124
|
+
name: "ui-cannot-reach-server",
|
|
125
|
+
severity: "error",
|
|
126
|
+
from: { path: "^src/ui" },
|
|
127
|
+
to: { path: "^src/server" },
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
That last block is the layer-boundary recipe: append a named rule per
|
|
134
|
+
boundary you own. A rule that restates a base name overrides it field
|
|
135
|
+
by field, which is how an entry point stops being an orphan:
|
|
136
|
+
redeclare `no-orphans` with your entry added to its `pathNot`.
|
|
137
|
+
This repo's own `.dependency-cruiser.cjs` does that for the plugin
|
|
138
|
+
entry.
|
|
139
|
+
|
|
140
|
+
`package.json` gains the script:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
"lint:deps": "depcruise --config .dependency-cruiser.cjs src"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Enforcement runs in CI beside the other checks:
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
jobs:
|
|
150
|
+
lint:
|
|
151
|
+
steps:
|
|
152
|
+
- uses: actions/checkout@v5
|
|
153
|
+
- uses: oven-sh/setup-bun@v2
|
|
154
|
+
- run: bun install --frozen-lockfile
|
|
155
|
+
- run: bun run lint:deps
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Commit lint
|
|
159
|
+
|
|
160
|
+
Commits follow `@commitlint/config-conventional` plus the house
|
|
161
|
+
prefixes listed in `commitlint.config.js`, shared from
|
|
162
|
+
`@avi2dg/checks/commitlint.config.js`. It arrives with the registry
|
|
163
|
+
dependency above, since `@commitlint/cli` and
|
|
164
|
+
`@commitlint/config-conventional` are dependencies, not peers.
|
|
165
|
+
Enforcement runs in CI on pull requests, because `jj` never fires a
|
|
166
|
+
git hook. Add this workflow to the consuming repo:
|
|
167
|
+
|
|
168
|
+
```yaml
|
|
169
|
+
on:
|
|
170
|
+
pull_request:
|
|
171
|
+
types: [opened, edited, synchronize, reopened]
|
|
172
|
+
jobs:
|
|
173
|
+
commitlint:
|
|
174
|
+
uses: avi2d/checks/.github/workflows/commitlint.yml@main
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
It lints the pull request title and nothing else. The title is the
|
|
178
|
+
enforced subject because squash merges use the PR title as the main
|
|
179
|
+
commit subject; per-commit messages are not linted. GitHub appends
|
|
180
|
+
` (#N)` to the squashed subject, so the workflow lints the title with
|
|
181
|
+
that suffix attached and the header length limit applies to the landed
|
|
182
|
+
subject, not the bare title.
|
|
183
|
+
|
|
184
|
+
It never sees a commit's author or committer fields, nor the
|
|
185
|
+
`Co-authored-by` trailer GitHub writes from a foreign author when it
|
|
186
|
+
squashes, so it cannot enforce who a commit belongs to. The
|
|
187
|
+
commit-identity check below is the enforcement.
|
|
188
|
+
|
|
189
|
+
## Commit identity
|
|
190
|
+
|
|
191
|
+
`scripts/commit-identity.ts` walks every commit in a range and fails when
|
|
192
|
+
one carries an identity other than the repository owner's:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
bun ./node_modules/@avi2dg/checks/scripts/commit-identity.ts <base-ref> <head-ref>
|
|
196
|
+
bun ./node_modules/@avi2dg/checks/scripts/commit-identity.ts <ref>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
With one argument it checks that commit alone, which is the form the
|
|
200
|
+
`lint` script above runs on `HEAD`. It refuses a commit whose author or
|
|
201
|
+
committer is outside the allowlist, and one whose trailer block carries
|
|
202
|
+
a `Co-authored-by:` trailer as git parses it, and it names the offending
|
|
203
|
+
commit and reason. Other trailers and prose mentioning an address in the
|
|
204
|
+
body are left alone. `GitHub <noreply@github.com>` is allowed as
|
|
205
|
+
committer only, since that is who writes a squash merge.
|
|
206
|
+
|
|
207
|
+
The allowlist defaults to `avi2d <avi2dg@gmail.com>`. A repo with other
|
|
208
|
+
owners restates it in `package.json`:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
"commitIdentity": {
|
|
212
|
+
"authors": [{ "name": "avi2d", "email": "avi2dg@gmail.com" }]
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Enforcement runs on pull requests, where the range from the base branch's
|
|
217
|
+
current tip to the head is visible:
|
|
218
|
+
|
|
219
|
+
```yaml
|
|
220
|
+
on:
|
|
221
|
+
pull_request:
|
|
222
|
+
types: [opened, edited, synchronize, reopened]
|
|
223
|
+
jobs:
|
|
224
|
+
commit-identity:
|
|
225
|
+
uses: avi2d/checks/.github/workflows/commit-identity.yml@main
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The workflow fetches the consumer's full history and ranges from the
|
|
229
|
+
fetched base branch, not the event's recorded base sha, which GitHub
|
|
230
|
+
leaves stale once the base branch advances after the pull request opens.
|
|
231
|
+
|
|
232
|
+
## Comment gate
|
|
233
|
+
|
|
234
|
+
`scripts/comment-gate.ts` runs the comment check over a diff and fails
|
|
235
|
+
when an added line carries a banned comment:
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
bun ./node_modules/@avi2dg/checks/scripts/comment-gate.ts <base-ref> <head-ref>
|
|
239
|
+
bun ./node_modules/@avi2dg/checks/scripts/comment-gate.ts <ref>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
With two arguments it diffs the base against the head. With one it diffs
|
|
243
|
+
that commit against its parent, and exits 2 when that parent is not in
|
|
244
|
+
the clone rather than widening to the whole tree, so a shallow checkout
|
|
245
|
+
running the one-argument form needs `fetch-depth: 2`. Only added lines
|
|
246
|
+
are checked, so a violation in a file the diff never touches stays
|
|
247
|
+
silent, and a refusal counts when any line of the comment carrying it was
|
|
248
|
+
added. The check refuses a machine-read directive, a record or ticket
|
|
249
|
+
pointer, a doc block, and a file opening with a rationale block over
|
|
250
|
+
three lines, licence headers excepted; `scripts/comments.ts` holds the
|
|
251
|
+
scanner the gate and the backtest share.
|
|
252
|
+
|
|
253
|
+
Enforcement runs on pull requests, where the range from the base branch's
|
|
254
|
+
current tip to the head is visible:
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
on:
|
|
258
|
+
pull_request:
|
|
259
|
+
types: [opened, edited, synchronize, reopened]
|
|
260
|
+
jobs:
|
|
261
|
+
comment-gate:
|
|
262
|
+
uses: avi2d/checks/.github/workflows/comment-gate.yml@main
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Backtest
|
|
266
|
+
|
|
267
|
+
`scripts/backtest.ts` reports what the comment check would have refused
|
|
268
|
+
at each recent commit, so a repository can measure its own history:
|
|
269
|
+
|
|
270
|
+
```sh
|
|
271
|
+
bun ./node_modules/@avi2dg/checks/scripts/backtest.ts [commit-count]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
It walks the last `commit-count` first-parent commits, 60 by default,
|
|
275
|
+
prints a row per commit that touches code and then the totals,
|
|
276
|
+
attributes only the refusals each commit introduced, and counts new
|
|
277
|
+
comment text as a share of added lines.
|
|
278
|
+
`generated/`, `vendor/`, `repos/`, `node_modules/` and `dist/` are out
|
|
279
|
+
of reach, so the figures are authored code.
|
|
280
|
+
|
|
281
|
+
## Why it is shaped this way
|
|
282
|
+
|
|
283
|
+
- `plugins` does not inherit through oxlint `extends`. `rules`,
|
|
284
|
+
`categories` and `jsPlugins` do. That is why the consumer snippet
|
|
285
|
+
restates `plugins` and nothing else.
|
|
286
|
+
- `node_modules/` is excluded through the consumer's `.gitignore`, not
|
|
287
|
+
`ignorePatterns`: oxlint still walks the installed package when only
|
|
288
|
+
`ignorePatterns` names it.
|
|
289
|
+
- `files` in package.json is the published surface: `tests/`, `AGENTS.md`
|
|
290
|
+
and the `.ts` plugin source never reach an install. npm adds
|
|
291
|
+
`package.json`, `README` and `LICENSE` to the tarball whatever `files`
|
|
292
|
+
says. `bun pm pack` builds the same tarball the registry serves, which
|
|
293
|
+
is what the packed-tarball consumer e2e test installs.
|
|
294
|
+
- The plugin ships compiled as `dist/index.js`, built with
|
|
295
|
+
`bun build effect-channel/index.ts --outdir dist --target node --format esm`.
|
|
296
|
+
Node refuses to type-strip a `.ts` plugin under `node_modules`, so the
|
|
297
|
+
`.ts` source would fail to load from an installed package.
|
|
298
|
+
- `dist/` is committed. No `prepack` or `prepublishOnly` builds it, so a
|
|
299
|
+
publish ships whatever bundle the publishing worktree holds. Rebuild it
|
|
300
|
+
after pulling with `bun run build`; CI fails when the committed bundle
|
|
301
|
+
is stale.
|
|
302
|
+
- The base parses with swc because typescript 7 (tsgo) has no compiler
|
|
303
|
+
API for dependency-cruiser to use. Without `@swc/core` installed the
|
|
304
|
+
cruise silently skips every `.ts` file, so this repo's test asserts its
|
|
305
|
+
own TypeScript is cruised.
|
|
306
|
+
- `bunfig.toml` has no `extends` and no include: bun ignores an unknown
|
|
307
|
+
top-level key in silence, so a preset cannot be inherited and the
|
|
308
|
+
consumer's copy is compared key by key against the installed one
|
|
309
|
+
instead. `[test] pathIgnorePatterns` is a real bunfig key, and an empty
|
|
310
|
+
`--path-ignore-patterns` flag overrides the file's own list.
|
|
311
|
+
- The layout check ships as `.ts` and is invoked with `bun`, which needs
|
|
312
|
+
no build step and no `dist/` entry, unlike the oxlint plugin that node
|
|
313
|
+
loads.
|
|
314
|
+
- `bun` counts as a built-in module. Nothing installed resolves it except
|
|
315
|
+
`@types/bun`, which would otherwise make every runtime `bun` import look
|
|
316
|
+
like a dev-only dependency.
|
|
317
|
+
- The pull request merge commit GitHub builds is authored by `GitHub
|
|
318
|
+
<noreply@github.com>`, which commit-identity refuses as an author. A
|
|
319
|
+
consumer that runs the check inside `lint` checks out
|
|
320
|
+
`github.event.pull_request.head.sha` instead of the default merge ref,
|
|
321
|
+
as this repo's `ci.yml` does.
|
|
322
|
+
- `no-deep-imports` judges the import specifier, never the resolved file.
|
|
323
|
+
The base honours `exports` maps, so a subpath the map publishes resolves
|
|
324
|
+
and passes, one it omits fails to resolve and is reported, and a package
|
|
325
|
+
without an `exports` map publishes every file. A bare import always
|
|
326
|
+
passes whatever file its entry lives in. Setting your own
|
|
327
|
+
`options.enhancedResolveOptions` replaces the base's, so restate
|
|
328
|
+
`exportsFields` and `conditionNames` if you do.
|
|
329
|
+
- dependency-cruiser `extends` merges same-name `forbidden` rules with the
|
|
330
|
+
child's fields winning. That is the entry-point and layer recipe above.
|
|
331
|
+
|
|
332
|
+
## Develop
|
|
333
|
+
|
|
334
|
+
```sh
|
|
335
|
+
bun install
|
|
336
|
+
bun run build
|
|
337
|
+
bun run lint
|
|
338
|
+
bun run typecheck
|
|
339
|
+
bun run test
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Release by tagging a commit on `main`. The `release` workflow refuses
|
|
343
|
+
a tag off `main` and reruns the build, `dist/` check, lint, typecheck
|
|
344
|
+
and tests before it publishes:
|
|
345
|
+
|
|
346
|
+
```sh
|
|
347
|
+
git tag v0.2.0 && git push origin v0.2.0
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
The `release` workflow publishes the tagged version through npm
|
|
351
|
+
trusted publishing, so the tag and `package.json` must agree. No
|
|
352
|
+
token is stored anywhere: GitHub mints the publish credential for
|
|
353
|
+
each run.
|
|
354
|
+
|
|
355
|
+
npm attaches a trusted publisher only to a package that already
|
|
356
|
+
exists, so the first version goes out by hand: `npm publish` from the
|
|
357
|
+
tagged commit as `avi2dg`, then add the trusted publisher (repository
|
|
358
|
+
`avi2d/checks`, workflow `release.yml`) in the package's npm settings.
|
|
359
|
+
That first tag's `release` run fails on the already-published version;
|
|
360
|
+
every later tag publishes through the workflow.
|
|
361
|
+
|
|
362
|
+
`publishConfig.access` in package.json is what makes the scoped package
|
|
363
|
+
public.
|
package/bunfig.toml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import configConventional from "@commitlint/config-conventional";
|
|
2
|
+
|
|
3
|
+
const houseTypes = [
|
|
4
|
+
"theme",
|
|
5
|
+
"herdr",
|
|
6
|
+
"renamer",
|
|
7
|
+
"shell",
|
|
8
|
+
"pi",
|
|
9
|
+
"home",
|
|
10
|
+
"audit",
|
|
11
|
+
"comments",
|
|
12
|
+
"unslop",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const conventionalEnum = configConventional.rules["type-enum"];
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
extends: ["@commitlint/config-conventional"],
|
|
19
|
+
rules: {
|
|
20
|
+
"type-enum": [conventionalEnum[0], conventionalEnum[1], [...conventionalEnum[2], ...houseTypes]],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
forbidden: [
|
|
3
|
+
{
|
|
4
|
+
name: "no-circular",
|
|
5
|
+
severity: "error",
|
|
6
|
+
comment: "A circular relationship never has a single entry point, so invert one side.",
|
|
7
|
+
from: {},
|
|
8
|
+
to: { circular: true },
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "no-orphans",
|
|
12
|
+
severity: "error",
|
|
13
|
+
comment:
|
|
14
|
+
"Nothing reaches this module. Use it, remove it, or exempt the entry point by name in your own config.",
|
|
15
|
+
from: {
|
|
16
|
+
orphan: true,
|
|
17
|
+
pathNot: [
|
|
18
|
+
"(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$",
|
|
19
|
+
"[.]d[.]ts$",
|
|
20
|
+
"(^|/)tsconfig[.]json$",
|
|
21
|
+
"(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$",
|
|
22
|
+
"(^|/)[^/]*[.]config[.](?:js|cjs|mjs|ts|cts|mts)$",
|
|
23
|
+
"[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$",
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
to: {},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "not-to-dev-dep",
|
|
30
|
+
severity: "error",
|
|
31
|
+
comment:
|
|
32
|
+
"Shipped source cannot rely on a package that is absent in production. Move it to dependencies or peerDependencies, or keep the import in a test or config file.",
|
|
33
|
+
from: {
|
|
34
|
+
pathNot: [
|
|
35
|
+
"[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$",
|
|
36
|
+
"(^|/)[^/]*[.]config[.](?:js|cjs|mjs|ts|cts|mts)$",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
to: {
|
|
40
|
+
dependencyTypes: ["npm-dev"],
|
|
41
|
+
dependencyTypesNot: ["type-only", "npm-peer"],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "not-to-unresolvable",
|
|
46
|
+
severity: "error",
|
|
47
|
+
comment: "Nothing installed answers to this specifier. Install the package or fix the path.",
|
|
48
|
+
from: {},
|
|
49
|
+
to: { couldNotResolve: true },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "no-deep-imports",
|
|
53
|
+
severity: "error",
|
|
54
|
+
comment:
|
|
55
|
+
"The specifier reaches past the package name into a subpath its exports map does not publish. Depend on a published entry instead.",
|
|
56
|
+
from: {},
|
|
57
|
+
to: {
|
|
58
|
+
couldNotResolve: true,
|
|
59
|
+
path: "^(@[^/]+/[^/]+|[^@./#][^/]*)/",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
options: {
|
|
64
|
+
parser: "swc",
|
|
65
|
+
builtInModules: { add: ["bun"] },
|
|
66
|
+
doNotFollow: { path: ["node_modules"] },
|
|
67
|
+
enhancedResolveOptions: {
|
|
68
|
+
exportsFields: ["exports"],
|
|
69
|
+
conditionNames: ["types", "import", "require", "node", "default"],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// effect-channel/no-error-channel-escape.ts
|
|
2
|
+
var EFFECT_SOURCES = new Set(["effect", "effect/Effect"]);
|
|
3
|
+
var INSTEAD = {
|
|
4
|
+
drops: "it drops the failure and the success together, so no caller can tell one from the other. Handle the error by tag, or keep it as a value with Effect.result or Effect.exit",
|
|
5
|
+
swallows: "a Cause is the typed error plus defects plus interruption, so this swallows the bugs and the cancellations along with it. Catch the errors you named, with Effect.catchTag or Effect.catchTags",
|
|
6
|
+
blind: "the handler cannot see what it is recovering from, so every error in the channel collapses into one fallback. Name the errors with Effect.catchTag, or take the error and use it"
|
|
7
|
+
};
|
|
8
|
+
var REFUSED = new Map([
|
|
9
|
+
["ignore", INSTEAD.drops],
|
|
10
|
+
["ignoreCause", INSTEAD.drops],
|
|
11
|
+
["catchCause", INSTEAD.swallows],
|
|
12
|
+
["catchCauseIf", INSTEAD.swallows],
|
|
13
|
+
["catchCauseFilter", INSTEAD.swallows]
|
|
14
|
+
]);
|
|
15
|
+
var blindToTheError = (handler) => {
|
|
16
|
+
if (!handler)
|
|
17
|
+
return false;
|
|
18
|
+
if (handler.type !== "ArrowFunctionExpression" && handler.type !== "FunctionExpression")
|
|
19
|
+
return false;
|
|
20
|
+
return handler.params.every((param) => param.type === "Identifier" && /^_+$/.test(param.name));
|
|
21
|
+
};
|
|
22
|
+
var rule = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: "problem",
|
|
25
|
+
docs: { description: "Disallow the combinators that erase Effect's error channel" }
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
const effect = new Set;
|
|
29
|
+
const named = (node) => {
|
|
30
|
+
if (!node || node.type !== "MemberExpression" || node.computed)
|
|
31
|
+
return null;
|
|
32
|
+
if (node.object.type !== "Identifier" || !effect.has(node.object.name))
|
|
33
|
+
return null;
|
|
34
|
+
return node.property.type === "Identifier" ? node.property.name : null;
|
|
35
|
+
};
|
|
36
|
+
const refuse = (node, combinator, instead) => {
|
|
37
|
+
context.report({ node, message: `Effect.${combinator} erases the error channel: ${instead}` });
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
ImportDeclaration(node) {
|
|
41
|
+
if (!EFFECT_SOURCES.has(node.source.value))
|
|
42
|
+
return;
|
|
43
|
+
const module = node.source.value === "effect/Effect";
|
|
44
|
+
for (const specifier of node.specifiers) {
|
|
45
|
+
if (specifier.type === "ImportSpecifier") {
|
|
46
|
+
if (specifier.imported.type === "Identifier" && specifier.imported.name === "Effect")
|
|
47
|
+
effect.add(specifier.local.name);
|
|
48
|
+
} else if (module)
|
|
49
|
+
effect.add(specifier.local.name);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
MemberExpression(node) {
|
|
53
|
+
const combinator = named(node);
|
|
54
|
+
if (combinator === null)
|
|
55
|
+
return;
|
|
56
|
+
const instead = REFUSED.get(combinator);
|
|
57
|
+
if (instead !== undefined)
|
|
58
|
+
refuse(node, combinator, instead);
|
|
59
|
+
},
|
|
60
|
+
CallExpression(node) {
|
|
61
|
+
if (named(node.callee) !== "catch")
|
|
62
|
+
return;
|
|
63
|
+
if (!blindToTheError(node.arguments[node.arguments.length - 1]))
|
|
64
|
+
return;
|
|
65
|
+
refuse(node, "catch", INSTEAD.blind);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var no_error_channel_escape_default = rule;
|
|
71
|
+
|
|
72
|
+
// effect-channel/index.ts
|
|
73
|
+
var plugin = {
|
|
74
|
+
meta: { name: "effect-channel" },
|
|
75
|
+
rules: { "no-error-channel-escape": no_error_channel_escape_default }
|
|
76
|
+
};
|
|
77
|
+
var effect_channel_default = plugin;
|
|
78
|
+
export {
|
|
79
|
+
effect_channel_default as default
|
|
80
|
+
};
|
package/oxlintrc.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugins": ["typescript", "oxc", "eslint", "import"],
|
|
3
|
+
"jsPlugins": ["./dist/index.js"],
|
|
4
|
+
"categories": { "correctness": "error", "suspicious": "error" },
|
|
5
|
+
"rules": {
|
|
6
|
+
"effect-channel/no-error-channel-escape": "error",
|
|
7
|
+
"typescript/no-explicit-any": "error",
|
|
8
|
+
"typescript/ban-ts-comment": ["error", { "ts-expect-error": true }],
|
|
9
|
+
"typescript/no-inferrable-types": "error",
|
|
10
|
+
"typescript/explicit-module-boundary-types": "error",
|
|
11
|
+
"typescript/switch-exhaustiveness-check": "error",
|
|
12
|
+
"typescript/prefer-readonly": "error",
|
|
13
|
+
"typescript/no-unnecessary-condition": ["error", { "allowConstantLoopConditions": true }],
|
|
14
|
+
"typescript/no-unnecessary-type-parameters": "error",
|
|
15
|
+
"typescript/use-unknown-in-catch-callback-variable": "error",
|
|
16
|
+
"eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "ignoreRestSiblings": true }],
|
|
17
|
+
"typescript/no-unsafe-type-assertion": "off",
|
|
18
|
+
"typescript/no-non-null-assertion": "off",
|
|
19
|
+
"typescript/consistent-return": "off"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@avi2dg/checks",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Deterministic checks shared across the captain's TypeScript repos",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/avi2d/checks.git"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"files": [
|
|
15
|
+
"bunfig.toml",
|
|
16
|
+
"commitlint.config.js",
|
|
17
|
+
"dependency-cruiser.config.js",
|
|
18
|
+
"scripts/lint-coverage.sh",
|
|
19
|
+
"scripts/test-layout.ts",
|
|
20
|
+
"scripts/commit-identity.ts",
|
|
21
|
+
"scripts/comments.ts",
|
|
22
|
+
"scripts/comment-gate.ts",
|
|
23
|
+
"scripts/backtest.ts",
|
|
24
|
+
"oxlintrc.json",
|
|
25
|
+
"tsconfig.effect.json",
|
|
26
|
+
"dist/index.js"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
"./bunfig.toml": "./bunfig.toml",
|
|
30
|
+
"./commitlint.config.js": "./commitlint.config.js",
|
|
31
|
+
"./dependency-cruiser.config.js": "./dependency-cruiser.config.js",
|
|
32
|
+
"./scripts/lint-coverage.sh": "./scripts/lint-coverage.sh",
|
|
33
|
+
"./scripts/test-layout.ts": "./scripts/test-layout.ts",
|
|
34
|
+
"./scripts/commit-identity.ts": "./scripts/commit-identity.ts",
|
|
35
|
+
"./scripts/comments.ts": "./scripts/comments.ts",
|
|
36
|
+
"./scripts/comment-gate.ts": "./scripts/comment-gate.ts",
|
|
37
|
+
"./scripts/backtest.ts": "./scripts/backtest.ts",
|
|
38
|
+
"./oxlintrc.json": "./oxlintrc.json",
|
|
39
|
+
"./tsconfig.effect.json": "./tsconfig.effect.json",
|
|
40
|
+
"./dist/index.js": "./dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "bun build effect-channel/index.ts --outdir dist --target node --format esm",
|
|
44
|
+
"lint": "oxlint --type-aware && ./scripts/lint-coverage.sh && bun scripts/test-layout.ts && bun scripts/commit-identity.ts HEAD && bun scripts/comment-gate.ts HEAD && depcruise --config .dependency-cruiser.cjs effect-channel scripts tests commitlint.config.js dependency-cruiser.config.js .dependency-cruiser.cjs",
|
|
45
|
+
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
46
|
+
"test": "bun test --randomize"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@swc/core": "1.16.2",
|
|
50
|
+
"dependency-cruiser": "18.4.0",
|
|
51
|
+
"@effect/tsgo": "0.45.0",
|
|
52
|
+
"oxlint": "1.83.0",
|
|
53
|
+
"oxlint-tsgolint": "7.0.2002"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@effect/tsgo": "0.45.0",
|
|
57
|
+
"@oxlint/plugins": "1.83.0",
|
|
58
|
+
"@swc/core": "1.16.2",
|
|
59
|
+
"@types/bun": "^1.3.0",
|
|
60
|
+
"dependency-cruiser": "18.4.0",
|
|
61
|
+
"effect": "4.0.0-rc.115",
|
|
62
|
+
"oxlint": "1.83.0",
|
|
63
|
+
"oxlint-tsgolint": "7.0.2002",
|
|
64
|
+
"typescript": "7.0.2"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@commitlint/cli": "21.2.3",
|
|
68
|
+
"@commitlint/config-conventional": "21.2.3"
|
|
69
|
+
}
|
|
70
|
+
}
|