@refrakt-md/cli 0.35.0 → 0.36.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/dist/bin.js +142 -21
- package/dist/bin.js.map +1 -1
- package/dist/commands/config.d.ts +3 -2
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +110 -2
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/theme-validate.d.ts +23 -0
- package/dist/commands/theme-validate.d.ts.map +1 -0
- package/dist/commands/theme-validate.js +73 -0
- package/dist/commands/theme-validate.js.map +1 -0
- package/dist/commands/validate-config.d.ts +35 -0
- package/dist/commands/validate-config.d.ts.map +1 -0
- package/dist/commands/validate-config.js +123 -0
- package/dist/commands/validate-config.js.map +1 -0
- package/dist/commands/validate-core.d.ts +51 -0
- package/dist/commands/validate-core.d.ts.map +1 -0
- package/dist/commands/validate-core.js +113 -0
- package/dist/commands/validate-core.js.map +1 -0
- package/dist/commands/validate.d.ts +12 -2
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +62 -60
- package/dist/commands/validate.js.map +1 -1
- package/package.json +10 -6
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `refrakt theme validate` — the theme-authoring checks (SPEC-135 D12 /
|
|
3
|
+
* WORK-579).
|
|
4
|
+
*
|
|
5
|
+
* These moved off the bare `refrakt validate`, which now belongs to site
|
|
6
|
+
* authors. Three artifacts share the word "config" and only one of them is the
|
|
7
|
+
* site author's:
|
|
8
|
+
*
|
|
9
|
+
* | Artifact | Who writes it | Checked by |
|
|
10
|
+
* |---|---|---|
|
|
11
|
+
* | `refrakt.config.json` | site author | the published JSON Schema, and `refrakt config validate` |
|
|
12
|
+
* | `ThemeConfig` (`prefix`, rune configs) | theme author | `validateThemeConfig` — here |
|
|
13
|
+
* | Theme manifest | theme author | `validateManifest` — here |
|
|
14
|
+
*
|
|
15
|
+
* The `theme` group already holds `install`, `info` and `list`, so this slots
|
|
16
|
+
* in beside them rather than inventing a noun.
|
|
17
|
+
*/
|
|
18
|
+
export interface ThemeValidateOptions {
|
|
19
|
+
configPath?: string;
|
|
20
|
+
manifestPath?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function themeValidateCommand(opts: ThemeValidateOptions): void;
|
|
23
|
+
//# sourceMappingURL=theme-validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-validate.d.ts","sourceRoot":"","sources":["../../src/commands/theme-validate.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,oBAAoB,GAAG,IAAI,CAmDrE"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { validateThemeConfig, validateManifest, } from '@refrakt-md/transform';
|
|
4
|
+
export function themeValidateCommand(opts) {
|
|
5
|
+
let hasErrors = false;
|
|
6
|
+
// A validation command must never report success on nothing (SPEC-135 D2).
|
|
7
|
+
// The old behaviour here was the defect that rule was written for: handed no
|
|
8
|
+
// arguments, it validated refrakt's own `baseConfig` and printed a
|
|
9
|
+
// checkmark — a self-test of the library, reported as though it were a check
|
|
10
|
+
// of the user's work.
|
|
11
|
+
if (!opts.configPath && !opts.manifestPath) {
|
|
12
|
+
console.error('Nothing to validate.');
|
|
13
|
+
console.error('');
|
|
14
|
+
console.error(' refrakt theme validate --config <path> a ThemeConfig JSON file');
|
|
15
|
+
console.error(' refrakt theme validate --manifest <path> a theme manifest');
|
|
16
|
+
console.error('');
|
|
17
|
+
console.error('To validate a site’s content and config, use `refrakt validate`.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
if (opts.configPath) {
|
|
21
|
+
const absPath = resolve(opts.configPath);
|
|
22
|
+
if (!existsSync(absPath)) {
|
|
23
|
+
console.error(`Error: Config file not found: ${absPath}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
// For now, only JSON configs are supported via CLI.
|
|
27
|
+
// TypeScript configs require dynamic import which needs the module to be built.
|
|
28
|
+
const raw = JSON.parse(readFileSync(absPath, 'utf-8'));
|
|
29
|
+
console.log('Validating theme config...');
|
|
30
|
+
const result = validateThemeConfig(raw);
|
|
31
|
+
printResult('Theme config', result);
|
|
32
|
+
if (!result.valid)
|
|
33
|
+
hasErrors = true;
|
|
34
|
+
}
|
|
35
|
+
if (opts.manifestPath) {
|
|
36
|
+
const absPath = resolve(opts.manifestPath);
|
|
37
|
+
if (!existsSync(absPath)) {
|
|
38
|
+
console.error(`Error: Manifest file not found: ${absPath}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const raw = JSON.parse(readFileSync(absPath, 'utf-8'));
|
|
42
|
+
console.log(`${opts.configPath ? '\n' : ''}Validating manifest...`);
|
|
43
|
+
const result = validateManifest(raw);
|
|
44
|
+
printResult('Manifest', result);
|
|
45
|
+
if (!result.valid)
|
|
46
|
+
hasErrors = true;
|
|
47
|
+
}
|
|
48
|
+
if (hasErrors) {
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function printResult(label, result) {
|
|
53
|
+
if (result.valid && result.warnings.length === 0) {
|
|
54
|
+
console.log(` ${label}: OK`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (result.errors.length > 0) {
|
|
58
|
+
console.log(` ${label}: FAIL (${result.errors.length} error${result.errors.length === 1 ? '' : 's'})`);
|
|
59
|
+
for (const err of result.errors) {
|
|
60
|
+
console.log(` ERROR ${err.path}: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log(` ${label}: OK`);
|
|
65
|
+
}
|
|
66
|
+
if (result.warnings.length > 0) {
|
|
67
|
+
console.log(` ${result.warnings.length} warning${result.warnings.length === 1 ? '' : 's'}:`);
|
|
68
|
+
for (const warn of result.warnings) {
|
|
69
|
+
console.log(` WARN ${warn.path}: ${warn.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=theme-validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-validate.js","sourceRoot":"","sources":["../../src/commands/theme-validate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SiteConfig } from '@refrakt-md/types';
|
|
2
|
+
/**
|
|
3
|
+
* The config layer of `refrakt validate` (SPEC-135 D1 / WORK-578).
|
|
4
|
+
*
|
|
5
|
+
* **Checks resolution, not shape.** Shape-checking `refrakt.config.json` is the
|
|
6
|
+
* published JSON Schema's job and editors already do it. What nothing covered
|
|
7
|
+
* is whether the config's *names resolve*: does `theme.package` exist, does
|
|
8
|
+
* every entry in `plugins[]` load, do `routeRules` layout names exist, do
|
|
9
|
+
* `entityRoutes` types match a registered type. None of that is expressible in
|
|
10
|
+
* JSON Schema.
|
|
11
|
+
*
|
|
12
|
+
* It is not optional garnish. `packages/sveltekit/src/plugin.ts:166` catches a
|
|
13
|
+
* failed package load, warns to console, and continues with that plugin's runes
|
|
14
|
+
* **absent** — so post-SPEC-132 every use of them becomes a `tag-undefined`
|
|
15
|
+
* finding. Dozens of errors against content that is perfectly correct, cause:
|
|
16
|
+
* one line of config. Running this first, and letting it explain the findings it
|
|
17
|
+
* would cause, is what stops the symptom drowning the cause.
|
|
18
|
+
*/
|
|
19
|
+
export interface ConfigFinding {
|
|
20
|
+
severity: 'error' | 'warning' | 'info';
|
|
21
|
+
/** Dotted config path, e.g. `sites.main.plugins[2]`. */
|
|
22
|
+
path: string;
|
|
23
|
+
message: string;
|
|
24
|
+
/** Set when this failure is known to *cause* content findings — the CLI
|
|
25
|
+
* uses it to explain `tag-undefined` rather than reporting both as peers. */
|
|
26
|
+
causesMissingRunes?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface ValidateConfigOptions {
|
|
29
|
+
/** Directory to resolve `theme.package` and `plugins[]` from. */
|
|
30
|
+
configDir: string;
|
|
31
|
+
/** Site name, used only to path findings. */
|
|
32
|
+
siteName: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function validateSiteConfig(site: SiteConfig, opts: ValidateConfigOptions): ConfigFinding[];
|
|
35
|
+
//# sourceMappingURL=validate-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-config.d.ts","sourceRoot":"","sources":["../../src/commands/validate-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;kFAC8E;IAC9E,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACrC,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;CACjB;AA6BD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,qBAAqB,GAAG,aAAa,EAAE,CAwEjG"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve, isAbsolute } from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { getThemePackage } from '@refrakt-md/types';
|
|
5
|
+
/** Can this specifier be resolved from `configDir`? */
|
|
6
|
+
function canResolve(specifier, configDir) {
|
|
7
|
+
// A relative or absolute path is a file, not a package.
|
|
8
|
+
if (specifier.startsWith('.') || isAbsolute(specifier)) {
|
|
9
|
+
return existsSync(resolve(configDir, specifier));
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
// `createRequire` resolves the way Node would from this directory,
|
|
13
|
+
// including workspace symlinks — which is what the adapters do at build
|
|
14
|
+
// time, so a package that resolves here resolves there.
|
|
15
|
+
const req = createRequire(resolve(configDir, 'package.json'));
|
|
16
|
+
req.resolve(specifier);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// A package can be importable without a resolvable main entry (exports
|
|
21
|
+
// maps without a "." key). Fall back to asking whether the directory is
|
|
22
|
+
// there at all before calling it missing.
|
|
23
|
+
try {
|
|
24
|
+
const req = createRequire(resolve(configDir, 'package.json'));
|
|
25
|
+
req.resolve(`${specifier}/package.json`);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function validateSiteConfig(site, opts) {
|
|
34
|
+
const findings = [];
|
|
35
|
+
const base = `sites.${opts.siteName}`;
|
|
36
|
+
// theme.package — the theme supplies every rune config and the layouts, so
|
|
37
|
+
// an unresolvable one is not a partial failure.
|
|
38
|
+
if (site.theme !== undefined) {
|
|
39
|
+
const themePackage = getThemePackage(site.theme);
|
|
40
|
+
if (themePackage && !canResolve(themePackage, opts.configDir)) {
|
|
41
|
+
findings.push({
|
|
42
|
+
severity: 'error',
|
|
43
|
+
path: `${base}.theme`,
|
|
44
|
+
message: `theme package "${themePackage}" cannot be resolved from ${opts.configDir}`,
|
|
45
|
+
causesMissingRunes: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// plugins[] — the load-bearing one. A failed plugin load is swallowed at
|
|
50
|
+
// build time, and every rune it would have contributed becomes tag-undefined.
|
|
51
|
+
for (const [i, name] of (site.plugins ?? []).entries()) {
|
|
52
|
+
if (typeof name !== 'string' || !name) {
|
|
53
|
+
findings.push({
|
|
54
|
+
severity: 'error',
|
|
55
|
+
path: `${base}.plugins[${i}]`,
|
|
56
|
+
message: 'plugin entry must be a non-empty string',
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!canResolve(name, opts.configDir)) {
|
|
61
|
+
findings.push({
|
|
62
|
+
severity: 'error',
|
|
63
|
+
path: `${base}.plugins[${i}]`,
|
|
64
|
+
message: `plugin "${name}" cannot be resolved from ${opts.configDir} — every rune it contributes will be reported as an undefined tag`,
|
|
65
|
+
causesMissingRunes: true,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// routeRules[].layout — a layout name that matches nothing falls back
|
|
70
|
+
// silently, so a typo costs the page its intended layout with no signal.
|
|
71
|
+
const declaredLayouts = collectDeclaredLayouts(site);
|
|
72
|
+
for (const [i, rule] of (site.routeRules ?? []).entries()) {
|
|
73
|
+
const layout = rule.layout;
|
|
74
|
+
if (typeof layout !== 'string' || !layout)
|
|
75
|
+
continue;
|
|
76
|
+
// Only judge when we actually know the layout set. An unresolvable theme
|
|
77
|
+
// means we cannot enumerate layouts, and guessing would produce a second
|
|
78
|
+
// wave of errors whose real cause is already reported above.
|
|
79
|
+
if (declaredLayouts === undefined)
|
|
80
|
+
continue;
|
|
81
|
+
if (!declaredLayouts.has(layout)) {
|
|
82
|
+
findings.push({
|
|
83
|
+
severity: 'error',
|
|
84
|
+
path: `${base}.routeRules[${i}].layout`,
|
|
85
|
+
message: `layout "${layout}" is not declared by the theme (known: ${[...declaredLayouts].sort().join(', ') || 'none'})`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// entityRoutes[].type — a type nothing registers produces no pages at all,
|
|
90
|
+
// silently: the rule simply never matches.
|
|
91
|
+
for (const [i, rule] of (readEntityRoutes(site) ?? []).entries()) {
|
|
92
|
+
const type = rule.type;
|
|
93
|
+
if (typeof type !== 'string' || !type) {
|
|
94
|
+
findings.push({
|
|
95
|
+
severity: 'error',
|
|
96
|
+
path: `${base}.entityRoutes[${i}].type`,
|
|
97
|
+
message: 'entityRoutes rule must declare a non-empty `type`',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return findings;
|
|
102
|
+
}
|
|
103
|
+
/** Layout names the site's theme declares, or `undefined` when the theme could
|
|
104
|
+
* not be read — in which case the caller must not judge layout references. */
|
|
105
|
+
function collectDeclaredLayouts(site) {
|
|
106
|
+
const theme = site.theme;
|
|
107
|
+
if (typeof theme === 'object' && theme !== null) {
|
|
108
|
+
const layouts = theme.layouts;
|
|
109
|
+
if (layouts && typeof layouts === 'object')
|
|
110
|
+
return new Set(Object.keys(layouts));
|
|
111
|
+
}
|
|
112
|
+
// The theme's layout set lives in its manifest / layout config, which
|
|
113
|
+
// resolving here would mean importing the theme package. Deliberately not
|
|
114
|
+
// done: it is the expensive half, and a wrong `layout` name is caught by the
|
|
115
|
+
// layout cascade at build time anyway. Returning undefined suppresses the
|
|
116
|
+
// check rather than guessing.
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
function readEntityRoutes(site) {
|
|
120
|
+
const raw = site.entityRoutes;
|
|
121
|
+
return Array.isArray(raw) ? raw : undefined;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=validate-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-config.js","sourceRoot":"","sources":["../../src/commands/validate-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The validation run itself — shared by `refrakt validate` and the
|
|
3
|
+
* `refrakt.validate` MCP tool (SPEC-135 D6 / WORK-581).
|
|
4
|
+
*
|
|
5
|
+
* Returns data. It never prints and never calls `process.exit`, so the CLI can
|
|
6
|
+
* format and gate while MCP returns structured findings from the same code.
|
|
7
|
+
* The alternative — an MCP tool that shells out to the CLI and parses its
|
|
8
|
+
* output — would be a worse CLI, and would put a second copy of the layering
|
|
9
|
+
* rules somewhere they could drift.
|
|
10
|
+
*/
|
|
11
|
+
import { type ConfigFinding } from './validate-config.js';
|
|
12
|
+
export interface ValidateRunOptions {
|
|
13
|
+
/** Restrict to one site. Omitted means every site in the config. */
|
|
14
|
+
site?: string;
|
|
15
|
+
/** Narrow to one layer. Both run when omitted. */
|
|
16
|
+
only?: 'content' | 'config';
|
|
17
|
+
/** Run the cross-page tier — a full pipeline run (SPEC-135 D3). */
|
|
18
|
+
deep?: boolean;
|
|
19
|
+
/** Path to `refrakt.config.json`. Default: `<cwd>/refrakt.config.json`. */
|
|
20
|
+
configPath?: string;
|
|
21
|
+
/** Directory to resolve a relative `configPath` against. Default: `cwd`. */
|
|
22
|
+
cwd?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ContentFindingOut {
|
|
25
|
+
file: string;
|
|
26
|
+
url: string;
|
|
27
|
+
line?: number;
|
|
28
|
+
severity: 'error' | 'warning' | 'info';
|
|
29
|
+
id?: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
export interface SiteResult {
|
|
33
|
+
site: string;
|
|
34
|
+
config: ConfigFinding[];
|
|
35
|
+
content: ContentFindingOut[];
|
|
36
|
+
/** Set when the config layer found a failure that makes content findings
|
|
37
|
+
* untrustworthy, so a report can say so rather than listing both as peers. */
|
|
38
|
+
contentSuppressed?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ValidateRunResult {
|
|
41
|
+
/** Whole-run failure — no config, unparseable config, unknown site. When
|
|
42
|
+
* set, `sites` is empty and the run validated nothing (D2). */
|
|
43
|
+
error?: string;
|
|
44
|
+
sites: SiteResult[];
|
|
45
|
+
/** Path the run resolved its config from, for a caller that wants to say so. */
|
|
46
|
+
configPath: string;
|
|
47
|
+
}
|
|
48
|
+
/** True when anything in the run is at error severity. The gate (D4). */
|
|
49
|
+
export declare function hasErrors(result: ValidateRunResult): boolean;
|
|
50
|
+
export declare function runValidation(opts?: ValidateRunOptions): Promise<ValidateRunResult>;
|
|
51
|
+
//# sourceMappingURL=validate-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-core.d.ts","sourceRoot":"","sources":["../../src/commands/validate-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE9E,MAAM,WAAW,kBAAkB;IAClC,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,mEAAmE;IACnE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B;mFAC+E;IAC/E,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IACjC;oEACgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,yEAAyE;AACzE,wBAAgB,SAAS,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAM5D;AAED,wBAAsB,aAAa,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAqG7F"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The validation run itself — shared by `refrakt validate` and the
|
|
3
|
+
* `refrakt.validate` MCP tool (SPEC-135 D6 / WORK-581).
|
|
4
|
+
*
|
|
5
|
+
* Returns data. It never prints and never calls `process.exit`, so the CLI can
|
|
6
|
+
* format and gate while MCP returns structured findings from the same code.
|
|
7
|
+
* The alternative — an MCP tool that shells out to the CLI and parses its
|
|
8
|
+
* output — would be a worse CLI, and would put a second copy of the layering
|
|
9
|
+
* rules somewhere they could drift.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { normalizeRefraktConfig } from '@refrakt-md/transform/node';
|
|
14
|
+
import { validateSiteConfig } from './validate-config.js';
|
|
15
|
+
/** True when anything in the run is at error severity. The gate (D4). */
|
|
16
|
+
export function hasErrors(result) {
|
|
17
|
+
if (result.error)
|
|
18
|
+
return true;
|
|
19
|
+
return result.sites.some((r) => r.config.some((f) => f.severity === 'error') || r.content.some((f) => f.severity === 'error'));
|
|
20
|
+
}
|
|
21
|
+
export async function runValidation(opts = {}) {
|
|
22
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
23
|
+
const configPath = resolve(cwd, opts.configPath ?? 'refrakt.config.json');
|
|
24
|
+
// D2 — a validation command must never report success on nothing. Each of
|
|
25
|
+
// these returns an error rather than an empty pass, so a caller cannot read
|
|
26
|
+
// "validated nothing" as green.
|
|
27
|
+
if (!existsSync(configPath)) {
|
|
28
|
+
return {
|
|
29
|
+
error: `No refrakt.config.json found at ${configPath}. Nothing to validate.`,
|
|
30
|
+
sites: [],
|
|
31
|
+
configPath,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return {
|
|
40
|
+
error: `Could not parse ${configPath}: ${err.message}`,
|
|
41
|
+
sites: [],
|
|
42
|
+
configPath,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const configDir = dirname(configPath);
|
|
46
|
+
let sites;
|
|
47
|
+
try {
|
|
48
|
+
sites = normalizeRefraktConfig(raw, { configDir }).sites;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return {
|
|
52
|
+
error: `${configPath} is not a valid refrakt config: ${err.message}`,
|
|
53
|
+
sites: [],
|
|
54
|
+
configPath,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (opts.site && !sites[opts.site]) {
|
|
58
|
+
return {
|
|
59
|
+
error: `No site "${opts.site}" in ${configPath}. Declared: ${Object.keys(sites).join(', ') || 'none'}`,
|
|
60
|
+
sites: [],
|
|
61
|
+
configPath,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const names = opts.site ? [opts.site] : Object.keys(sites);
|
|
65
|
+
if (names.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
error: `${configPath} declares no sites. Nothing to validate.`,
|
|
68
|
+
sites: [],
|
|
69
|
+
configPath,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const name of names) {
|
|
74
|
+
const site = sites[name];
|
|
75
|
+
const result = { site: name, config: [], content: [] };
|
|
76
|
+
// D1 — config resolution runs FIRST, because its failures cause content
|
|
77
|
+
// findings. A plugin that fails to load takes its runes with it, and
|
|
78
|
+
// every use of them becomes `tag-undefined`.
|
|
79
|
+
if (opts.only !== 'content') {
|
|
80
|
+
result.config = validateSiteConfig(site, { configDir, siteName: name });
|
|
81
|
+
}
|
|
82
|
+
if (opts.only !== 'config') {
|
|
83
|
+
// Computed even under `--only content`, so the suppression rule can
|
|
84
|
+
// still explain itself. Just not reported.
|
|
85
|
+
const resolution = opts.only === 'content'
|
|
86
|
+
? validateSiteConfig(site, { configDir, siteName: name })
|
|
87
|
+
: result.config;
|
|
88
|
+
const breaking = resolution.filter((f) => f.causesMissingRunes);
|
|
89
|
+
if (breaking.length > 0) {
|
|
90
|
+
// Suppress rather than report both as peers (D1). Validating here
|
|
91
|
+
// produces dozens of `tag-undefined` errors against correct content
|
|
92
|
+
// whose real cause is one line of config — the symptom drowning the
|
|
93
|
+
// cause.
|
|
94
|
+
result.contentSuppressed =
|
|
95
|
+
`skipped: ${breaking.length} unresolved ${breaking.length === 1 ? 'dependency' : 'dependencies'} ` +
|
|
96
|
+
`would report every affected rune as an undefined tag. Fix the config findings above first.`;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const { createRefraktLoader } = await import('@refrakt-md/content');
|
|
100
|
+
const loader = createRefraktLoader({
|
|
101
|
+
configPath,
|
|
102
|
+
site: name,
|
|
103
|
+
// The build summary is not this command's output. Findings are.
|
|
104
|
+
reporter: () => { },
|
|
105
|
+
});
|
|
106
|
+
result.content = await loader.validateSite({ deep: opts.deep });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
results.push(result);
|
|
110
|
+
}
|
|
111
|
+
return { sites: results, configPath };
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=validate-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-core.js","sourceRoot":"","sources":["../../src/commands/validate-core.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `refrakt validate` — the site author's command (SPEC-135 D1 / WORK-578).
|
|
3
|
+
*
|
|
4
|
+
* Presentation only. The run itself lives in `validate-core.ts`, which returns
|
|
5
|
+
* data and never exits, so the `refrakt.validate` MCP tool can call the same
|
|
6
|
+
* code and return structured findings instead of parsing this output (D6).
|
|
7
|
+
*/
|
|
1
8
|
export interface ValidateOptions {
|
|
9
|
+
site?: string;
|
|
10
|
+
only?: 'content' | 'config';
|
|
11
|
+
deep?: boolean;
|
|
12
|
+
format?: 'text' | 'json';
|
|
2
13
|
configPath?: string;
|
|
3
|
-
manifestPath?: string;
|
|
4
14
|
}
|
|
5
|
-
export declare function validateCommand(opts: ValidateOptions): void
|
|
15
|
+
export declare function validateCommand(opts: ValidateOptions): Promise<void>;
|
|
6
16
|
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,eAAe;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA6D1E"}
|
|
@@ -1,70 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `refrakt validate` — the site author's command (SPEC-135 D1 / WORK-578).
|
|
3
|
+
*
|
|
4
|
+
* Presentation only. The run itself lives in `validate-core.ts`, which returns
|
|
5
|
+
* data and never exits, so the `refrakt.validate` MCP tool can call the same
|
|
6
|
+
* code and return structured findings instead of parsing this output (D6).
|
|
7
|
+
*/
|
|
8
|
+
import { hasErrors, runValidation } from './validate-core.js';
|
|
9
|
+
export async function validateCommand(opts) {
|
|
10
|
+
const result = await runValidation({
|
|
11
|
+
site: opts.site,
|
|
12
|
+
only: opts.only,
|
|
13
|
+
deep: opts.deep,
|
|
14
|
+
configPath: opts.configPath,
|
|
15
|
+
});
|
|
16
|
+
if (result.error) {
|
|
17
|
+
if (opts.format === 'json') {
|
|
18
|
+
process.stdout.write(`${JSON.stringify({ error: result.error, sites: [] }, null, 2)}\n`);
|
|
13
19
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const raw = JSON.parse(readFileSync(absPath, 'utf-8'));
|
|
17
|
-
console.log('Validating theme config...');
|
|
18
|
-
const result = validateThemeConfig(raw);
|
|
19
|
-
printResult('Theme config', result);
|
|
20
|
-
if (!result.valid)
|
|
21
|
-
hasErrors = true;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
// Validate the base config as a sanity check
|
|
25
|
-
console.log('Validating base theme config...');
|
|
26
|
-
const result = validateThemeConfig(baseConfig);
|
|
27
|
-
printResult('Base theme config', result);
|
|
28
|
-
if (!result.valid)
|
|
29
|
-
hasErrors = true;
|
|
30
|
-
}
|
|
31
|
-
// Validate manifest
|
|
32
|
-
if (opts.manifestPath) {
|
|
33
|
-
const absPath = resolve(opts.manifestPath);
|
|
34
|
-
if (!existsSync(absPath)) {
|
|
35
|
-
console.error(`Error: Manifest file not found: ${absPath}`);
|
|
36
|
-
process.exit(1);
|
|
20
|
+
else {
|
|
21
|
+
process.stderr.write(`${result.error}\n`);
|
|
37
22
|
}
|
|
38
|
-
const raw = JSON.parse(readFileSync(absPath, 'utf-8'));
|
|
39
|
-
console.log('\nValidating manifest...');
|
|
40
|
-
const result = validateManifest(raw);
|
|
41
|
-
printResult('Manifest', result);
|
|
42
|
-
if (!result.valid)
|
|
43
|
-
hasErrors = true;
|
|
44
|
-
}
|
|
45
|
-
if (hasErrors) {
|
|
46
23
|
process.exit(1);
|
|
47
24
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
console.log(` ${label}: OK`);
|
|
52
|
-
return;
|
|
25
|
+
if (opts.format === 'json') {
|
|
26
|
+
process.stdout.write(`${JSON.stringify({ sites: result.sites }, null, 2)}\n`);
|
|
27
|
+
process.exit(hasErrors(result) ? 1 : 0);
|
|
53
28
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
29
|
+
let totalErrors = 0;
|
|
30
|
+
let totalWarnings = 0;
|
|
31
|
+
for (const r of result.sites) {
|
|
32
|
+
process.stdout.write(`\n${r.site}\n`);
|
|
33
|
+
if (opts.only !== 'content') {
|
|
34
|
+
if (r.config.length === 0) {
|
|
35
|
+
process.stdout.write(' config: OK\n');
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
for (const f of r.config) {
|
|
39
|
+
process.stdout.write(` ${icon(f.severity)} ${f.path}: ${f.message}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
58
42
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
43
|
+
if (opts.only !== 'config') {
|
|
44
|
+
if (r.contentSuppressed) {
|
|
45
|
+
process.stdout.write(` content: ${r.contentSuppressed}\n`);
|
|
46
|
+
}
|
|
47
|
+
else if (r.content.length === 0) {
|
|
48
|
+
process.stdout.write(' content: OK\n');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
for (const f of r.content) {
|
|
52
|
+
const at = f.line ? `:${f.line}` : '';
|
|
53
|
+
process.stdout.write(` ${icon(f.severity)} ${f.file}${at} ${f.message}\n`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
67
56
|
}
|
|
57
|
+
totalErrors += count(r, 'error');
|
|
58
|
+
totalWarnings += count(r, 'warning');
|
|
68
59
|
}
|
|
60
|
+
const status = totalErrors > 0 ? '✗' : '✓';
|
|
61
|
+
process.stdout.write(`\n ${status} ${totalErrors} error${totalErrors === 1 ? '' : 's'}, ` +
|
|
62
|
+
`${totalWarnings} warning${totalWarnings === 1 ? '' : 's'}\n\n`);
|
|
63
|
+
process.exit(totalErrors > 0 ? 1 : 0);
|
|
64
|
+
}
|
|
65
|
+
function count(r, severity) {
|
|
66
|
+
return (r.config.filter((f) => f.severity === severity).length +
|
|
67
|
+
r.content.filter((f) => f.severity === severity).length);
|
|
68
|
+
}
|
|
69
|
+
function icon(severity) {
|
|
70
|
+
return severity === 'error' ? '✗ error ' : severity === 'warning' ? '⚠ warn ' : 'ℹ info ';
|
|
69
71
|
}
|
|
70
72
|
//# sourceMappingURL=validate.js.map
|