@web-hig/core 1.12.2
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/index.mjs +4 -0
- package/lib/config.mjs +99 -0
- package/lib/registry.mjs +55 -0
- package/lib/report.mjs +62 -0
- package/lib/resolve-root.mjs +19 -0
- package/package.json +22 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { loadRegistry, parseRegistryYaml, filterRulesByContract } from './lib/registry.mjs';
|
|
2
|
+
export { loadProjectConfig, defaultProjectConfig } from './lib/config.mjs';
|
|
3
|
+
export { createReport, printTerminalReport, severityBucketForRule } from './lib/report.mjs';
|
|
4
|
+
export { resolveHigRoot } from './lib/resolve-root.mjs';
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
profile: 'practical',
|
|
6
|
+
archetype: 'application',
|
|
7
|
+
framework: { name: null },
|
|
8
|
+
scope: { files: ['src/**'], exclude: [], routes: ['/**'] },
|
|
9
|
+
gates: { blocking: 'fail', warnings: 'report', observations: 'report' },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function defaultProjectConfig(higVersion) {
|
|
13
|
+
return {
|
|
14
|
+
version: higVersion,
|
|
15
|
+
...structuredClone(DEFAULTS),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseScalar(line) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
22
|
+
return trimmed.slice(1, -1);
|
|
23
|
+
}
|
|
24
|
+
return trimmed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Minimal web-hig.yaml parser (no dependency). */
|
|
28
|
+
export function loadProjectConfig(cwd, fileName = 'web-hig.yaml') {
|
|
29
|
+
const configPath = path.join(cwd, fileName);
|
|
30
|
+
if (!fs.existsSync(configPath)) {
|
|
31
|
+
return { configPath: null, config: null, exists: false };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const text = fs.readFileSync(configPath, 'utf8');
|
|
35
|
+
const config = structuredClone(DEFAULTS);
|
|
36
|
+
let version = null;
|
|
37
|
+
let section = null;
|
|
38
|
+
|
|
39
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
40
|
+
const line = rawLine.replace(/#.*$/, '').trimEnd();
|
|
41
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
42
|
+
|
|
43
|
+
if (/^scope:\s*$/.test(line)) {
|
|
44
|
+
section = 'scope';
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (/^framework:\s*$/.test(line)) {
|
|
48
|
+
section = 'framework';
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (/^gates:\s*$/.test(line)) {
|
|
52
|
+
section = 'gates';
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const top = line.match(/^(\w+):\s*(.*)$/);
|
|
57
|
+
if (top && !line.startsWith(' ')) {
|
|
58
|
+
section = null;
|
|
59
|
+
const [, key, value] = top;
|
|
60
|
+
if (key === 'version') version = parseScalar(value);
|
|
61
|
+
if (key === 'profile') config.profile = parseScalar(value);
|
|
62
|
+
if (key === 'archetype') config.archetype = parseScalar(value);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nested = line.match(/^\s{2}(\w+):\s*(.*)$/);
|
|
67
|
+
if (!nested) continue;
|
|
68
|
+
const [, key, value] = nested;
|
|
69
|
+
|
|
70
|
+
if (section === 'framework' && key === 'name') {
|
|
71
|
+
config.framework.name = parseScalar(value);
|
|
72
|
+
}
|
|
73
|
+
if (section === 'gates') {
|
|
74
|
+
config.gates[key] = parseScalar(value);
|
|
75
|
+
}
|
|
76
|
+
if (section === 'scope' && (key === 'files' || key === 'exclude' || key === 'routes')) {
|
|
77
|
+
if (!Array.isArray(config.scope[key])) config.scope[key] = [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const lines = text.split(/\r?\n/);
|
|
82
|
+
let scopeList = null;
|
|
83
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
84
|
+
const line = lines[i];
|
|
85
|
+
if (/^\s{2}files:\s*$/.test(line)) scopeList = 'files';
|
|
86
|
+
else if (/^\s{2}exclude:\s*$/.test(line)) scopeList = 'exclude';
|
|
87
|
+
else if (/^\s{2}routes:\s*$/.test(line)) scopeList = 'routes';
|
|
88
|
+
else if (/^\s{2}\w/.test(line) && !/^\s{4}-/.test(line)) scopeList = null;
|
|
89
|
+
else if (scopeList && /^\s{4}-\s+(.+)$/.test(line)) {
|
|
90
|
+
config.scope[scopeList].push(parseScalar(line.match(/^\s{4}-\s+(.+)$/)[1]));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
configPath,
|
|
96
|
+
exists: true,
|
|
97
|
+
config: { version, ...config },
|
|
98
|
+
};
|
|
99
|
+
}
|
package/lib/registry.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function parseRegistryYaml(yaml) {
|
|
5
|
+
const version = yaml.match(/^version:\s*"([^"]+)"/m)?.[1];
|
|
6
|
+
const rules = new Map();
|
|
7
|
+
for (const match of yaml.matchAll(/^ (HIG-[A-Z0-9]+-\d+):\r?\n((?: .+\r?\n)*)/gm)) {
|
|
8
|
+
const id = match[1];
|
|
9
|
+
const body = match[2];
|
|
10
|
+
const pick = (key) => body.match(new RegExp(`^ ${key}:\\s*(.+)$`, 'm'))?.[1]?.trim();
|
|
11
|
+
const profiles = [...body.matchAll(/^ profiles:\r?\n((?: - \S+\r?\n)*)/gm)][0];
|
|
12
|
+
const profileList = profiles
|
|
13
|
+
? [...profiles[1].matchAll(/^ - (\S+)/gm)].map((m) => m[1])
|
|
14
|
+
: [];
|
|
15
|
+
const archetypeBlock = [...body.matchAll(/^ archetypes:\r?\n((?: - \S+\r?\n)*)/gm)][0];
|
|
16
|
+
const archetypeList = archetypeBlock
|
|
17
|
+
? [...archetypeBlock[1].matchAll(/^ - (\S+)/gm)].map((m) => m[1])
|
|
18
|
+
: [];
|
|
19
|
+
const evalBlock = [...body.matchAll(/^ evaluation:\r?\n((?: - \S+\r?\n)*)/gm)][0];
|
|
20
|
+
const evaluationList = evalBlock
|
|
21
|
+
? [...evalBlock[1].matchAll(/^ - (\S+)/gm)].map((m) => m[1])
|
|
22
|
+
: [];
|
|
23
|
+
|
|
24
|
+
rules.set(id, {
|
|
25
|
+
id,
|
|
26
|
+
severity: pick('severity'),
|
|
27
|
+
requirement: pick('requirement')?.replace(/^"|"$/g, ''),
|
|
28
|
+
profiles: profileList,
|
|
29
|
+
archetypes: archetypeList,
|
|
30
|
+
evaluation: evaluationList,
|
|
31
|
+
autofix: pick('autofix'),
|
|
32
|
+
module: pick('module'),
|
|
33
|
+
hig_section: pick('hig_section')?.replace(/^"|"$/g, ''),
|
|
34
|
+
eslint_rule: pick('eslint_rule'),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return { version, rules };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function loadRegistry(higRoot) {
|
|
41
|
+
const filePath = path.join(higRoot, 'rules', 'registry.yaml');
|
|
42
|
+
const yaml = fs.readFileSync(filePath, 'utf8');
|
|
43
|
+
return parseRegistryYaml(yaml);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function filterRulesByContract(registry, { profile, archetype }) {
|
|
47
|
+
const applicable = [];
|
|
48
|
+
for (const rule of registry.rules.values()) {
|
|
49
|
+
if (!rule.profiles.includes(profile)) continue;
|
|
50
|
+
if (!rule.archetypes.includes(archetype)) continue;
|
|
51
|
+
if (!rule.evaluation.includes('static')) continue;
|
|
52
|
+
applicable.push(rule);
|
|
53
|
+
}
|
|
54
|
+
return applicable;
|
|
55
|
+
}
|
package/lib/report.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const SEVERITY_TO_BUCKET = {
|
|
2
|
+
error: 'blocking',
|
|
3
|
+
warning: 'warnings',
|
|
4
|
+
info: 'observations',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function severityBucketForRule(severity) {
|
|
8
|
+
return SEVERITY_TO_BUCKET[severity] ?? 'warnings';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createReport({
|
|
12
|
+
higVersion,
|
|
13
|
+
profile,
|
|
14
|
+
archetype,
|
|
15
|
+
framework,
|
|
16
|
+
findings = [],
|
|
17
|
+
}) {
|
|
18
|
+
const counts = { blocking: 0, warnings: 0, observations: 0 };
|
|
19
|
+
for (const finding of findings) {
|
|
20
|
+
counts[finding.severity_bucket] = (counts[finding.severity_bucket] ?? 0) + 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
hig_version: higVersion,
|
|
25
|
+
profile,
|
|
26
|
+
archetype,
|
|
27
|
+
framework: framework ?? null,
|
|
28
|
+
severity_counts: counts,
|
|
29
|
+
findings,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function printTerminalReport(report) {
|
|
34
|
+
const lines = [
|
|
35
|
+
`Web HIG v${report.hig_version}`,
|
|
36
|
+
'',
|
|
37
|
+
`Profile: ${capitalize(report.profile)}`,
|
|
38
|
+
`Archetype: ${capitalize(report.archetype)}`,
|
|
39
|
+
];
|
|
40
|
+
if (report.framework) lines.push(`Framework: ${capitalize(report.framework)}`);
|
|
41
|
+
lines.push(
|
|
42
|
+
'',
|
|
43
|
+
`BLOCKING ${report.severity_counts.blocking}`,
|
|
44
|
+
`WARNINGS ${report.severity_counts.warnings}`,
|
|
45
|
+
`OBSERVATIONS ${report.severity_counts.observations}`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (report.findings.length) {
|
|
49
|
+
lines.push('', 'Findings:');
|
|
50
|
+
for (const f of report.findings) {
|
|
51
|
+
const loc = f.location ? ` (${f.location})` : '';
|
|
52
|
+
lines.push(` [${f.rule_id}] ${f.message}${loc}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function capitalize(value) {
|
|
60
|
+
if (!value) return value;
|
|
61
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
62
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function resolveHigRoot(startDir = process.cwd()) {
|
|
5
|
+
if (process.env.WEB_HIG_ROOT) {
|
|
6
|
+
const root = path.resolve(process.env.WEB_HIG_ROOT);
|
|
7
|
+
if (fs.existsSync(path.join(root, 'rules', 'registry.yaml'))) return root;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let dir = path.resolve(startDir);
|
|
11
|
+
for (let i = 0; i < 12; i += 1) {
|
|
12
|
+
if (fs.existsSync(path.join(dir, 'rules', 'registry.yaml'))) return dir;
|
|
13
|
+
const parent = path.dirname(dir);
|
|
14
|
+
if (parent === dir) break;
|
|
15
|
+
dir = parent;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return null;
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@web-hig/core",
|
|
3
|
+
"version": "1.12.2",
|
|
4
|
+
"description": "Shared evaluator engine for The Web HIG conformance tooling",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"lib"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/frozonfreak/hig.git",
|
|
17
|
+
"directory": "packages/core"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
}
|
|
22
|
+
}
|