@canonical/webarchitect 0.10.0 → 0.12.0-experimental.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 +35 -9
- package/package.json +17 -15
- package/rulesets/library.ruleset.json +19 -0
- package/rulesets/package.ruleset.json +1 -5
- package/rulesets/tool-ts.ruleset.json +107 -0
- package/rulesets/tool.ruleset.json +19 -0
- package/src/cli.ts +322 -0
- package/src/constants.ts +8 -0
- package/{dist/esm/index.js → src/index.ts} +0 -1
- package/{dist/esm/lib/ajv.js → src/lib/ajv.ts} +47 -24
- package/src/lib/describeSchema.ts +59 -0
- package/src/lib/discoverAllRulesets.ts +18 -0
- package/src/lib/discoverRulesetsInDir.ts +41 -0
- package/src/lib/executeValidationRules.ts +54 -0
- package/{dist/esm/lib/index.js → src/lib/index.ts} +1 -1
- package/{dist/esm/lib/listDirectory.js → src/lib/listDirectory.ts} +12 -10
- package/{dist/esm/lib/loadFullSchema.js → src/lib/loadFullSchema.ts} +16 -12
- package/src/lib/resolveSchema.ts +128 -0
- package/src/lib/validateDirectoryRule.ts +158 -0
- package/src/lib/validateFileRule.ts +105 -0
- package/{dist/esm/lib/validateRule.js → src/lib/validateRule.ts} +18 -11
- package/src/schema.json +61 -0
- package/src/schemas/draft-2020-12.json +58 -0
- package/src/schemas/meta/applicator.json +45 -0
- package/src/schemas/meta/content.json +14 -0
- package/src/schemas/meta/core.json +48 -0
- package/src/schemas/meta/format-annotation.json +11 -0
- package/src/schemas/meta/meta-data.json +34 -0
- package/src/schemas/meta/unevaluated.json +12 -0
- package/src/schemas/meta/validation.json +95 -0
- package/src/types.ts +49 -0
- package/{dist/esm/validate.js → src/validate.ts} +8 -4
- package/dist/esm/cli.js +0 -277
- package/dist/esm/cli.js.map +0 -1
- package/dist/esm/constants.js +0 -3
- package/dist/esm/constants.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/lib/ajv.js.map +0 -1
- package/dist/esm/lib/describeSchema.js +0 -53
- package/dist/esm/lib/describeSchema.js.map +0 -1
- package/dist/esm/lib/discoverAllRulesets.js +0 -13
- package/dist/esm/lib/discoverAllRulesets.js.map +0 -1
- package/dist/esm/lib/discoverRulesetsInDir.js +0 -35
- package/dist/esm/lib/discoverRulesetsInDir.js.map +0 -1
- package/dist/esm/lib/executeValidationRules.js +0 -41
- package/dist/esm/lib/executeValidationRules.js.map +0 -1
- package/dist/esm/lib/index.js.map +0 -1
- package/dist/esm/lib/listDirectory.js.map +0 -1
- package/dist/esm/lib/loadFullSchema.js.map +0 -1
- package/dist/esm/lib/resolveSchema.js +0 -113
- package/dist/esm/lib/resolveSchema.js.map +0 -1
- package/dist/esm/lib/validateDirectoryRule.js +0 -138
- package/dist/esm/lib/validateDirectoryRule.js.map +0 -1
- package/dist/esm/lib/validateFileRule.js +0 -92
- package/dist/esm/lib/validateFileRule.js.map +0 -1
- package/dist/esm/lib/validateRule.js.map +0 -1
- package/dist/esm/schema.json +0 -61
- package/dist/esm/schemas/draft-2020-12.json +0 -57
- package/dist/esm/schemas/meta/applicator.json +0 -44
- package/dist/esm/schemas/meta/content.json +0 -12
- package/dist/esm/schemas/meta/core.json +0 -47
- package/dist/esm/schemas/meta/format-annotation.json +0 -10
- package/dist/esm/schemas/meta/meta-data.json +0 -32
- package/dist/esm/schemas/meta/unevaluated.json +0 -11
- package/dist/esm/schemas/meta/validation.json +0 -94
- package/dist/esm/types.js +0 -2
- package/dist/esm/types.js.map +0 -1
- package/dist/esm/validate.js.map +0 -1
- package/dist/types/cli.d.ts +0 -3
- package/dist/types/cli.d.ts.map +0 -1
- package/dist/types/constants.d.ts +0 -2
- package/dist/types/constants.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -2
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/lib/ajv.d.ts +0 -4
- package/dist/types/lib/ajv.d.ts.map +0 -1
- package/dist/types/lib/describeSchema.d.ts +0 -22
- package/dist/types/lib/describeSchema.d.ts.map +0 -1
- package/dist/types/lib/discoverAllRulesets.d.ts +0 -9
- package/dist/types/lib/discoverAllRulesets.d.ts.map +0 -1
- package/dist/types/lib/discoverRulesetsInDir.d.ts +0 -6
- package/dist/types/lib/discoverRulesetsInDir.d.ts.map +0 -1
- package/dist/types/lib/executeValidationRules.d.ts +0 -22
- package/dist/types/lib/executeValidationRules.d.ts.map +0 -1
- package/dist/types/lib/index.d.ts +0 -15
- package/dist/types/lib/index.d.ts.map +0 -1
- package/dist/types/lib/listDirectory.d.ts +0 -20
- package/dist/types/lib/listDirectory.d.ts.map +0 -1
- package/dist/types/lib/loadFullSchema.d.ts +0 -20
- package/dist/types/lib/loadFullSchema.d.ts.map +0 -1
- package/dist/types/lib/resolveSchema.d.ts +0 -29
- package/dist/types/lib/resolveSchema.d.ts.map +0 -1
- package/dist/types/lib/validateDirectoryRule.d.ts +0 -26
- package/dist/types/lib/validateDirectoryRule.d.ts.map +0 -1
- package/dist/types/lib/validateFileRule.d.ts +0 -29
- package/dist/types/lib/validateFileRule.d.ts.map +0 -1
- package/dist/types/lib/validateRule.d.ts +0 -21
- package/dist/types/lib/validateRule.d.ts.map +0 -1
- package/dist/types/types.d.ts +0 -42
- package/dist/types/types.d.ts.map +0 -1
- package/dist/types/validate.d.ts +0 -23
- package/dist/types/validate.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,22 +1,47 @@
|
|
|
1
1
|
# Webarchitect
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Validates project architecture and configuration against rulesets. Run it to ensure packages follow organizational standards.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```bash
|
|
6
|
+
webarchitect library
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Built-in Rulesets
|
|
10
|
+
|
|
11
|
+
Pragma uses three rulesets:
|
|
12
|
+
|
|
13
|
+
| Ruleset | License | Use Case |
|
|
14
|
+
|---------|---------|----------|
|
|
15
|
+
| `library` | LGPL-3.0 | Packages consumed by other packages or applications |
|
|
16
|
+
| `tool` | GPL-3.0 | Compiled CLI tools with a build step |
|
|
17
|
+
| `tool-ts` | GPL-3.0 | TypeScript tools that run directly with Bun (no build) |
|
|
6
18
|
|
|
7
|
-
|
|
19
|
+
Each ruleset validates package.json structure, required scripts, TypeScript configuration, Biome setup, and license compliance.
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
## Quick Example
|
|
10
22
|
|
|
11
|
-
|
|
23
|
+
A library package includes this in its check script:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"scripts": {
|
|
28
|
+
"check:webarchitect": "webarchitect library"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
12
32
|
|
|
13
|
-
|
|
33
|
+
Running `bun run check:webarchitect` validates that the package has correct exports, required fields, and LGPL-3.0 license.
|
|
14
34
|
|
|
15
|
-
|
|
35
|
+
## What It Validates
|
|
16
36
|
|
|
17
|
-
|
|
37
|
+
Webarchitect checks that:
|
|
38
|
+
- `package.json` has required fields (name, version, type, module, types, exports)
|
|
39
|
+
- `biome.json` extends `@canonical/biome-config`
|
|
40
|
+
- `tsconfig.json` exists with proper configuration
|
|
41
|
+
- License matches the ruleset requirement
|
|
42
|
+
- Required scripts exist (build, check, test)
|
|
18
43
|
|
|
19
|
-
When
|
|
44
|
+
When validation fails, you get specific error messages explaining what's wrong and what's expected.
|
|
20
45
|
|
|
21
46
|
## How to install and run?
|
|
22
47
|
|
|
@@ -249,3 +274,4 @@ The programmatic API makes it easy to integrate webarchitect into existing build
|
|
|
249
274
|
- **IDE Integration** - No real-time validation feedback is available yet in code editors. Developers must run webarchitect manually or through build scripts to see validation results. Real-time diagnostics would require Language Server Protocol implementation or editor-specific plugins.
|
|
250
275
|
- **Svelte support** - No svelte-specific ruleset has been developed yet.
|
|
251
276
|
- **Error Code Granularity** - All validation failures return the same exit code (1) regardless of failure type. Missing files, invalid JSON syntax, and schema validation failures are not distinguished programmatically. This limits automated error handling and reporting capabilities.
|
|
277
|
+
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonical/webarchitect",
|
|
3
3
|
"description": "A tool to test the compliance with architecture specifications for packages and applications.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.0-experimental.0",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"module": "
|
|
7
|
-
"types": "
|
|
6
|
+
"module": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
8
|
"bin": {
|
|
9
|
-
"webarchitect": "
|
|
9
|
+
"webarchitect": "src/cli.ts"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
-
"
|
|
12
|
+
"src",
|
|
13
13
|
"rulesets"
|
|
14
14
|
],
|
|
15
15
|
"author": {
|
|
@@ -26,25 +26,27 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/canonical/ds25#readme",
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "
|
|
30
|
-
"
|
|
29
|
+
"build": "echo 'No build needed - runs directly from TypeScript'",
|
|
30
|
+
"build:all": "bun run build",
|
|
31
|
+
"check": "bun run check:biome && bun run check:ts && bun run check:webarchitect",
|
|
31
32
|
"check:fix": "bun run check:biome:fix && bun run check:ts",
|
|
32
33
|
"check:biome": "biome check",
|
|
33
34
|
"check:biome:fix": "biome check --write",
|
|
34
35
|
"check:ts": "tsc --noEmit",
|
|
35
|
-
"test": "echo 'No tests defined yet'"
|
|
36
|
+
"test": "echo 'No tests defined yet'",
|
|
37
|
+
"check:webarchitect": "webarchitect tool-ts"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
|
-
"@biomejs/biome": "2.
|
|
39
|
-
"@canonical/biome-config": "^0.
|
|
40
|
-
"@canonical/typescript-config-base": "^0.
|
|
40
|
+
"@biomejs/biome": "2.3.11",
|
|
41
|
+
"@canonical/biome-config": "^0.11.0",
|
|
42
|
+
"@canonical/typescript-config-base": "^0.11.0",
|
|
41
43
|
"@types/json-schema": "^7.0.15",
|
|
42
|
-
"typescript": "^5.
|
|
44
|
+
"typescript": "^5.9.3"
|
|
43
45
|
},
|
|
44
46
|
"dependencies": {
|
|
45
47
|
"ajv": "^8.17.1",
|
|
46
|
-
"chalk": "^5.
|
|
47
|
-
"commander": "^14.0.
|
|
48
|
+
"chalk": "^5.6.2",
|
|
49
|
+
"commander": "^14.0.2"
|
|
48
50
|
},
|
|
49
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "0e56eee8b9ff804c676811e1fe55e2716d11db5b"
|
|
50
52
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://github.com/webarchitect/schemas/main/schema.json",
|
|
3
|
+
"name": "library",
|
|
4
|
+
"extends": ["package"],
|
|
5
|
+
"package-license": {
|
|
6
|
+
"file": {
|
|
7
|
+
"name": "package.json",
|
|
8
|
+
"contains": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"license": {
|
|
12
|
+
"const": "LGPL-3.0"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"required": ["license"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
-
"package": {
|
|
37
|
+
"package-structure": {
|
|
38
38
|
"file": {
|
|
39
39
|
"name": "package.json",
|
|
40
40
|
"contains": {
|
|
@@ -60,9 +60,6 @@
|
|
|
60
60
|
"type": "array",
|
|
61
61
|
"contains": { "const": "dist" }
|
|
62
62
|
},
|
|
63
|
-
"license": {
|
|
64
|
-
"const": "GPL-3.0"
|
|
65
|
-
},
|
|
66
63
|
"scripts": {
|
|
67
64
|
"type": "object",
|
|
68
65
|
"properties": {
|
|
@@ -88,7 +85,6 @@
|
|
|
88
85
|
"module",
|
|
89
86
|
"types",
|
|
90
87
|
"files",
|
|
91
|
-
"license",
|
|
92
88
|
"scripts"
|
|
93
89
|
]
|
|
94
90
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://github.com/webarchitect/schemas/main/schema.json",
|
|
3
|
+
"name": "tool-ts",
|
|
4
|
+
"extends": ["base"],
|
|
5
|
+
"biome": {
|
|
6
|
+
"file": {
|
|
7
|
+
"name": "biome.json",
|
|
8
|
+
"contains": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"$schema": {
|
|
12
|
+
"type": "string"
|
|
13
|
+
},
|
|
14
|
+
"extends": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": { "const": "@canonical/biome-config" },
|
|
17
|
+
"minItems": 1,
|
|
18
|
+
"maxItems": 1
|
|
19
|
+
},
|
|
20
|
+
"files": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"includes": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": { "type": "string" }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"description": "File inclusion patterns only"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"required": ["extends"],
|
|
33
|
+
"additionalProperties": false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"package-structure": {
|
|
38
|
+
"file": {
|
|
39
|
+
"name": "package.json",
|
|
40
|
+
"contains": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"name": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"pattern": "^@canonical/"
|
|
46
|
+
},
|
|
47
|
+
"version": {
|
|
48
|
+
"type": "string"
|
|
49
|
+
},
|
|
50
|
+
"type": {
|
|
51
|
+
"const": "module"
|
|
52
|
+
},
|
|
53
|
+
"module": {
|
|
54
|
+
"const": "src/index.ts"
|
|
55
|
+
},
|
|
56
|
+
"types": {
|
|
57
|
+
"const": "src/index.ts"
|
|
58
|
+
},
|
|
59
|
+
"files": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"contains": { "const": "src" }
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"build": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Build command (can be no-op for TS-only packages)"
|
|
69
|
+
},
|
|
70
|
+
"test": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Test command (implementation flexible)"
|
|
73
|
+
},
|
|
74
|
+
"check:ts": {
|
|
75
|
+
"const": "tsc --noEmit"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"required": ["build", "test", "check:ts"]
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"required": [
|
|
82
|
+
"name",
|
|
83
|
+
"version",
|
|
84
|
+
"type",
|
|
85
|
+
"module",
|
|
86
|
+
"types",
|
|
87
|
+
"files",
|
|
88
|
+
"scripts"
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"package-license": {
|
|
94
|
+
"file": {
|
|
95
|
+
"name": "package.json",
|
|
96
|
+
"contains": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"license": {
|
|
100
|
+
"const": "GPL-3.0"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"required": ["license"]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://github.com/webarchitect/schemas/main/schema.json",
|
|
3
|
+
"name": "tool",
|
|
4
|
+
"extends": ["package"],
|
|
5
|
+
"package-license": {
|
|
6
|
+
"file": {
|
|
7
|
+
"name": "package.json",
|
|
8
|
+
"contains": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"license": {
|
|
12
|
+
"const": "GPL-3.0"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"required": ["license"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import type { ErrorObject } from "ajv";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { discoverAllRulesets } from "./lib/index.js";
|
|
6
|
+
import type { ValidationResult } from "./types.js";
|
|
7
|
+
import validate from "./validate.js";
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Formats AJV validation errors into human-readable messages.
|
|
13
|
+
* Groups errors by their property path and removes duplicate messages.
|
|
14
|
+
*
|
|
15
|
+
* @param errorsJson - JSON string containing an array of AJV ErrorObject instances
|
|
16
|
+
* @returns Array of formatted error messages, one per property path
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const errors = '[{"instancePath": "/name", "message": "must be string"}]';
|
|
21
|
+
* const formatted = formatAjvErrors(errors);
|
|
22
|
+
* // Returns: ["name: must be string"]
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function formatAjvErrors(errorsJson: string): string[] {
|
|
26
|
+
try {
|
|
27
|
+
const errors = JSON.parse(errorsJson) as ErrorObject[];
|
|
28
|
+
const errorsByPath = new Map<string, Set<string>>();
|
|
29
|
+
|
|
30
|
+
for (const error of errors) {
|
|
31
|
+
const path = error.instancePath || "root";
|
|
32
|
+
const message = error.message || "validation failed";
|
|
33
|
+
|
|
34
|
+
if (!errorsByPath.has(path)) {
|
|
35
|
+
errorsByPath.set(path, new Set());
|
|
36
|
+
}
|
|
37
|
+
// Use optional chaining for safety
|
|
38
|
+
errorsByPath.get(path)?.add(message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Array.from(errorsByPath.entries()).map(([path, messages]) => {
|
|
42
|
+
const property = path === "root" ? "file" : path.replace("/", "");
|
|
43
|
+
const messageList = Array.from(messages).join(", ");
|
|
44
|
+
return `${property}: ${messageList}`;
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
// Fallback if JSON parsing fails
|
|
48
|
+
return [errorsJson];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Formats validation results for terminal output with color coding.
|
|
54
|
+
* Displays failed validations first, followed by passed validations,
|
|
55
|
+
* and ends with a summary. In verbose mode, shows additional context
|
|
56
|
+
* including file contents and validation rules.
|
|
57
|
+
*
|
|
58
|
+
* @param results - Array of validation results to format
|
|
59
|
+
* @param verbose - Whether to show detailed information including file contents and rules
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const results = [
|
|
64
|
+
* { rule: "package.json", passed: true },
|
|
65
|
+
* { rule: "biome.json", passed: false, message: "File not found" }
|
|
66
|
+
* ];
|
|
67
|
+
* formatTerminalOutput(results, true);
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
function formatTerminalOutput(
|
|
71
|
+
results: ValidationResult[],
|
|
72
|
+
verbose = false,
|
|
73
|
+
): void {
|
|
74
|
+
console.log(); // Empty line for spacing
|
|
75
|
+
|
|
76
|
+
const passedResults = results.filter((r) => r.passed);
|
|
77
|
+
const failedResults = results.filter((r) => !r.passed);
|
|
78
|
+
|
|
79
|
+
if (failedResults.length > 0) {
|
|
80
|
+
console.log(chalk.red.bold("✗ FAILED VALIDATIONS"));
|
|
81
|
+
console.log(chalk.gray("─".repeat(50)));
|
|
82
|
+
|
|
83
|
+
for (const result of failedResults) {
|
|
84
|
+
console.log(chalk.red(`✗ ${result.rule}`));
|
|
85
|
+
|
|
86
|
+
if (verbose && result.context) {
|
|
87
|
+
console.log(chalk.gray(` Target: ${result.context.target}`));
|
|
88
|
+
if (result.context.description) {
|
|
89
|
+
console.log(chalk.gray(` Rule: ${result.context.description}`));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (result.message) {
|
|
94
|
+
// Check if message contains JSON (AJV errors)
|
|
95
|
+
if (result.message.includes("Validation failed: [")) {
|
|
96
|
+
const jsonStart = result.message.indexOf("[");
|
|
97
|
+
const prefix = result.message.substring(0, jsonStart).trim();
|
|
98
|
+
const jsonPart = result.message.substring(jsonStart);
|
|
99
|
+
|
|
100
|
+
if (prefix) {
|
|
101
|
+
console.log(chalk.gray(` ${prefix}`));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const formattedErrors = formatAjvErrors(jsonPart);
|
|
105
|
+
for (const error of formattedErrors) {
|
|
106
|
+
console.log(chalk.gray(` • ${error}`));
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
console.log(chalk.gray(` ${result.message}`));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
verbose &&
|
|
115
|
+
result.context?.value &&
|
|
116
|
+
result.context.value !== "[File not found]"
|
|
117
|
+
) {
|
|
118
|
+
// Use regular string instead of template literal where no interpolation needed
|
|
119
|
+
console.log(chalk.gray(" Found:"));
|
|
120
|
+
if (typeof result.context.value === "object") {
|
|
121
|
+
const preview = JSON.stringify(result.context.value, null, 2)
|
|
122
|
+
.split("\n")
|
|
123
|
+
.slice(0, 10) // Show first 10 lines
|
|
124
|
+
.map((line) => ` ${line}`)
|
|
125
|
+
.join("\n");
|
|
126
|
+
console.log(chalk.gray(preview));
|
|
127
|
+
if (JSON.stringify(result.context.value).split("\n").length > 10) {
|
|
128
|
+
console.log(chalk.gray(" ... (truncated)"));
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
console.log(chalk.gray(` ${result.context.value}`));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
console.log(); // Empty line between failures
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (passedResults.length > 0) {
|
|
139
|
+
console.log(chalk.green.bold("✓ PASSED VALIDATIONS"));
|
|
140
|
+
console.log(chalk.gray("─".repeat(50)));
|
|
141
|
+
|
|
142
|
+
for (const result of passedResults) {
|
|
143
|
+
console.log(chalk.green(`✓ ${result.rule}`));
|
|
144
|
+
|
|
145
|
+
if (verbose && result.context) {
|
|
146
|
+
console.log(chalk.gray(` Target: ${result.context.target}`));
|
|
147
|
+
if (result.context.description) {
|
|
148
|
+
console.log(chalk.gray(` Rule: ${result.context.description}`));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (result.context.value && typeof result.context.value === "object") {
|
|
152
|
+
// Use regular string instead of template literal
|
|
153
|
+
console.log(chalk.gray(" Validated content:"));
|
|
154
|
+
const preview = JSON.stringify(result.context.value, null, 2)
|
|
155
|
+
.split("\n")
|
|
156
|
+
.slice(0, 5) // Show fewer lines for passed validations
|
|
157
|
+
.map((line) => ` ${line}`)
|
|
158
|
+
.join("\n");
|
|
159
|
+
console.log(chalk.gray(preview));
|
|
160
|
+
if (JSON.stringify(result.context.value).split("\n").length > 5) {
|
|
161
|
+
console.log(chalk.gray(" ... (content validated successfully)"));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
console.log();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Summary
|
|
170
|
+
const total = results.length;
|
|
171
|
+
const failed = failedResults.length;
|
|
172
|
+
const passed = passedResults.length;
|
|
173
|
+
|
|
174
|
+
if (failed > 0) {
|
|
175
|
+
console.log(
|
|
176
|
+
chalk.red.bold(
|
|
177
|
+
`Summary: ${failed}/${total} validations failed (${passed} passed)`,
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
} else {
|
|
181
|
+
console.log(chalk.green.bold(`Summary: All ${total} validations passed`));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Formats validation results as JSON for programmatic consumption.
|
|
187
|
+
* Outputs a structured object with summary statistics and detailed results.
|
|
188
|
+
*
|
|
189
|
+
* @param results - Array of validation results to format
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* const results = [
|
|
194
|
+
* { rule: "package.json", passed: true },
|
|
195
|
+
* { rule: "biome.json", passed: false, message: "File not found" }
|
|
196
|
+
* ];
|
|
197
|
+
* formatJsonOutput(results);
|
|
198
|
+
* // Outputs: {
|
|
199
|
+
* // "summary": { "total": 2, "passed": 1, "failed": 1 },
|
|
200
|
+
* // "results": [...]
|
|
201
|
+
* // }
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
function formatJsonOutput(results: ValidationResult[]): void {
|
|
205
|
+
const output = {
|
|
206
|
+
summary: {
|
|
207
|
+
total: results.length,
|
|
208
|
+
passed: results.filter((r) => r.passed).length,
|
|
209
|
+
failed: results.filter((r) => !r.passed).length,
|
|
210
|
+
},
|
|
211
|
+
results: results.map((result) => ({
|
|
212
|
+
rule: result.rule,
|
|
213
|
+
passed: result.passed,
|
|
214
|
+
...(result.message && { message: result.message }),
|
|
215
|
+
})),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
console.log(JSON.stringify(output, null, 2));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Displays available rulesets in a tree-like format.
|
|
223
|
+
* Groups rulesets by their location (bundled vs current directory).
|
|
224
|
+
*/
|
|
225
|
+
async function displayRulesetTree(): Promise<void> {
|
|
226
|
+
const { bundled, local } = await discoverAllRulesets();
|
|
227
|
+
|
|
228
|
+
if (bundled.length === 0 && local.length === 0) {
|
|
229
|
+
console.log(chalk.yellow("No rulesets found"));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log(chalk.bold("\nAvailable Webarchitect Rulesets:"));
|
|
234
|
+
console.log(chalk.gray("─".repeat(50)));
|
|
235
|
+
|
|
236
|
+
// Display bundled rulesets
|
|
237
|
+
if (bundled.length > 0) {
|
|
238
|
+
console.log(chalk.cyan("Bundled Rulesets:"));
|
|
239
|
+
console.log(chalk.gray(`└── ${bundled[0].path.replace(/\/[^/]+$/, "")}/`));
|
|
240
|
+
for (let i = 0; i < bundled.length; i++) {
|
|
241
|
+
const isLast = i === bundled.length - 1;
|
|
242
|
+
const prefix = isLast ? "└──" : "├──";
|
|
243
|
+
const filename = `${bundled[i].name}.ruleset.json`;
|
|
244
|
+
console.log(
|
|
245
|
+
` ${prefix} ${chalk.bold(bundled[i].name)} ${chalk.gray(`(${filename})`)}`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Display current directory rulesets
|
|
251
|
+
if (bundled.length > 0) console.log(); // Add spacing between sections
|
|
252
|
+
console.log(chalk.green("Current Directory:"));
|
|
253
|
+
console.log(chalk.gray(`└── ${process.cwd()}/`));
|
|
254
|
+
if (local.length > 0) {
|
|
255
|
+
for (let i = 0; i < local.length; i++) {
|
|
256
|
+
const isLast = i === local.length - 1;
|
|
257
|
+
const prefix = isLast ? "└──" : "├──";
|
|
258
|
+
const filename = `${local[i].name}.ruleset.json`;
|
|
259
|
+
console.log(
|
|
260
|
+
` ${prefix} ${chalk.bold(local[i].name)} ${chalk.gray(`(${filename})`)}`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
console.log(chalk.gray(` └── (no rulesets found at this path)`));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
program
|
|
269
|
+
.name("webarchitect")
|
|
270
|
+
.argument("[ruleset]", "ruleset identifier, local path, or URL")
|
|
271
|
+
.option("-v, --verbose", "show all validation results")
|
|
272
|
+
.option("--json", "output results in JSON format")
|
|
273
|
+
.option("--list", "list all available rulesets")
|
|
274
|
+
.action(async (schemaArg, options) => {
|
|
275
|
+
// Handle --list option
|
|
276
|
+
if (options.list) {
|
|
277
|
+
await displayRulesetTree();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Require ruleset argument if not listing
|
|
282
|
+
if (!schemaArg) {
|
|
283
|
+
console.error(chalk.red("Error: Missing required argument 'ruleset'"));
|
|
284
|
+
console.error(
|
|
285
|
+
chalk.gray("Use 'webarchitect --list' to see available rulesets"),
|
|
286
|
+
);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const results = await validate(process.cwd(), schemaArg);
|
|
291
|
+
|
|
292
|
+
if (options.json) {
|
|
293
|
+
formatJsonOutput(results);
|
|
294
|
+
} else {
|
|
295
|
+
formatTerminalOutput(results, options.verbose);
|
|
296
|
+
|
|
297
|
+
// Exit with error code if any validations failed
|
|
298
|
+
const hasFailures = results.some((r) => !r.passed);
|
|
299
|
+
if (hasFailures) {
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch (e) {
|
|
304
|
+
if (options.json) {
|
|
305
|
+
console.log(
|
|
306
|
+
JSON.stringify(
|
|
307
|
+
{
|
|
308
|
+
error: (e as Error).message,
|
|
309
|
+
success: false,
|
|
310
|
+
},
|
|
311
|
+
null,
|
|
312
|
+
2,
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
} else {
|
|
316
|
+
console.error(chalk.red(`Error: ${(e as Error).message}`));
|
|
317
|
+
}
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
program.parse();
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
// Handle both source (src/) and built (dist/esm/) paths
|
|
5
|
+
const srcPath = join(import.meta.dirname, "../rulesets");
|
|
6
|
+
const distPath = join(import.meta.dirname, "../../rulesets");
|
|
7
|
+
|
|
8
|
+
export const BUNDLED_RULESETS_DIR = existsSync(srcPath) ? srcPath : distPath;
|