stratifyjs 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/LICENSE +21 -0
- package/README.md +354 -0
- package/lib/adapters/config-file-loader.d.ts +8 -0
- package/lib/adapters/config-file-loader.js +55 -0
- package/lib/adapters/file-system-discovery.d.ts +22 -0
- package/lib/adapters/file-system-discovery.js +61 -0
- package/lib/api/api.d.ts +42 -0
- package/lib/api/api.js +75 -0
- package/lib/api/index.d.ts +15 -0
- package/lib/api/index.js +12 -0
- package/lib/cli/command-handler.d.ts +6 -0
- package/lib/cli/command-handler.js +50 -0
- package/lib/cli/index.d.ts +2 -0
- package/lib/cli/index.js +23 -0
- package/lib/cli/options.d.ts +18 -0
- package/lib/cli/options.js +21 -0
- package/lib/cli/output-helpers.d.ts +9 -0
- package/lib/cli/output-helpers.js +22 -0
- package/lib/core/config-defaults.d.ts +7 -0
- package/lib/core/config-defaults.js +22 -0
- package/lib/core/config-schema.d.ts +11 -0
- package/lib/core/config-schema.js +102 -0
- package/lib/core/errors.d.ts +44 -0
- package/lib/core/errors.js +25 -0
- package/lib/core/formatters/console-formatter.d.ts +3 -0
- package/lib/core/formatters/console-formatter.js +34 -0
- package/lib/core/formatters/json-formatter.d.ts +5 -0
- package/lib/core/formatters/json-formatter.js +11 -0
- package/lib/core/package-parser.d.ts +11 -0
- package/lib/core/package-parser.js +37 -0
- package/lib/core/report-builder.d.ts +18 -0
- package/lib/core/report-builder.js +17 -0
- package/lib/core/result.d.ts +33 -0
- package/lib/core/result.js +24 -0
- package/lib/core/rules.d.ts +14 -0
- package/lib/core/rules.js +19 -0
- package/lib/core/validation.d.ts +5 -0
- package/lib/core/validation.js +51 -0
- package/lib/types/types.d.ts +66 -0
- package/lib/types/types.js +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Scott Mikula
|
|
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,354 @@
|
|
|
1
|
+
# Stratify
|
|
2
|
+
|
|
3
|
+
Enforce architectural layer boundaries in monorepos. Catches invalid cross-layer dependencies at build time by analyzing `workspace:` protocol imports in `package.json` files.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install stratifyjs
|
|
9
|
+
# or
|
|
10
|
+
yarn add stratifyjs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For CLI-only usage you can install globally:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g stratifyjs
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
1. Add a `"layer"` field to each workspace `package.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"name": "my-feature",
|
|
26
|
+
"layer": "features",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"my-core-lib": "workspace:*"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
2. Create a `stratify.config.json` at your workspace root:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"layers": {
|
|
38
|
+
"features": {
|
|
39
|
+
"description": "Feature packages",
|
|
40
|
+
"allowedDependencies": ["core", "shared"]
|
|
41
|
+
},
|
|
42
|
+
"core": {
|
|
43
|
+
"description": "Core business logic",
|
|
44
|
+
"allowedDependencies": ["shared"]
|
|
45
|
+
},
|
|
46
|
+
"shared": {
|
|
47
|
+
"description": "Shared utilities",
|
|
48
|
+
"allowedDependencies": []
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
3. Run:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
stratify --config stratify.config.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## CLI Usage
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
stratify [options]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Option | Default | Description |
|
|
67
|
+
| --------------------- | ---------------------- | ---------------------------------------------------- |
|
|
68
|
+
| `-c, --config <path>` | `stratify.config.json` | Path to the layer config file (relative to root) |
|
|
69
|
+
| `-r, --root <path>` | `process.cwd()` | Workspace root directory |
|
|
70
|
+
| `-m, --mode <mode>` | Config value or `warn` | Override enforcement mode: `error`, `warn`, or `off` |
|
|
71
|
+
| `--format <type>` | `console` | Output format: `console` or `json` |
|
|
72
|
+
| `-V, --version` | | Print version |
|
|
73
|
+
| `-h, --help` | | Print help |
|
|
74
|
+
|
|
75
|
+
### Examples
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Basic check with defaults
|
|
79
|
+
stratify
|
|
80
|
+
|
|
81
|
+
# Explicit config and root
|
|
82
|
+
stratify --config stratify.config.json --root /path/to/monorepo
|
|
83
|
+
|
|
84
|
+
# Fail CI on violations
|
|
85
|
+
stratify --mode error
|
|
86
|
+
|
|
87
|
+
# Machine-readable output
|
|
88
|
+
stratify --format json
|
|
89
|
+
|
|
90
|
+
# Combine options
|
|
91
|
+
stratify -c stratify.config.json -r ../.. -m error --format console
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Exit Codes
|
|
95
|
+
|
|
96
|
+
| Code | Meaning |
|
|
97
|
+
| ---- | --------------------------------------------------------------- |
|
|
98
|
+
| `0` | No violations, or mode is `warn`/`off` |
|
|
99
|
+
| `1` | Violations found and mode is `error`, or a fatal error occurred |
|
|
100
|
+
|
|
101
|
+
## Programmatic API
|
|
102
|
+
|
|
103
|
+
The library API is available for custom tooling, editor integrations, or CI pipelines.
|
|
104
|
+
|
|
105
|
+
### `enforceLayersAsync(options?)`
|
|
106
|
+
|
|
107
|
+
Run the full enforcement pipeline: load config → discover packages → validate → report.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { enforceLayersAsync, formatResults } from 'stratifyjs';
|
|
111
|
+
|
|
112
|
+
const result = await enforceLayersAsync({
|
|
113
|
+
workspaceRoot: '/path/to/monorepo',
|
|
114
|
+
configPath: 'stratify.config.json',
|
|
115
|
+
mode: 'error', // optional override
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!result.success) {
|
|
119
|
+
console.error('Error:', result.error);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { violations, packages, config, report, warnings } = result.value;
|
|
124
|
+
console.log(`Checked ${packages.length} packages, found ${violations.length} violations`);
|
|
125
|
+
|
|
126
|
+
// Format for display
|
|
127
|
+
const output = formatResults(result.value, 'console', 'warn');
|
|
128
|
+
console.log(output);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `validateConfig(raw)`
|
|
132
|
+
|
|
133
|
+
Validate a raw config object without reading from disk. Useful for editor integrations.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { validateConfig } from 'stratifyjs';
|
|
137
|
+
|
|
138
|
+
const result = validateConfig({
|
|
139
|
+
layers: {
|
|
140
|
+
core: { allowedDependencies: [] },
|
|
141
|
+
features: { allowedDependencies: ['core'] },
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (result.success) {
|
|
146
|
+
console.log('Valid config:', result.value);
|
|
147
|
+
} else {
|
|
148
|
+
console.error('Invalid:', result.error);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `formatResults(result, format, mode)`
|
|
153
|
+
|
|
154
|
+
Format an `EnforceLayersResult` as a string for display.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { formatResults } from 'stratifyjs';
|
|
158
|
+
|
|
159
|
+
const consoleOutput = formatResults(result.value, 'console', 'warn');
|
|
160
|
+
const jsonOutput = formatResults(result.value, 'json', 'error');
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Config File Format
|
|
164
|
+
|
|
165
|
+
The config file (default: `stratify.config.json`) is a JSON object with three sections:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"layers": { ... },
|
|
170
|
+
"enforcement": { ... },
|
|
171
|
+
"workspaces": { ... }
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `layers` (required)
|
|
176
|
+
|
|
177
|
+
A map of layer names to their definitions. Each layer must specify which other layers it is allowed to depend on.
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"layers": {
|
|
182
|
+
"adapters": {
|
|
183
|
+
"description": "I/O and external integrations",
|
|
184
|
+
"allowedDependencies": ["core", "types"]
|
|
185
|
+
},
|
|
186
|
+
"core": {
|
|
187
|
+
"description": "Pure business logic",
|
|
188
|
+
"allowedDependencies": ["types"]
|
|
189
|
+
},
|
|
190
|
+
"types": {
|
|
191
|
+
"description": "Shared type definitions",
|
|
192
|
+
"allowedDependencies": []
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
| Field | Type | Required | Description |
|
|
199
|
+
| --------------------- | ---------- | -------- | -------------------------------------------------------------------- |
|
|
200
|
+
| `description` | `string` | No | Human-readable description of the layer's purpose |
|
|
201
|
+
| `allowedDependencies` | `string[]` | **Yes** | Layer names this layer may depend on. Use `"*"` to allow all layers. |
|
|
202
|
+
|
|
203
|
+
### `enforcement` (optional)
|
|
204
|
+
|
|
205
|
+
Controls how violations are reported.
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"enforcement": {
|
|
210
|
+
"mode": "error"
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
| Field | Type | Default | Description |
|
|
216
|
+
| ------ | ---------------------------- | -------- | ------------------------------------------------------------------------- |
|
|
217
|
+
| `mode` | `"error" \| "warn" \| "off"` | `"warn"` | `error` = non-zero exit on violations; `warn` = report only; `off` = skip |
|
|
218
|
+
|
|
219
|
+
### `workspaces` (optional)
|
|
220
|
+
|
|
221
|
+
Controls which packages are discovered for validation.
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"workspaces": {
|
|
226
|
+
"patterns": ["packages/**/*", "shared/**/*"]
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| Field | Type | Default | Description |
|
|
232
|
+
| ---------- | ---------- | ------------------- | ------------------------------------------------------------------------------- |
|
|
233
|
+
| `patterns` | `string[]` | `["packages/**/*"]` | Glob patterns to locate workspace packages (each must contain a `package.json`) |
|
|
234
|
+
|
|
235
|
+
Ignored paths (hardcoded): `**/node_modules/**`, `**/lib/**`, `**/dist/**`.
|
|
236
|
+
|
|
237
|
+
## Layer Definition Reference
|
|
238
|
+
|
|
239
|
+
### Package Assignment
|
|
240
|
+
|
|
241
|
+
Each workspace package declares its layer via the `"layer"` field in `package.json`:
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"name": "my-package",
|
|
246
|
+
"version": "1.0.0",
|
|
247
|
+
"layer": "core"
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Packages missing the `"layer"` field produce a `missing-layer` violation.
|
|
252
|
+
|
|
253
|
+
### Dependency Detection
|
|
254
|
+
|
|
255
|
+
Only `workspace:` protocol dependencies are checked, across all three dependency fields:
|
|
256
|
+
|
|
257
|
+
- `dependencies`
|
|
258
|
+
- `devDependencies`
|
|
259
|
+
- `peerDependencies`
|
|
260
|
+
|
|
261
|
+
External (npm registry) dependencies are ignored — layers only govern internal monorepo boundaries.
|
|
262
|
+
|
|
263
|
+
### Violation Types
|
|
264
|
+
|
|
265
|
+
| Type | Description |
|
|
266
|
+
| -------------------- | ------------------------------------------------------------------------------ |
|
|
267
|
+
| `missing-layer` | Package has no `"layer"` field in its `package.json` |
|
|
268
|
+
| `unknown-layer` | Package declares a layer not defined in the config |
|
|
269
|
+
| `invalid-dependency` | Package depends on another package whose layer is not in `allowedDependencies` |
|
|
270
|
+
|
|
271
|
+
### Wildcard Dependencies
|
|
272
|
+
|
|
273
|
+
Use `"*"` to allow a layer to depend on any other layer:
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"layers": {
|
|
278
|
+
"app": {
|
|
279
|
+
"description": "Application entry points — can use anything",
|
|
280
|
+
"allowedDependencies": ["*"]
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Full Config Example
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"layers": {
|
|
291
|
+
"app": {
|
|
292
|
+
"description": "Application entry points",
|
|
293
|
+
"allowedDependencies": ["features", "core", "shared"]
|
|
294
|
+
},
|
|
295
|
+
"features": {
|
|
296
|
+
"description": "Feature modules",
|
|
297
|
+
"allowedDependencies": ["core", "shared"]
|
|
298
|
+
},
|
|
299
|
+
"core": {
|
|
300
|
+
"description": "Core business logic",
|
|
301
|
+
"allowedDependencies": ["shared"]
|
|
302
|
+
},
|
|
303
|
+
"shared": {
|
|
304
|
+
"description": "Shared utilities and types",
|
|
305
|
+
"allowedDependencies": []
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
"enforcement": {
|
|
309
|
+
"mode": "error"
|
|
310
|
+
},
|
|
311
|
+
"workspaces": {
|
|
312
|
+
"patterns": ["packages/**/*", "libs/**/*"]
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## CI Integration
|
|
318
|
+
|
|
319
|
+
Add to your CI pipeline to enforce boundaries on every PR:
|
|
320
|
+
|
|
321
|
+
```yaml
|
|
322
|
+
# GitHub Actions
|
|
323
|
+
- name: Enforce layers
|
|
324
|
+
run: npx stratify --config stratify.config.json --mode error
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
```yaml
|
|
328
|
+
# Azure Pipelines
|
|
329
|
+
- script: npx stratify --config stratify.config.json --mode error
|
|
330
|
+
displayName: 'Enforce layer boundaries'
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Development
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
# Install dependencies
|
|
337
|
+
yarn install
|
|
338
|
+
|
|
339
|
+
# Run tests
|
|
340
|
+
yarn test
|
|
341
|
+
|
|
342
|
+
# Run tests in watch mode
|
|
343
|
+
yarn test:watch
|
|
344
|
+
|
|
345
|
+
# Build (compile TypeScript → lib/)
|
|
346
|
+
yarn build
|
|
347
|
+
|
|
348
|
+
# Type-check without emitting
|
|
349
|
+
yarn typecheck
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResolvedConfig } from '../types/types.js';
|
|
2
|
+
import type { ConfigError } from '../core/errors.js';
|
|
3
|
+
import type { Result } from '../core/result.js';
|
|
4
|
+
/**
|
|
5
|
+
* Load a layer config file from disk, validate it, and apply defaults.
|
|
6
|
+
* I/O adapter — reads from the file system.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadConfigFromFile(workspaceRoot: string, configPath: string): Promise<Result<ResolvedConfig, ConfigError>>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFile, access } from 'fs/promises';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { ok, err } from '../core/result.js';
|
|
4
|
+
import { validateConfigSchema } from '../core/config-schema.js';
|
|
5
|
+
import { applyDefaults } from '../core/config-defaults.js';
|
|
6
|
+
/**
|
|
7
|
+
* Load a layer config file from disk, validate it, and apply defaults.
|
|
8
|
+
* I/O adapter — reads from the file system.
|
|
9
|
+
*/
|
|
10
|
+
export async function loadConfigFromFile(workspaceRoot, configPath) {
|
|
11
|
+
const fullPath = resolve(workspaceRoot, configPath);
|
|
12
|
+
// Check file exists
|
|
13
|
+
try {
|
|
14
|
+
await access(fullPath);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return err({
|
|
18
|
+
type: 'config-not-found',
|
|
19
|
+
message: `Config file not found: ${fullPath}`,
|
|
20
|
+
path: fullPath,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// Read file
|
|
24
|
+
let content;
|
|
25
|
+
try {
|
|
26
|
+
content = await readFile(fullPath, 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return err({
|
|
30
|
+
type: 'config-read-error',
|
|
31
|
+
message: error instanceof Error ? error.message : String(error),
|
|
32
|
+
path: fullPath,
|
|
33
|
+
cause: error,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Parse JSON
|
|
37
|
+
let rawConfig;
|
|
38
|
+
try {
|
|
39
|
+
rawConfig = JSON.parse(content);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return err({
|
|
43
|
+
type: 'config-parse-error',
|
|
44
|
+
message: error instanceof Error ? error.message : String(error),
|
|
45
|
+
path: fullPath,
|
|
46
|
+
cause: error,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Validate schema
|
|
50
|
+
const validated = validateConfigSchema(rawConfig);
|
|
51
|
+
if (!validated.success) {
|
|
52
|
+
return validated;
|
|
53
|
+
}
|
|
54
|
+
return ok(applyDefaults(validated.value));
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Package, WorkspaceConfig } from '../types/types.js';
|
|
2
|
+
import type { DiscoveryError } from '../core/errors.js';
|
|
3
|
+
import type { Result } from '../core/result.js';
|
|
4
|
+
/**
|
|
5
|
+
* Non-fatal warning produced during discovery.
|
|
6
|
+
*/
|
|
7
|
+
export interface DiscoveryWarning {
|
|
8
|
+
path: string;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Successful discovery result containing packages and any non-fatal warnings.
|
|
13
|
+
*/
|
|
14
|
+
export interface DiscoveryResult {
|
|
15
|
+
packages: Package[];
|
|
16
|
+
warnings: DiscoveryWarning[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Discover all packages in the workspace by globbing for package.json files.
|
|
20
|
+
* I/O adapter — reads from the file system using glob + readFile.
|
|
21
|
+
*/
|
|
22
|
+
export declare function discoverPackages(root: string, config: WorkspaceConfig, ignore?: string[]): Promise<Result<DiscoveryResult, DiscoveryError>>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import { ok, err } from '../core/result.js';
|
|
5
|
+
import { parsePackageJson } from '../core/package-parser.js';
|
|
6
|
+
const DEFAULT_IGNORE = ['**/node_modules/**', '**/lib/**', '**/dist/**'];
|
|
7
|
+
/**
|
|
8
|
+
* Discover all packages in the workspace by globbing for package.json files.
|
|
9
|
+
* I/O adapter — reads from the file system using glob + readFile.
|
|
10
|
+
*/
|
|
11
|
+
export async function discoverPackages(root, config, ignore = DEFAULT_IGNORE) {
|
|
12
|
+
const allPaths = [];
|
|
13
|
+
for (const pattern of config.patterns) {
|
|
14
|
+
try {
|
|
15
|
+
const paths = await glob(`${pattern}/package.json`, {
|
|
16
|
+
cwd: root,
|
|
17
|
+
ignore,
|
|
18
|
+
});
|
|
19
|
+
allPaths.push(...paths);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return err({
|
|
23
|
+
type: 'glob-failed',
|
|
24
|
+
message: error instanceof Error ? error.message : String(error),
|
|
25
|
+
pattern,
|
|
26
|
+
cause: error,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const uniquePaths = [...new Set(allPaths)];
|
|
31
|
+
const settledPackages = await Promise.allSettled(uniquePaths.map(async (relativePath) => {
|
|
32
|
+
const fullPath = resolve(root, relativePath);
|
|
33
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
34
|
+
const parsed = JSON.parse(content);
|
|
35
|
+
return { relativePath, fullPath, parsed };
|
|
36
|
+
}));
|
|
37
|
+
const packages = [];
|
|
38
|
+
const warnings = [];
|
|
39
|
+
for (let i = 0; i < settledPackages.length; i++) {
|
|
40
|
+
const entry = settledPackages[i];
|
|
41
|
+
const relativePath = uniquePaths[i];
|
|
42
|
+
const fullPath = resolve(root, relativePath);
|
|
43
|
+
if (entry.status === 'rejected') {
|
|
44
|
+
const reason = entry.reason;
|
|
45
|
+
warnings.push({
|
|
46
|
+
path: fullPath,
|
|
47
|
+
message: reason instanceof Error ? reason.message : String(reason),
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const { parsed } = entry.value;
|
|
52
|
+
const result = parsePackageJson(parsed, relativePath);
|
|
53
|
+
if (result.success) {
|
|
54
|
+
packages.push(result.value);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
warnings.push({ path: fullPath, message: result.error.message });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return ok({ packages, warnings });
|
|
61
|
+
}
|
package/lib/api/api.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ResolvedConfig, Violation, Package, EnforcementConfig } from '../types/types.js';
|
|
2
|
+
import type { LayerError, ConfigError } from '../core/errors.js';
|
|
3
|
+
import type { Result } from '../core/result.js';
|
|
4
|
+
import { type DiscoveryWarning } from '../adapters/file-system-discovery.js';
|
|
5
|
+
import { type ValidationReport } from '../core/report-builder.js';
|
|
6
|
+
/**
|
|
7
|
+
* Options for the enforce-layers library API.
|
|
8
|
+
*/
|
|
9
|
+
export interface EnforceLayersOptions {
|
|
10
|
+
/** Workspace root directory. Defaults to process.cwd(). */
|
|
11
|
+
workspaceRoot?: string;
|
|
12
|
+
/** Path to the config file, relative to workspaceRoot. */
|
|
13
|
+
configPath?: string;
|
|
14
|
+
/** Provide a pre-built config directly, skipping file loading. */
|
|
15
|
+
config?: ResolvedConfig;
|
|
16
|
+
/** Override the enforcement mode from config. */
|
|
17
|
+
mode?: EnforcementConfig['mode'];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Result of running layer enforcement.
|
|
21
|
+
*/
|
|
22
|
+
export interface EnforceLayersResult {
|
|
23
|
+
violations: Violation[];
|
|
24
|
+
packages: Package[];
|
|
25
|
+
config: ResolvedConfig;
|
|
26
|
+
report: ValidationReport;
|
|
27
|
+
warnings: DiscoveryWarning[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run layer enforcement programmatically.
|
|
31
|
+
* Returns a Result
|
|
32
|
+
*/
|
|
33
|
+
export declare function enforceLayersAsync(options?: EnforceLayersOptions): Promise<Result<EnforceLayersResult, LayerError>>;
|
|
34
|
+
/**
|
|
35
|
+
* Validate a raw config object without reading from a file.
|
|
36
|
+
* Useful for editor integrations or programmatic config construction.
|
|
37
|
+
*/
|
|
38
|
+
export declare function validateConfig(raw: unknown): Result<ResolvedConfig, ConfigError>;
|
|
39
|
+
/**
|
|
40
|
+
* Format an enforcement result for display.
|
|
41
|
+
*/
|
|
42
|
+
export declare function formatResults(result: EnforceLayersResult, format?: 'console' | 'json', mode?: EnforcementConfig['mode']): string;
|
package/lib/api/api.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { ok } from '../core/result.js';
|
|
3
|
+
import { validateConfigSchema } from '../core/config-schema.js';
|
|
4
|
+
import { applyDefaults } from '../core/config-defaults.js';
|
|
5
|
+
import { validatePackages } from '../core/validation.js';
|
|
6
|
+
import { loadConfigFromFile } from '../adapters/config-file-loader.js';
|
|
7
|
+
import { discoverPackages } from '../adapters/file-system-discovery.js';
|
|
8
|
+
import { buildReport } from '../core/report-builder.js';
|
|
9
|
+
import { formatConsole } from '../core/formatters/console-formatter.js';
|
|
10
|
+
import { formatJson } from '../core/formatters/json-formatter.js';
|
|
11
|
+
/**
|
|
12
|
+
* Run layer enforcement programmatically.
|
|
13
|
+
* Returns a Result
|
|
14
|
+
*/
|
|
15
|
+
export async function enforceLayersAsync(options = {}) {
|
|
16
|
+
const startTime = performance.now();
|
|
17
|
+
const workspaceRoot = resolve(options.workspaceRoot ?? process.cwd());
|
|
18
|
+
// Resolve config
|
|
19
|
+
let config;
|
|
20
|
+
if (options.config) {
|
|
21
|
+
config = options.config;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const configPath = options.configPath ?? 'stratify.config.json';
|
|
25
|
+
const configResult = await loadConfigFromFile(workspaceRoot, configPath);
|
|
26
|
+
if (!configResult.success) {
|
|
27
|
+
return configResult;
|
|
28
|
+
}
|
|
29
|
+
config = configResult.value;
|
|
30
|
+
}
|
|
31
|
+
// Apply mode override
|
|
32
|
+
if (options.mode) {
|
|
33
|
+
config = {
|
|
34
|
+
...config,
|
|
35
|
+
enforcement: { ...config.enforcement, mode: options.mode },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Discover packages
|
|
39
|
+
const discoveryResult = await discoverPackages(workspaceRoot, config.workspaces);
|
|
40
|
+
if (!discoveryResult.success) {
|
|
41
|
+
return discoveryResult;
|
|
42
|
+
}
|
|
43
|
+
const { packages, warnings } = discoveryResult.value;
|
|
44
|
+
// Validate packages against config
|
|
45
|
+
const violations = validatePackages(packages, config);
|
|
46
|
+
const duration = performance.now() - startTime;
|
|
47
|
+
const report = buildReport(violations, {
|
|
48
|
+
totalPackages: packages.length,
|
|
49
|
+
duration,
|
|
50
|
+
});
|
|
51
|
+
return ok({ violations, packages, config, report, warnings });
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate a raw config object without reading from a file.
|
|
55
|
+
* Useful for editor integrations or programmatic config construction.
|
|
56
|
+
*/
|
|
57
|
+
export function validateConfig(raw) {
|
|
58
|
+
const validated = validateConfigSchema(raw);
|
|
59
|
+
if (!validated.success) {
|
|
60
|
+
return validated;
|
|
61
|
+
}
|
|
62
|
+
return ok(applyDefaults(validated.value));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Format an enforcement result for display.
|
|
66
|
+
*/
|
|
67
|
+
export function formatResults(result, format = 'console', mode = 'warn') {
|
|
68
|
+
switch (format) {
|
|
69
|
+
case 'json':
|
|
70
|
+
return formatJson(result.report);
|
|
71
|
+
case 'console':
|
|
72
|
+
default:
|
|
73
|
+
return formatConsole(result.report, mode);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { enforceLayersAsync, validateConfig, formatResults } from './api.js';
|
|
2
|
+
export type { EnforceLayersOptions, EnforceLayersResult } from './api.js';
|
|
3
|
+
export type { Package, LayerConfig, LayerDefinition, LayerMap, ResolvedConfig, EnforcementConfig, WorkspaceConfig, Violation, ViolationType, } from '../types/types.js';
|
|
4
|
+
export type { LayerError, ConfigError, DiscoveryError } from '../core/errors.js';
|
|
5
|
+
export { formatLayerError } from '../core/errors.js';
|
|
6
|
+
export type { Result } from '../core/result.js';
|
|
7
|
+
export { ok, err, isOk, isErr } from '../core/result.js';
|
|
8
|
+
export type { ValidationReport } from '../core/report-builder.js';
|
|
9
|
+
export { buildReport } from '../core/report-builder.js';
|
|
10
|
+
export { validatePackages } from '../core/validation.js';
|
|
11
|
+
export { validateConfigSchema } from '../core/config-schema.js';
|
|
12
|
+
export { applyDefaults } from '../core/config-defaults.js';
|
|
13
|
+
export { parsePackageJson, extractWorkspaceDependencies } from '../core/package-parser.js';
|
|
14
|
+
export { formatConsole } from '../core/formatters/console-formatter.js';
|
|
15
|
+
export { formatJson } from '../core/formatters/json-formatter.js';
|
package/lib/api/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Main API
|
|
2
|
+
export { enforceLayersAsync, validateConfig, formatResults } from './api.js';
|
|
3
|
+
export { formatLayerError } from '../core/errors.js';
|
|
4
|
+
export { ok, err, isOk, isErr } from '../core/result.js';
|
|
5
|
+
export { buildReport } from '../core/report-builder.js';
|
|
6
|
+
// Advanced: individual components for custom pipelines
|
|
7
|
+
export { validatePackages } from '../core/validation.js';
|
|
8
|
+
export { validateConfigSchema } from '../core/config-schema.js';
|
|
9
|
+
export { applyDefaults } from '../core/config-defaults.js';
|
|
10
|
+
export { parsePackageJson, extractWorkspaceDependencies } from '../core/package-parser.js';
|
|
11
|
+
export { formatConsole } from '../core/formatters/console-formatter.js';
|
|
12
|
+
export { formatJson } from '../core/formatters/json-formatter.js';
|