@konvert7/klint 0.37.1 → 1.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 +49 -166
- package/bin/klint.js +47 -0
- package/klint.schema.json +1 -1
- package/klint.schema.yaml +1 -1
- package/package.json +15 -25
- package/cli/command.ts +0 -189
- package/cli/commands.ts +0 -90
- package/cli/help.ts +0 -21
- package/cli/install-skill.ts +0 -260
- package/cli/output.ts +0 -178
- package/cli/paths.ts +0 -20
- package/cli/rust-config.ts +0 -145
- package/cli/rust-engine.ts +0 -323
- package/cli/stats.ts +0 -92
- package/cli.ts +0 -214
- package/core/arch-comments.ts +0 -125
- package/core/arch.ts +0 -470
- package/core/ast.ts +0 -63
- package/core/config.schema.ts +0 -413
- package/core/fixer.ts +0 -47
- package/core/native-binary.ts +0 -123
- package/core/paths.ts +0 -9
- package/core/rule-helpers.ts +0 -76
- package/core/runner.ts +0 -196
- package/core/schema-version.ts +0 -46
- package/core/skill.ts +0 -164
- package/core/stats.ts +0 -160
- package/core/types.ts +0 -146
- package/plugins/index.ts +0 -6
- package/plugins/sonar.ts +0 -27
- package/rules/index.ts +0 -26
- package/rules/no-async-predicate.ts +0 -51
- package/rules/no-consecutive-array-push.ts +0 -61
- package/rules/no-date-equality.ts +0 -35
- package/rules/no-floating-promise.ts +0 -37
- package/rules/no-misused-promises.ts +0 -59
- package/rules/no-nested-template-literals.ts +0 -47
- package/rules/no-object-in-template.ts +0 -95
- package/rules/no-optional-chain-on-non-nullable.ts +0 -47
- package/rules/no-single-char-class.ts +0 -113
- package/rules/no-string-match.ts +0 -63
- package/rules/no-sync-in-async.ts +0 -43
- package/rules/no-unguarded-json-parse.ts +0 -35
- package/rules/prefer-at.ts +0 -46
- package/rules/prefer-nullish-coalescing-assign.ts +0 -111
- package/rules/prefer-string-raw-regexp.ts +0 -93
- package/rules/prefer-string-raw.ts +0 -39
- package/rules/prefer-string-replaceall.ts +0 -67
- package/skill/klint-rules/SKILL.md +0 -264
- package/tools/generate-homebrew-formula.ts +0 -64
- package/tools/prepare-native-release.ts +0 -166
- package/tools/prepare-nuget-release.ts +0 -62
- package/tools/prepare-python-release.ts +0 -76
- package/tools/semantic-release-crates.js +0 -96
- package/tools/semantic-release-native-npm.js +0 -121
- package/tools/semantic-release-root-npm.js +0 -87
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: klint-rules
|
|
3
|
-
description: Add, modify, or explain klint architecture rules for this repo. Use when asked to enforce a new structural constraint, adjust rule scope, or explain why a violation fired.
|
|
4
|
-
argument-hint: <constraint to enforce>
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Schema
|
|
8
|
-
|
|
9
|
-
The full, authoritative config schema lives in the repo and is the source of truth for every field below:
|
|
10
|
-
|
|
11
|
-
- YAML: <https://github.com/konvert7/klint/blob/main/klint.schema.yaml>
|
|
12
|
-
- JSON: <https://github.com/konvert7/klint/blob/main/klint.schema.json>
|
|
13
|
-
|
|
14
|
-
Wire it into `klint.yaml` for editor autocomplete and validation:
|
|
15
|
-
|
|
16
|
-
```yaml
|
|
17
|
-
# yaml-language-server: $schema=./klint.schema.yaml
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Workflow
|
|
21
|
-
|
|
22
|
-
1. **Understand the constraint** — if scope is ambiguous, ask: which files/layers are involved? should it block or warn? are there legitimate exceptions?
|
|
23
|
-
|
|
24
|
-
2. **Grep first** — verify the pattern exists and count current occurrences before touching `klint.yaml`:
|
|
25
|
-
```sh
|
|
26
|
-
grep -rn "the-pattern" src/ --include="*.ts"
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
3. **Choose the right primitive:**
|
|
30
|
-
|
|
31
|
-
| The constraint is... | Primitive |
|
|
32
|
-
|---|---|
|
|
33
|
-
| Layer X must not import from layer Y | `arch.imports` + `deny` |
|
|
34
|
-
| Layer X may only import from Y and Z | `arch.imports` + `allow` |
|
|
35
|
-
| Layer X must not import an npm package or `node:` builtin | `arch.imports` + `deny-packages` |
|
|
36
|
-
| Pattern P must only appear in one designated file | `arch.singleton` + `pattern` |
|
|
37
|
-
| Pattern P must never appear inside a scoped layer | `arch.forbidden` + `pattern` |
|
|
38
|
-
| Raw HTML/JSX element must never appear in a scoped layer | `arch.forbidden` + `jsx-element` |
|
|
39
|
-
| Raw HTML/JSX element may only appear in one primitive file | `arch.singleton` + `jsx-element` |
|
|
40
|
-
| A value-shaped pattern (regex) must never appear in a scoped layer | `arch.forbidden` + `pattern: "re:…"` |
|
|
41
|
-
| No file in a layer may exceed N lines | `arch.maxLines` + `limit` |
|
|
42
|
-
| No file may be more than N% comments | `arch.maxCommentDensity` + `limit` |
|
|
43
|
-
| No comment block may stack more than N lines | `arch.maxCommentBlock` + `limit` |
|
|
44
|
-
|
|
45
|
-
4. **Read `klint.yaml`** — check existing `arch.layers` and rules before adding anything. Add a new named layer to `arch.layers` if the file group doesn't exist yet.
|
|
46
|
-
|
|
47
|
-
5. **Write the stanza** in `klint.yaml` under the correct `arch:` section.
|
|
48
|
-
|
|
49
|
-
6. **Verify zero new violations on the current codebase:**
|
|
50
|
-
```sh
|
|
51
|
-
bun klint/cli.ts # in-repo
|
|
52
|
-
npx klint # after npm install
|
|
53
|
-
```
|
|
54
|
-
If new violations appear on existing code, fix them first or adjust the rule scope — then land the rule.
|
|
55
|
-
|
|
56
|
-
7. **Write a break test** — prove the rule fires on a deliberate violation, and doesn't fire on clean code. Run the test suite to confirm.
|
|
57
|
-
|
|
58
|
-
## Run tracking
|
|
59
|
-
|
|
60
|
-
Every run is recorded in `.klint/stats.json` next to the config. The folder ignores itself, so never add it to `.gitignore`.
|
|
61
|
-
|
|
62
|
-
| Command or flag | Use |
|
|
63
|
-
|---|---|
|
|
64
|
-
| `klint stats [--config <dir>]` | Print runs, violations reported and errors prevented, in total and per rule. Use it to answer "how much has klint caught?" |
|
|
65
|
-
| `--hook` | Put it only in agent hook commands, e.g. `klint --json --hook`. Leave it off when you run klint yourself, so your runs count as direct. |
|
|
66
|
-
| `KLINT_NO_STATS=1` | Turn recording off for a run, e.g. in a throwaway fixture. |
|
|
67
|
-
|
|
68
|
-
Every violation a run reports counts as reported, repeats included; klint's own `klint/*` advisories do not. An error counts as prevented once, when a later run no longer reports it. Errors match by rule, file and message, so moving code to another line is not a fix. Warnings are not tracked.
|
|
69
|
-
|
|
70
|
-
## Primitive reference
|
|
71
|
-
|
|
72
|
-
### Layers
|
|
73
|
-
```yaml
|
|
74
|
-
arch:
|
|
75
|
-
layers:
|
|
76
|
-
core: ["src/lib/**", "src/hooks/**"]
|
|
77
|
-
ui: ["src/components/**"]
|
|
78
|
-
targets: ["src/targets/**"]
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### Import boundaries
|
|
82
|
-
```yaml
|
|
83
|
-
arch:
|
|
84
|
-
imports:
|
|
85
|
-
# deny: block imports from one layer into another
|
|
86
|
-
- from: core
|
|
87
|
-
deny: targets
|
|
88
|
-
message: "Core must not depend on agent-specific code"
|
|
89
|
-
severity: error # optional — default is error; use warn to record without blocking
|
|
90
|
-
|
|
91
|
-
# allow: whitelist — anything not listed is denied (npm + node: builtins always pass)
|
|
92
|
-
- from: ["src/dao/**"]
|
|
93
|
-
allow: ["src/dao/**", "src/prisma/**"]
|
|
94
|
-
message: "DAO may only import from dao or prisma"
|
|
95
|
-
|
|
96
|
-
# type-only: allow — import type {} and Python if TYPE_CHECKING: blocks stay permitted
|
|
97
|
-
- from: core
|
|
98
|
-
deny: targets
|
|
99
|
-
type-only: allow
|
|
100
|
-
message: "Core must not depend on agent-specific code"
|
|
101
|
-
|
|
102
|
-
# deny-packages: block npm packages and node: builtins, which deny/allow cannot reach
|
|
103
|
-
# because they never resolve to a project file. Composes with deny in the same rule.
|
|
104
|
-
- from: ["src/dao/**"]
|
|
105
|
-
deny-packages: ["next/headers", "node:fs"]
|
|
106
|
-
message: "Data access must not read request state or touch the filesystem"
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
`deny-packages` matches per segment: `next` covers `next/headers`, `nextra` is unaffected, and
|
|
110
|
-
`next/headers` does not match `next/navigation`. Python uses dotted segments, so `os` covers
|
|
111
|
-
`os.path` and blocks pip packages and stdlib modules alike. Swift uses dotted segments too, at
|
|
112
|
-
module granularity, which is what makes system frameworks like `UIKit` reachable. Rust splits on
|
|
113
|
-
`::`, so `tokio` covers `tokio::sync::Mutex` while `tokio_util` is unaffected. The specifier is read off the AST, so
|
|
114
|
-
static imports, dynamic `import()`, and `export … from` re-exports all count while a comment
|
|
115
|
-
mentioning the package does not — unlike `forbidden` + `pattern`, which is a line-based text scan.
|
|
116
|
-
|
|
117
|
-
### Singleton — one designated location
|
|
118
|
-
|
|
119
|
-
Pin a `pattern` (literal string) **or** a `jsx-element` (AST-matched tag) to exactly one file. Every other in-scope occurrence is a violation.
|
|
120
|
-
|
|
121
|
-
```yaml
|
|
122
|
-
arch:
|
|
123
|
-
singleton:
|
|
124
|
-
# pattern: literal string match — fires if it appears anywhere except `only`
|
|
125
|
-
- pattern: "process.env.API_KEY"
|
|
126
|
-
only: "src/lib/auth.ts"
|
|
127
|
-
in: ["src/**"] # optional: limit scan scope (default: all files). string OR array
|
|
128
|
-
message: "Use the auth module instead of reading API_KEY directly"
|
|
129
|
-
severity: error # optional — default error; use warn to record without blocking
|
|
130
|
-
|
|
131
|
-
# jsx-element: pin a raw element to its design-system primitive
|
|
132
|
-
- jsx-element: "button" # one tag, or a list: ["button", "input"]
|
|
133
|
-
only: "src/components/ui/button.tsx"
|
|
134
|
-
in: ["src/**/*.tsx"]
|
|
135
|
-
message: "Raw <button> belongs only to the Button primitive"
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Required: `only` + `message`, plus one of `pattern` / `jsx-element` (not both in the same stanza).
|
|
139
|
-
|
|
140
|
-
### Forbidden — banned pattern in scope
|
|
141
|
-
|
|
142
|
-
Block a `pattern` (literal string) **or** a `jsx-element` (AST-matched tag) inside a scoped layer.
|
|
143
|
-
|
|
144
|
-
```yaml
|
|
145
|
-
arch:
|
|
146
|
-
forbidden:
|
|
147
|
-
# pattern: literal substring by default; prefix with `re:` for a regex
|
|
148
|
-
- pattern: "console.log("
|
|
149
|
-
in: ["src/lib/**"] # string OR array; supports ! negation
|
|
150
|
-
message: "Use the logger — console.log leaks into the agent event stream"
|
|
151
|
-
severity: error # optional — default error; use warn to record without blocking
|
|
152
|
-
|
|
153
|
-
# re: prefix — match a regular expression (common JS/RE2 subset; no lookaround/backrefs)
|
|
154
|
-
- pattern: 're:\b(?:p|gap)-\['
|
|
155
|
-
in: ["src/**/*.tsx"]
|
|
156
|
-
message: "Use the spacing scale, not arbitrary bracket values like p-[18px]"
|
|
157
|
-
|
|
158
|
-
# jsx-element: forbid raw HTML elements outside the design system
|
|
159
|
-
- jsx-element: ["button", "input", "label"]
|
|
160
|
-
in: ["src/app/**/*.tsx", "src/components/**/*.tsx", "!src/components/ui/**"]
|
|
161
|
-
message: "Use the design-system primitives in @/components/ui/* instead of raw HTML elements"
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Required: `in` + `message`, plus one of `pattern` / `jsx-element` (not both in the same stanza). A `pattern` may be a literal substring or, with the `re:` prefix, a regex; `singleton` patterns accept `re:` too.
|
|
165
|
-
|
|
166
|
-
`jsx-element` matches intrinsic element names on the AST (opening and self-closing tags), so it is robust to whitespace, attributes, and naming collisions like `<buttonGroup>` — unlike a literal `pattern: "<button"` scan. It works on `.tsx`/`.jsx` files in both the TS and Rust engines.
|
|
167
|
-
|
|
168
|
-
### Max lines — cap file length
|
|
169
|
-
|
|
170
|
-
Limit how many physical lines a file in scope may have. Use a separate stanza per scope to give different ceilings to source and tests.
|
|
171
|
-
|
|
172
|
-
```yaml
|
|
173
|
-
arch:
|
|
174
|
-
maxLines:
|
|
175
|
-
- limit: 300 # positive integer; required
|
|
176
|
-
in: ["src/**"] # string OR array; supports ! negation
|
|
177
|
-
message: "Split this module" # optional — defaults to "File exceeds the maximum of N lines"
|
|
178
|
-
severity: error # optional — default error; use warn to record without blocking
|
|
179
|
-
|
|
180
|
-
- limit: 600 # tests may run longer
|
|
181
|
-
in: ["tests/**"]
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
Required: `limit` + `in`. The count is **total physical lines** — blanks and comments included, not code lines — and a file over the limit is flagged at line `limit + 1`. Enforced identically in the TS and Rust engines, across every language klint scans.
|
|
185
|
-
|
|
186
|
-
### Comment budget — cap explanatory prose
|
|
187
|
-
|
|
188
|
-
Two ceilings on how much of a file may be comments. Use them to push intent into names and small
|
|
189
|
-
functions rather than paragraphs that drift from the code they describe.
|
|
190
|
-
|
|
191
|
-
```yaml
|
|
192
|
-
arch:
|
|
193
|
-
# maxCommentDensity: what share of a file may be comment lines
|
|
194
|
-
maxCommentDensity:
|
|
195
|
-
- limit: 5 # percent; required
|
|
196
|
-
in: ["src/**"] # string OR array; supports ! negation
|
|
197
|
-
countDocComments: false # optional — default false, so JSDoc/docstrings are exempt
|
|
198
|
-
message: "Encode intent in names and small functions, not prose"
|
|
199
|
-
severity: error # optional — default error; use warn to record without blocking
|
|
200
|
-
|
|
201
|
-
# maxCommentBlock: how tall a single stack of comment lines may get
|
|
202
|
-
maxCommentBlock:
|
|
203
|
-
- limit: 2 # consecutive comment lines; required
|
|
204
|
-
in: ["src/**"]
|
|
205
|
-
ignore: ["@codegen:"] # optional — structural markers, not prose
|
|
206
|
-
message: "Extract a well-named function instead of stacking comment lines"
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Required: `limit` + `in`. Density is a **percentage of total physical lines** (code + comments +
|
|
210
|
-
blanks — the same denominator `maxLines` uses). Block height counts **consecutive** comment lines and
|
|
211
|
-
is reported at the first offending line. Both exempt doc-comments by default — `/** … */` JSDoc and
|
|
212
|
-
Python docstrings — so documentation is not penalised; ordinary `//`, `/* */`, and `#` comments always
|
|
213
|
-
count. Set `countDocComments: true` to include them.
|
|
214
|
-
|
|
215
|
-
The two are complementary: density catches a file that is 30% commented in scattered one-liners,
|
|
216
|
-
block catches a ten-line essay above one function in an otherwise sparse file. Neither alone catches both.
|
|
217
|
-
|
|
218
|
-
**`ignore` — comments that are machinery, not prose.** Some comments are load-bearing input to a
|
|
219
|
-
build or codegen step (`// @codegen:region-start`, feature-strip markers, pragmas). They are comments to the
|
|
220
|
-
lexer but structure to the project, and counting them makes both limits measure the wrong thing:
|
|
221
|
-
|
|
222
|
-
```yaml
|
|
223
|
-
arch:
|
|
224
|
-
maxCommentDensity:
|
|
225
|
-
- limit: 5
|
|
226
|
-
in: ["src/**"]
|
|
227
|
-
ignore: ["@codegen:"] # literal substring, or "re:" for a regex
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
Two rules govern how an ignored line behaves, and both matter:
|
|
231
|
-
|
|
232
|
-
- **It stays in the density denominator.** A marker is a physical line like code, so a 20-line file
|
|
233
|
-
with 4 markers and 1 real comment measures 1/20, not 1/16.
|
|
234
|
-
- **It does not break a comment block.** `// a` / `// @marker` / `// b` / `// c` is a run of three
|
|
235
|
-
counted lines, not two runs. Otherwise sprinkling a marker every other line would defeat the cap.
|
|
236
|
-
|
|
237
|
-
`ignore` tests the **physical source line**, so it only ever applies to lines already identified as
|
|
238
|
-
comments — code containing the same text is untouched. Enforced identically in both engines.
|
|
239
|
-
|
|
240
|
-
## Pitfalls
|
|
241
|
-
|
|
242
|
-
- `pattern` in `singleton` and `forbidden` is a **literal substring** by default — it will not catch `process.env["KEY"]` (bracket notation). Either grep for both forms, or use a `re:` regex prefix to match them in one rule. Caveat: a literal pattern that itself begins with `re:` cannot be expressed (it is always read as a regex), and regexes must stay in the common JS/RE2 subset (no lookaround or backreferences) so both engines agree.
|
|
243
|
-
- `maxLines.limit` counts **total physical lines** (blanks and comments included), not code lines; a trailing newline does not add a line, and both engines count identically.
|
|
244
|
-
- `maxCommentDensity.limit` is a **percentage, not a line count** — `limit: 5` means 5% of the file, so a 40-line file is over budget at its third comment line. Measure before picking a number: land it as `severity: warn` first, read the reported densities, then tighten to `error`.
|
|
245
|
-
- `jsx-element` matches **intrinsic** (lowercase HTML) tags only — `button`, `input`, `label`. It does not match custom React components like `<Button>`; to restrict those, use `arch.imports` against the component's module instead.
|
|
246
|
-
- A `forbidden`/`singleton` stanza takes either `pattern` or `jsx-element`, never both — split into two stanzas if you need both.
|
|
247
|
-
- `only` in `singleton` is relative to the project root — use forward slashes on all platforms.
|
|
248
|
-
- `severity: warn` records the violation but does not block the session. Use `error` (the default) to block.
|
|
249
|
-
- Path aliases (`@/*`) resolve via `tsconfig.json` `compilerOptions.paths`, including `extends` chains.
|
|
250
|
-
- Adding a rule that fires on existing code is not a blocker — fix the violations first, then add the rule.
|
|
251
|
-
|
|
252
|
-
## Output format
|
|
253
|
-
|
|
254
|
-
After adding or modifying a rule, report:
|
|
255
|
-
- The YAML stanza added
|
|
256
|
-
- The grep evidence that confirmed the pattern is real
|
|
257
|
-
- The test written and its result
|
|
258
|
-
- The output of `bun run klint` (or `npx klint`) confirming zero new violations
|
|
259
|
-
|
|
260
|
-
## Do NOT use
|
|
261
|
-
|
|
262
|
-
- To fix TypeScript type errors, lint issues, or Biome violations — those are separate tools
|
|
263
|
-
- To enforce naming conventions (hooks must start with `use`, components must be PascalCase) — use ESLint with the appropriate plugin
|
|
264
|
-
- To write custom TypeScript AST rules in `klint.rules.ts` — that is a separate, code-level workflow
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
interface FormulaOptions {
|
|
5
|
-
version: string;
|
|
6
|
-
darwinArm64Sha: string;
|
|
7
|
-
darwinX64Sha: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function homebrewFormula(options: FormulaOptions): string {
|
|
11
|
-
const version = options.version.replace(/^v/, "");
|
|
12
|
-
|
|
13
|
-
return `# typed: false
|
|
14
|
-
# frozen_string_literal: true
|
|
15
|
-
|
|
16
|
-
class Klint < Formula
|
|
17
|
-
desc "Architecture-as-Code linter for TypeScript, Python, and Swift projects"
|
|
18
|
-
homepage "https://github.com/konvert7/klint"
|
|
19
|
-
version "${version}"
|
|
20
|
-
license "MIT"
|
|
21
|
-
|
|
22
|
-
if Hardware::CPU.arm?
|
|
23
|
-
url "https://github.com/konvert7/klint/releases/download/v${version}/klint-${version}-darwin-arm64.tar.gz"
|
|
24
|
-
sha256 "${options.darwinArm64Sha}"
|
|
25
|
-
else
|
|
26
|
-
url "https://github.com/konvert7/klint/releases/download/v${version}/klint-${version}-darwin-x64.tar.gz"
|
|
27
|
-
sha256 "${options.darwinX64Sha}"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def install
|
|
31
|
-
binary = File.exist?("klint") ? "klint" : Dir["klint-*/klint"].first
|
|
32
|
-
odie "klint binary not found" unless binary
|
|
33
|
-
bin.install binary => "klint"
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
test do
|
|
37
|
-
assert_match "klint-rs", shell_output("#{bin}/klint --version")
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (import.meta.main) {
|
|
44
|
-
const args = process.argv.slice(2);
|
|
45
|
-
const output = requireValue(args, "--output");
|
|
46
|
-
const formula = homebrewFormula({
|
|
47
|
-
version: requireValue(args, "--version"),
|
|
48
|
-
darwinArm64Sha: requireValue(args, "--darwin-arm64-sha"),
|
|
49
|
-
darwinX64Sha: requireValue(args, "--darwin-x64-sha"),
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const path = resolve(output);
|
|
53
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
54
|
-
writeFileSync(path, formula);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function requireValue(args: string[], name: string): string {
|
|
58
|
-
const index = args.indexOf(name);
|
|
59
|
-
const value = index >= 0 ? args[index + 1] : undefined;
|
|
60
|
-
if (!value) {
|
|
61
|
-
throw new Error(`${name} is required`);
|
|
62
|
-
}
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
copyFileSync,
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
mkdtempSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
rmSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
} from "node:fs";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { dirname, join, resolve } from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { nativePackages } from "../core/native-binary";
|
|
14
|
-
|
|
15
|
-
interface PackageJson {
|
|
16
|
-
name: string;
|
|
17
|
-
version: string;
|
|
18
|
-
private?: boolean;
|
|
19
|
-
optionalDependencies?: Record<string, string>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface Options {
|
|
23
|
-
version: string;
|
|
24
|
-
outDir: string;
|
|
25
|
-
requireBinaries: boolean;
|
|
26
|
-
keep: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
30
|
-
const options = parseArgs();
|
|
31
|
-
|
|
32
|
-
if (existsSync(options.outDir)) {
|
|
33
|
-
rmSync(options.outDir, { recursive: true, force: true });
|
|
34
|
-
}
|
|
35
|
-
mkdirSync(options.outDir, { recursive: true });
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const releaseRootPackageJson = prepareRootPackageJson();
|
|
39
|
-
const nativeResults = nativePackages().map(prepareNativePackage);
|
|
40
|
-
|
|
41
|
-
validateRootPackageJson(releaseRootPackageJson);
|
|
42
|
-
for (const result of nativeResults) validateNativePackage(result.packageJson);
|
|
43
|
-
|
|
44
|
-
const missing = nativeResults
|
|
45
|
-
.filter((result) => !result.binaryCopied)
|
|
46
|
-
.map((result) => result.packageJson.name);
|
|
47
|
-
|
|
48
|
-
if (options.requireBinaries && missing.length > 0) {
|
|
49
|
-
fail(`Missing native release binaries:\n${missing.join("\n")}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const missingMessage =
|
|
53
|
-
missing.length > 0
|
|
54
|
-
? `missing binaries: ${missing.join(", ")}`
|
|
55
|
-
: "missing binaries: none";
|
|
56
|
-
const outputLines = [
|
|
57
|
-
`native release dry-run ok (${options.version})`,
|
|
58
|
-
`output: ${options.outDir}`,
|
|
59
|
-
`native packages: ${nativeResults.length}`,
|
|
60
|
-
missingMessage,
|
|
61
|
-
];
|
|
62
|
-
process.stdout.write(`${outputLines.join("\n")}\n`);
|
|
63
|
-
} finally {
|
|
64
|
-
if (!options.keep) {
|
|
65
|
-
rmSync(options.outDir, { recursive: true, force: true });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function prepareRootPackageJson(): PackageJson {
|
|
70
|
-
const packageJson = readPackageJson(join(root, "package.json"));
|
|
71
|
-
packageJson.version = options.version;
|
|
72
|
-
delete packageJson.optionalDependencies;
|
|
73
|
-
|
|
74
|
-
const outPath = join(options.outDir, "package.json");
|
|
75
|
-
writeJson(outPath, packageJson);
|
|
76
|
-
return packageJson;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function prepareNativePackage(nativePackage: ReturnType<typeof nativePackages>[number]): {
|
|
80
|
-
binaryCopied: boolean;
|
|
81
|
-
packageJson: PackageJson;
|
|
82
|
-
} {
|
|
83
|
-
const packageDirName = `${nativePackage.platform}-${nativePackage.arch}`;
|
|
84
|
-
const sourceDir = join(root, "npm", "native", packageDirName);
|
|
85
|
-
const outDir = join(options.outDir, "npm", "native", packageDirName);
|
|
86
|
-
mkdirSync(outDir, { recursive: true });
|
|
87
|
-
|
|
88
|
-
const packageJson = readPackageJson(join(sourceDir, "package.json"));
|
|
89
|
-
packageJson.version = options.version;
|
|
90
|
-
packageJson.private = false;
|
|
91
|
-
writeJson(join(outDir, "package.json"), packageJson);
|
|
92
|
-
|
|
93
|
-
const sourceBinary = join(sourceDir, nativePackage.binaryPath);
|
|
94
|
-
const outBinary = join(outDir, nativePackage.binaryPath);
|
|
95
|
-
const binaryCopied = existsSync(sourceBinary);
|
|
96
|
-
if (binaryCopied) {
|
|
97
|
-
mkdirSync(dirname(outBinary), { recursive: true });
|
|
98
|
-
copyFileSync(sourceBinary, outBinary);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return { binaryCopied, packageJson };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function validateRootPackageJson(packageJson: PackageJson): void {
|
|
105
|
-
const optionalDependencies = packageJson.optionalDependencies ?? {};
|
|
106
|
-
for (const nativePackage of nativePackages()) {
|
|
107
|
-
if (nativePackage.packageName in optionalDependencies) {
|
|
108
|
-
fail(
|
|
109
|
-
`Root package must not consume dark native package ${nativePackage.packageName}`
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function validateNativePackage(packageJson: PackageJson): void {
|
|
116
|
-
if (packageJson.version !== options.version) {
|
|
117
|
-
fail(
|
|
118
|
-
`${packageJson.name} version is ${packageJson.version}, expected ${options.version}`
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
if (packageJson.private !== false) {
|
|
122
|
-
fail(`${packageJson.name} private must be false in release prep output`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function readPackageJson(path: string): PackageJson {
|
|
127
|
-
try {
|
|
128
|
-
return JSON.parse(readFileSync(path, "utf-8")) as PackageJson;
|
|
129
|
-
} catch (error) {
|
|
130
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
-
fail(`Failed to read package JSON at ${path}: ${message}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function writeJson(path: string, value: unknown): void {
|
|
136
|
-
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function parseArgs(): Options {
|
|
140
|
-
const args = process.argv.slice(2);
|
|
141
|
-
const version = valueAfter(args, "--version");
|
|
142
|
-
if (!version) {
|
|
143
|
-
fail(
|
|
144
|
-
"Usage: bun tools/prepare-native-release.ts --version <version> [--out <dir>] [--require-binaries] [--keep]"
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
version,
|
|
150
|
-
outDir: resolve(
|
|
151
|
-
valueAfter(args, "--out") ?? mkdtempSync(join(tmpdir(), "klint-native-release-"))
|
|
152
|
-
),
|
|
153
|
-
requireBinaries: args.includes("--require-binaries"),
|
|
154
|
-
keep: args.includes("--keep"),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function valueAfter(args: string[], name: string): string | undefined {
|
|
159
|
-
const index = args.indexOf(name);
|
|
160
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function fail(message: string): never {
|
|
164
|
-
process.stderr.write(`${message}\n`);
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
interface Options {
|
|
6
|
-
version: string;
|
|
7
|
-
root: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
11
|
-
const options = parseArgs();
|
|
12
|
-
const csprojPath = join(options.root, "nuget", "Klint.Tool.csproj");
|
|
13
|
-
|
|
14
|
-
const csproj = readFileSync(csprojPath, "utf-8");
|
|
15
|
-
const nextCsproj = replaceRequired(
|
|
16
|
-
csproj,
|
|
17
|
-
/<Version>.+<\/Version>/,
|
|
18
|
-
`<Version>${options.version}</Version>`,
|
|
19
|
-
csprojPath
|
|
20
|
-
);
|
|
21
|
-
writeFileSync(csprojPath, nextCsproj);
|
|
22
|
-
|
|
23
|
-
process.stdout.write(
|
|
24
|
-
`prepared NuGet release metadata: Konvert7.Klint@${options.version}\n`
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
function parseArgs(): Options {
|
|
28
|
-
const args = process.argv.slice(2);
|
|
29
|
-
const version = valueAfter(args, "--version");
|
|
30
|
-
if (!version) {
|
|
31
|
-
fail(
|
|
32
|
-
"Usage: bun tools/prepare-nuget-release.ts --version <version> [--root <repo-root>]"
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
version,
|
|
38
|
-
root: resolve(valueAfter(args, "--root") ?? repoRoot),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function replaceRequired(
|
|
43
|
-
input: string,
|
|
44
|
-
pattern: RegExp,
|
|
45
|
-
replacement: string,
|
|
46
|
-
path: string
|
|
47
|
-
): string {
|
|
48
|
-
if (!pattern.test(input)) {
|
|
49
|
-
fail(`Expected release metadata pattern ${pattern} in ${path}`);
|
|
50
|
-
}
|
|
51
|
-
return input.replace(pattern, replacement);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function valueAfter(args: string[], name: string): string | undefined {
|
|
55
|
-
const index = args.indexOf(name);
|
|
56
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function fail(message: string): never {
|
|
60
|
-
process.stderr.write(`${message}\n`);
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
interface Options {
|
|
6
|
-
name: string;
|
|
7
|
-
root: string;
|
|
8
|
-
version: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
12
|
-
const options = parseArgs();
|
|
13
|
-
const pythonRoot = join(options.root, "python");
|
|
14
|
-
const pyprojectPath = join(pythonRoot, "pyproject.toml");
|
|
15
|
-
const initPath = join(pythonRoot, "klint", "__init__.py");
|
|
16
|
-
|
|
17
|
-
const pyproject = readFileSync(pyprojectPath, "utf-8");
|
|
18
|
-
const nextPyproject = replaceRequired(
|
|
19
|
-
replaceRequired(pyproject, /^name = ".+"$/m, `name = "${options.name}"`, pyprojectPath),
|
|
20
|
-
/^version = ".+"$/m,
|
|
21
|
-
`version = "${options.version}"`,
|
|
22
|
-
pyprojectPath
|
|
23
|
-
);
|
|
24
|
-
writeFileSync(pyprojectPath, nextPyproject);
|
|
25
|
-
|
|
26
|
-
const init = readFileSync(initPath, "utf-8");
|
|
27
|
-
const nextInit = replaceRequired(
|
|
28
|
-
init,
|
|
29
|
-
/^__version__ = ".+"$/m,
|
|
30
|
-
`__version__ = "${options.version}"`,
|
|
31
|
-
initPath
|
|
32
|
-
);
|
|
33
|
-
writeFileSync(initPath, nextInit);
|
|
34
|
-
|
|
35
|
-
process.stdout.write(
|
|
36
|
-
`prepared Python release metadata: ${options.name}@${options.version}\n`
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
function parseArgs(): Options {
|
|
40
|
-
const args = process.argv.slice(2);
|
|
41
|
-
const name = valueAfter(args, "--name");
|
|
42
|
-
const version = valueAfter(args, "--version");
|
|
43
|
-
if (!name || !version) {
|
|
44
|
-
fail(
|
|
45
|
-
"Usage: bun tools/prepare-python-release.ts --name <package-name> --version <version> [--root <repo-root>]"
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
name,
|
|
51
|
-
version,
|
|
52
|
-
root: resolve(valueAfter(args, "--root") ?? repoRoot),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function replaceRequired(
|
|
57
|
-
input: string,
|
|
58
|
-
pattern: RegExp,
|
|
59
|
-
replacement: string,
|
|
60
|
-
path: string
|
|
61
|
-
): string {
|
|
62
|
-
if (!pattern.test(input)) {
|
|
63
|
-
fail(`Expected release metadata pattern ${pattern} in ${path}`);
|
|
64
|
-
}
|
|
65
|
-
return input.replace(pattern, replacement);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function valueAfter(args: string[], name: string): string | undefined {
|
|
69
|
-
const index = args.indexOf(name);
|
|
70
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function fail(message: string): never {
|
|
74
|
-
process.stderr.write(`${message}\n`);
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|