@makinzm/mille 0.0.6 → 0.0.7
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 +394 -0
- package/index.js +51 -1
- package/mille.wasm +0 -0
- package/package.json +8 -1
package/README.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# mille
|
|
2
|
+
|
|
3
|
+
> Architecture Checker — static analysis CLI for layered architecture rules
|
|
4
|
+
|
|
5
|
+
`mille` is a CLI tool that enforces **dependency rules for layered architectures** (Clean Architecture, Onion Architecture, Hexagonal Architecture, etc.).
|
|
6
|
+
|
|
7
|
+
It is implemented in Rust, supports multiple languages from a single TOML config, and is designed to run in CI/CD pipelines.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
| Feature | Status |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Internal layer dependency check (`dependency_mode`) | ✅ |
|
|
14
|
+
| External library dependency check (`external_mode`) | ✅ |
|
|
15
|
+
| DI entrypoint method call check (`allow_call_patterns`) | ✅ |
|
|
16
|
+
| Rust support | ✅ |
|
|
17
|
+
| Go support | ✅ |
|
|
18
|
+
| Python support | ✅ |
|
|
19
|
+
| TypeScript / JavaScript support | ✅ |
|
|
20
|
+
|
|
21
|
+
## How to Install
|
|
22
|
+
|
|
23
|
+
### cargo (Rust users)
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
cargo install mille
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### pip / uv (Python users)
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
# uv (recommended)
|
|
33
|
+
uv add --dev mille
|
|
34
|
+
uv run mille check
|
|
35
|
+
|
|
36
|
+
# pip
|
|
37
|
+
pip install mille
|
|
38
|
+
mille check
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The Python package is a native extension built with [maturin](https://github.com/PyO3/maturin) (PyO3). It provides both a CLI (`mille check`) and a Python API (`import mille; mille.check(...)`).
|
|
42
|
+
|
|
43
|
+
### npm (Node.js users)
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm install -g @makinzm/mille
|
|
47
|
+
mille check
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or use it without installing globally:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npx @makinzm/mille check
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Requires Node.js ≥ 18. The npm package bundles `mille.wasm` (the compiled Rust core) and runs it via Node.js's built-in `node:wasi` module — no native compilation or network access required at install time.
|
|
57
|
+
|
|
58
|
+
### go install
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
go install github.com/makinzm/mille/packages/go@latest
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The Go wrapper embeds `mille.wasm` (the compiled Rust core) and runs it via [wazero](https://wazero.io/) — a zero-dependency WebAssembly runtime. No network access or caching required; the binary is fully self-contained.
|
|
65
|
+
|
|
66
|
+
### Direct binary download
|
|
67
|
+
|
|
68
|
+
Pre-built binaries for each platform are available on [GitHub Releases](https://github.com/makinzm/mille/releases):
|
|
69
|
+
|
|
70
|
+
| Platform | Archive |
|
|
71
|
+
|---|---|
|
|
72
|
+
| Linux x86_64 | `mille-<version>-x86_64-unknown-linux-gnu.tar.gz` |
|
|
73
|
+
| Linux arm64 | `mille-<version>-aarch64-unknown-linux-gnu.tar.gz` |
|
|
74
|
+
| macOS x86_64 | `mille-<version>-x86_64-apple-darwin.tar.gz` |
|
|
75
|
+
| macOS arm64 | `mille-<version>-aarch64-apple-darwin.tar.gz` |
|
|
76
|
+
| Windows x86_64 | `mille-<version>-x86_64-pc-windows-msvc.zip` |
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
# Example: Linux x86_64
|
|
80
|
+
curl -L https://github.com/makinzm/mille/releases/latest/download/mille-<version>-x86_64-unknown-linux-gnu.tar.gz | tar xz
|
|
81
|
+
./mille check
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
85
|
+
|
|
86
|
+
### 1. Create `mille.toml`
|
|
87
|
+
|
|
88
|
+
Place `mille.toml` in your project root:
|
|
89
|
+
|
|
90
|
+
**Rust project example:**
|
|
91
|
+
|
|
92
|
+
```toml
|
|
93
|
+
[project]
|
|
94
|
+
name = "my-app"
|
|
95
|
+
root = "."
|
|
96
|
+
languages = ["rust"]
|
|
97
|
+
|
|
98
|
+
[[layers]]
|
|
99
|
+
name = "domain"
|
|
100
|
+
paths = ["src/domain/**"]
|
|
101
|
+
dependency_mode = "opt-in"
|
|
102
|
+
allow = []
|
|
103
|
+
external_mode = "opt-in"
|
|
104
|
+
external_allow = []
|
|
105
|
+
|
|
106
|
+
[[layers]]
|
|
107
|
+
name = "usecase"
|
|
108
|
+
paths = ["src/usecase/**"]
|
|
109
|
+
dependency_mode = "opt-in"
|
|
110
|
+
allow = ["domain"]
|
|
111
|
+
external_mode = "opt-in"
|
|
112
|
+
external_allow = []
|
|
113
|
+
|
|
114
|
+
[[layers]]
|
|
115
|
+
name = "infrastructure"
|
|
116
|
+
paths = ["src/infrastructure/**"]
|
|
117
|
+
dependency_mode = "opt-out"
|
|
118
|
+
deny = []
|
|
119
|
+
external_mode = "opt-out"
|
|
120
|
+
external_deny = []
|
|
121
|
+
|
|
122
|
+
[[layers]]
|
|
123
|
+
name = "main"
|
|
124
|
+
paths = ["src/main.rs"]
|
|
125
|
+
dependency_mode = "opt-in"
|
|
126
|
+
allow = ["domain", "infrastructure", "usecase"]
|
|
127
|
+
external_mode = "opt-in"
|
|
128
|
+
external_allow = ["clap"]
|
|
129
|
+
|
|
130
|
+
[[layers.allow_call_patterns]]
|
|
131
|
+
callee_layer = "infrastructure"
|
|
132
|
+
allow_methods = ["new", "build", "create", "init", "setup"]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Python project example:**
|
|
136
|
+
|
|
137
|
+
```toml
|
|
138
|
+
[project]
|
|
139
|
+
name = "my-python-app"
|
|
140
|
+
root = "."
|
|
141
|
+
languages = ["python"]
|
|
142
|
+
|
|
143
|
+
[resolve.python]
|
|
144
|
+
src_root = "."
|
|
145
|
+
package_names = ["domain", "usecase", "infrastructure"]
|
|
146
|
+
|
|
147
|
+
[[layers]]
|
|
148
|
+
name = "domain"
|
|
149
|
+
paths = ["domain/**"]
|
|
150
|
+
dependency_mode = "opt-in"
|
|
151
|
+
allow = []
|
|
152
|
+
external_mode = "opt-out"
|
|
153
|
+
external_deny = []
|
|
154
|
+
|
|
155
|
+
[[layers]]
|
|
156
|
+
name = "usecase"
|
|
157
|
+
paths = ["usecase/**"]
|
|
158
|
+
dependency_mode = "opt-in"
|
|
159
|
+
allow = ["domain"]
|
|
160
|
+
external_mode = "opt-out"
|
|
161
|
+
external_deny = []
|
|
162
|
+
|
|
163
|
+
[[layers]]
|
|
164
|
+
name = "infrastructure"
|
|
165
|
+
paths = ["infrastructure/**"]
|
|
166
|
+
dependency_mode = "opt-out"
|
|
167
|
+
deny = []
|
|
168
|
+
external_mode = "opt-out"
|
|
169
|
+
external_deny = []
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**TypeScript / JavaScript project example:**
|
|
173
|
+
|
|
174
|
+
```toml
|
|
175
|
+
[project]
|
|
176
|
+
name = "my-ts-app"
|
|
177
|
+
root = "."
|
|
178
|
+
languages = ["typescript"]
|
|
179
|
+
|
|
180
|
+
[resolve.typescript]
|
|
181
|
+
tsconfig = "./tsconfig.json"
|
|
182
|
+
|
|
183
|
+
[[layers]]
|
|
184
|
+
name = "domain"
|
|
185
|
+
paths = ["domain/**"]
|
|
186
|
+
dependency_mode = "opt-in"
|
|
187
|
+
allow = []
|
|
188
|
+
external_mode = "opt-out"
|
|
189
|
+
external_deny = []
|
|
190
|
+
|
|
191
|
+
[[layers]]
|
|
192
|
+
name = "usecase"
|
|
193
|
+
paths = ["usecase/**"]
|
|
194
|
+
dependency_mode = "opt-in"
|
|
195
|
+
allow = ["domain"]
|
|
196
|
+
external_mode = "opt-in"
|
|
197
|
+
external_allow = ["zod"]
|
|
198
|
+
|
|
199
|
+
[[layers]]
|
|
200
|
+
name = "infrastructure"
|
|
201
|
+
paths = ["infrastructure/**"]
|
|
202
|
+
dependency_mode = "opt-out"
|
|
203
|
+
deny = []
|
|
204
|
+
external_mode = "opt-out"
|
|
205
|
+
external_deny = []
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
> Use `languages = ["javascript"]` for plain `.js` / `.jsx` projects (no `[resolve.typescript]` needed).
|
|
209
|
+
|
|
210
|
+
**Go project example:**
|
|
211
|
+
|
|
212
|
+
```toml
|
|
213
|
+
[project]
|
|
214
|
+
name = "my-go-app"
|
|
215
|
+
root = "."
|
|
216
|
+
languages = ["go"]
|
|
217
|
+
|
|
218
|
+
[resolve.go]
|
|
219
|
+
module_name = "github.com/myorg/my-go-app"
|
|
220
|
+
|
|
221
|
+
[[layers]]
|
|
222
|
+
name = "domain"
|
|
223
|
+
paths = ["domain/**"]
|
|
224
|
+
dependency_mode = "opt-in"
|
|
225
|
+
allow = []
|
|
226
|
+
|
|
227
|
+
[[layers]]
|
|
228
|
+
name = "usecase"
|
|
229
|
+
paths = ["usecase/**"]
|
|
230
|
+
dependency_mode = "opt-in"
|
|
231
|
+
allow = ["domain"]
|
|
232
|
+
|
|
233
|
+
[[layers]]
|
|
234
|
+
name = "infrastructure"
|
|
235
|
+
paths = ["infrastructure/**"]
|
|
236
|
+
dependency_mode = "opt-out"
|
|
237
|
+
deny = []
|
|
238
|
+
|
|
239
|
+
[[layers]]
|
|
240
|
+
name = "cmd"
|
|
241
|
+
paths = ["cmd/**"]
|
|
242
|
+
dependency_mode = "opt-in"
|
|
243
|
+
allow = ["domain", "usecase", "infrastructure"]
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### 2. Run `mille check`
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
mille check
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Exit codes:
|
|
253
|
+
|
|
254
|
+
| Code | Meaning |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `0` | No violations |
|
|
257
|
+
| `1` | One or more errors detected |
|
|
258
|
+
| `3` | Configuration file error |
|
|
259
|
+
|
|
260
|
+
## Configuration Reference
|
|
261
|
+
|
|
262
|
+
### `[project]`
|
|
263
|
+
|
|
264
|
+
| Key | Description |
|
|
265
|
+
|---|---|
|
|
266
|
+
| `name` | Project name |
|
|
267
|
+
| `root` | Root directory for analysis |
|
|
268
|
+
| `languages` | List of languages to check (e.g. `["rust", "go"]`) |
|
|
269
|
+
|
|
270
|
+
### `[[layers]]`
|
|
271
|
+
|
|
272
|
+
| Key | Description |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `name` | Layer name |
|
|
275
|
+
| `paths` | Glob patterns for files belonging to this layer |
|
|
276
|
+
| `dependency_mode` | `"opt-in"` (deny all except `allow`) or `"opt-out"` (allow all except `deny`) |
|
|
277
|
+
| `allow` | Layers allowed as dependencies (when `dependency_mode = "opt-in"`) |
|
|
278
|
+
| `deny` | Layers forbidden as dependencies (when `dependency_mode = "opt-out"`) |
|
|
279
|
+
| `external_mode` | `"opt-in"` or `"opt-out"` for external library usage |
|
|
280
|
+
| `external_allow` | Regex patterns of allowed external packages (when `external_mode = "opt-in"`) |
|
|
281
|
+
| `external_deny` | Regex patterns of forbidden external packages (when `external_mode = "opt-out"`) |
|
|
282
|
+
|
|
283
|
+
### `[[layers.allow_call_patterns]]`
|
|
284
|
+
|
|
285
|
+
Restricts which methods may be called on a given layer's types. Only valid on the `main` layer.
|
|
286
|
+
|
|
287
|
+
| Key | Description |
|
|
288
|
+
|---|---|
|
|
289
|
+
| `callee_layer` | The layer whose methods are being restricted |
|
|
290
|
+
| `allow_methods` | List of method names that are permitted |
|
|
291
|
+
|
|
292
|
+
### `[resolve.typescript]`
|
|
293
|
+
|
|
294
|
+
| Key | Description |
|
|
295
|
+
|---|---|
|
|
296
|
+
| `tsconfig` | Path to `tsconfig.json`. When specified, mille reads `compilerOptions.paths` and resolves path aliases (e.g. `@/*`) as internal imports. |
|
|
297
|
+
|
|
298
|
+
**How TypeScript / JavaScript imports are classified:**
|
|
299
|
+
|
|
300
|
+
| Import | Classification |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `import X from "./module"` (starts with `./`) | Internal |
|
|
303
|
+
| `import X from "../module"` (starts with `../`) | Internal |
|
|
304
|
+
| `import X from "@/module"` (path alias in `tsconfig.json`) | Internal |
|
|
305
|
+
| `import X from "react"` (npm package) | External |
|
|
306
|
+
| `import fs from "node:fs"` (Node.js built-in) | External |
|
|
307
|
+
|
|
308
|
+
For relative imports, mille resolves the path from the importing file and matches it against layer glob patterns. For example, `import { User } from "../domain/user"` in `usecase/user_usecase.ts` resolves to `domain/user`, matching the layer glob `domain/**`.
|
|
309
|
+
|
|
310
|
+
For path aliases, mille expands the alias using `compilerOptions.paths` and treats the result as an internal import. For example, with `"@/*": ["./src/*"]`, `import { User } from "@/domain/user"` resolves to `src/domain/user`.
|
|
311
|
+
|
|
312
|
+
### `[resolve.go]`
|
|
313
|
+
|
|
314
|
+
| Key | Description |
|
|
315
|
+
|---|---|
|
|
316
|
+
| `module_name` | Go module name (matches the module path in `go.mod`) |
|
|
317
|
+
|
|
318
|
+
### `[resolve.python]`
|
|
319
|
+
|
|
320
|
+
| Key | Description |
|
|
321
|
+
|---|---|
|
|
322
|
+
| `src_root` | Root directory of the Python source tree (relative to `mille.toml`) |
|
|
323
|
+
| `package_names` | List of your own package names (used to classify absolute imports as internal). e.g. `["domain", "usecase", "infrastructure"]` |
|
|
324
|
+
|
|
325
|
+
**How Python imports are classified:**
|
|
326
|
+
|
|
327
|
+
| Import | Classification |
|
|
328
|
+
|---|---|
|
|
329
|
+
| `from .sibling import X` (relative) | Internal |
|
|
330
|
+
| `import domain.entity` (matches a `package_names` entry) | Internal |
|
|
331
|
+
| `import os`, `import sqlalchemy` (others) | External |
|
|
332
|
+
|
|
333
|
+
## Python API
|
|
334
|
+
|
|
335
|
+
In addition to the CLI, the Python package exposes a programmatic API:
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
import mille
|
|
339
|
+
|
|
340
|
+
# Run architecture check and get a result object
|
|
341
|
+
result = mille.check("path/to/mille.toml") # defaults to "mille.toml"
|
|
342
|
+
|
|
343
|
+
print(f"violations: {len(result.violations)}")
|
|
344
|
+
for v in result.violations:
|
|
345
|
+
print(f" {v.file}:{v.line} {v.from_layer} -> {v.to_layer} ({v.import_path})")
|
|
346
|
+
|
|
347
|
+
for stat in result.layer_stats:
|
|
348
|
+
print(f" {stat.name}: {stat.file_count} file(s), {stat.violation_count} violation(s)")
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Types exposed:**
|
|
352
|
+
|
|
353
|
+
| Class | Attributes |
|
|
354
|
+
|---|---|
|
|
355
|
+
| `CheckResult` | `violations: list[Violation]`, `layer_stats: list[LayerStat]` |
|
|
356
|
+
| `Violation` | `file`, `line`, `from_layer`, `to_layer`, `import_path`, `kind` |
|
|
357
|
+
| `LayerStat` | `name`, `file_count`, `violation_count` |
|
|
358
|
+
|
|
359
|
+
## How it Works
|
|
360
|
+
|
|
361
|
+
mille uses [tree-sitter](https://tree-sitter.github.io/) for AST-based import extraction — no regex heuristics. The core engine is language-agnostic; language-specific logic is isolated to the `parser` and `resolver` layers.
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
mille.toml
|
|
365
|
+
│
|
|
366
|
+
▼
|
|
367
|
+
Layer definitions
|
|
368
|
+
│
|
|
369
|
+
Source files (*.rs, *.go, *.py, ...)
|
|
370
|
+
│ tree-sitter parse
|
|
371
|
+
▼
|
|
372
|
+
RawImport list
|
|
373
|
+
│ Resolver (stdlib / internal / external)
|
|
374
|
+
▼
|
|
375
|
+
ResolvedImport list
|
|
376
|
+
│ ViolationDetector
|
|
377
|
+
▼
|
|
378
|
+
Violations → terminal output
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Dogfooding
|
|
382
|
+
|
|
383
|
+
mille checks its own source code on every CI run:
|
|
384
|
+
|
|
385
|
+
```sh
|
|
386
|
+
mille check # uses ./mille.toml
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
See [mille.toml](./mille.toml) for the architecture rules applied to mille itself.
|
|
390
|
+
|
|
391
|
+
## Documentation
|
|
392
|
+
|
|
393
|
+
- [spec.md](./spec.md) — Full specification (in Japanese)
|
|
394
|
+
- [docs/TODO.md](./docs/TODO.md) — Development roadmap
|
package/index.js
CHANGED
|
@@ -1,2 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Suppress the WASI experimental warning so that mille's own stderr output
|
|
5
|
+
// is not mixed with Node.js internals noise.
|
|
6
|
+
process.on('warning', (w) => {
|
|
7
|
+
if (w.name === 'ExperimentalWarning' && /WASI/i.test(w.message)) return;
|
|
8
|
+
process.stderr.write(w.stack + '\n');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const { WASI } = require('node:wasi');
|
|
12
|
+
const { readFileSync } = require('node:fs');
|
|
13
|
+
const { join } = require('node:path');
|
|
14
|
+
|
|
15
|
+
async function run() {
|
|
16
|
+
const wasmPath = join(__dirname, 'mille.wasm');
|
|
17
|
+
let wasmBuffer;
|
|
18
|
+
try {
|
|
19
|
+
wasmBuffer = readFileSync(wasmPath);
|
|
20
|
+
} catch {
|
|
21
|
+
process.stderr.write(
|
|
22
|
+
'mille: mille.wasm not found. Please reinstall the package.\n'
|
|
23
|
+
);
|
|
24
|
+
process.exit(3);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// NOTE: Mount the host CWD as "/" inside WASI so that paths like
|
|
28
|
+
// "mille.toml" and "src/domain/**" resolve correctly relative
|
|
29
|
+
// to the project root — same as the Go/wazero wrapper.
|
|
30
|
+
const wasi = new WASI({
|
|
31
|
+
version: 'preview1',
|
|
32
|
+
args: ['mille', ...process.argv.slice(2)],
|
|
33
|
+
env: process.env,
|
|
34
|
+
preopens: { '/': process.cwd() },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const { instance } = await WebAssembly.instantiate(wasmBuffer, {
|
|
38
|
+
wasi_snapshot_preview1: wasi.wasiImport,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// NOTE: wasi.start() calls process.exit() directly when the WASI module
|
|
42
|
+
// invokes proc_exit. Exit codes:
|
|
43
|
+
// 0 — no violations
|
|
44
|
+
// 1 — at least one error-severity violation
|
|
45
|
+
// 3 — configuration or runtime error
|
|
46
|
+
wasi.start(instance);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
run().catch((err) => {
|
|
50
|
+
process.stderr.write('mille: ' + err.message + '\n');
|
|
51
|
+
process.exit(3);
|
|
52
|
+
});
|
package/mille.wasm
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makinzm/mille",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Architecture Checker — Rust-based multi-language architecture linter",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mille": "index.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"mille.wasm"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
9
16
|
"repository": {
|
|
10
17
|
"type": "git",
|
|
11
18
|
"url": "git+https://github.com/makinzm/mille.git"
|