@rokkit/cli 1.3.11 → 1.3.13
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/src/checks.d.ts +17 -0
- package/dist/src/doctor.d.ts +3 -51
- package/dist/src/fix.d.ts +28 -0
- package/dist/src/report.d.ts +13 -0
- package/dist/src/validate.d.ts +33 -0
- package/package.json +2 -2
- package/src/checks.js +146 -0
- package/src/doctor.js +16 -471
- package/src/fix.js +176 -0
- package/src/report.js +79 -0
- package/src/validate.js +89 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run all doctor checks against the given filesystem adapter.
|
|
3
|
+
* @param {{ exists: (p: string) => boolean, read: (p: string) => string, resolve: (p: string) => string }} fs
|
|
4
|
+
* @returns {Array<{ id: string, label: string, status: 'pass'|'fail', fixable: boolean, fix: string, autoFix?: string }>}
|
|
5
|
+
*/
|
|
6
|
+
export function runChecks(fs: {
|
|
7
|
+
exists: (p: string) => boolean;
|
|
8
|
+
read: (p: string) => string;
|
|
9
|
+
resolve: (p: string) => string;
|
|
10
|
+
}): Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
status: "pass" | "fail";
|
|
14
|
+
fixable: boolean;
|
|
15
|
+
fix: string;
|
|
16
|
+
autoFix?: string;
|
|
17
|
+
}>;
|
package/dist/src/doctor.d.ts
CHANGED
|
@@ -1,54 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build the default named-token starter config source used by `doctor --fix`.
|
|
3
|
-
* @returns {string}
|
|
4
|
-
*/
|
|
5
|
-
export function defaultStarterSource(): string;
|
|
6
|
-
/**
|
|
7
|
-
* Run all doctor checks against the given filesystem adapter.
|
|
8
|
-
* @param {{ exists: (p: string) => boolean, read: (p: string) => string, resolve: (p: string) => string }} fs
|
|
9
|
-
* @returns {Array<{ id: string, label: string, status: 'pass'|'fail', fixable: boolean, fix: string, autoFix?: string }>}
|
|
10
|
-
*/
|
|
11
|
-
export function runChecks(fs: {
|
|
12
|
-
exists: (p: string) => boolean;
|
|
13
|
-
read: (p: string) => string;
|
|
14
|
-
resolve: (p: string) => string;
|
|
15
|
-
}): Array<{
|
|
16
|
-
id: string;
|
|
17
|
-
label: string;
|
|
18
|
-
status: "pass" | "fail";
|
|
19
|
-
fixable: boolean;
|
|
20
|
-
fix: string;
|
|
21
|
-
autoFix?: string;
|
|
22
|
-
}>;
|
|
23
|
-
/**
|
|
24
|
-
* Validate a parsed rokkit config for the named-token system. All advisory (warn).
|
|
25
|
-
* @param {Record<string, unknown> | null} config
|
|
26
|
-
* @returns {Array<{ id: string, label: string, status: 'warn', fixable: false, fix: string }>}
|
|
27
|
-
*/
|
|
28
|
-
export function validateConfigShape(config: Record<string, unknown> | null): Array<{
|
|
29
|
-
id: string;
|
|
30
|
-
label: string;
|
|
31
|
-
status: "warn";
|
|
32
|
-
fixable: false;
|
|
33
|
-
fix: string;
|
|
34
|
-
}>;
|
|
35
|
-
/**
|
|
36
|
-
* Advisory lint: scan src/ for surface/border tokens used as text colour.
|
|
37
|
-
* @param {{ list?: (dir: string) => string[], exists: (p: string) => boolean, read: (p: string) => string, resolve: (p: string) => string }} fs
|
|
38
|
-
* @returns {Array<{ id: string, label: string, status: 'warn', fixable: false, fix: string }>}
|
|
39
|
-
*/
|
|
40
|
-
export function checkTextTokenUsage(fs: {
|
|
41
|
-
list?: (dir: string) => string[];
|
|
42
|
-
exists: (p: string) => boolean;
|
|
43
|
-
read: (p: string) => string;
|
|
44
|
-
resolve: (p: string) => string;
|
|
45
|
-
}): Array<{
|
|
46
|
-
id: string;
|
|
47
|
-
label: string;
|
|
48
|
-
status: "warn";
|
|
49
|
-
fixable: false;
|
|
50
|
-
fix: string;
|
|
51
|
-
}>;
|
|
52
1
|
/**
|
|
53
2
|
* Interactive doctor command — validates project setup.
|
|
54
3
|
* @param {{ fix?: boolean }} [opts]
|
|
@@ -56,3 +5,6 @@ export function checkTextTokenUsage(fs: {
|
|
|
56
5
|
export function doctor(opts?: {
|
|
57
6
|
fix?: boolean;
|
|
58
7
|
}): Promise<void>;
|
|
8
|
+
export { runChecks } from "./checks.js";
|
|
9
|
+
export { defaultStarterSource } from "./fix.js";
|
|
10
|
+
export { validateConfigShape, checkTextTokenUsage } from "./validate.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the default named-token starter config source used by `doctor --fix`.
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function defaultStarterSource(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Create a real filesystem adapter rooted at cwd.
|
|
8
|
+
* @param {string} cwd
|
|
9
|
+
*/
|
|
10
|
+
export function createFsAdapter(cwd: string): {
|
|
11
|
+
exists: (p: any) => boolean;
|
|
12
|
+
read: (p: any) => string;
|
|
13
|
+
resolve: (p: any) => string;
|
|
14
|
+
list: (dir: any) => string[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Auto-fix failed checks that are marked fixable.
|
|
18
|
+
* @param {Array<{ id: string, status: string, fixable: boolean, autoFix?: string, label: string }>} checks
|
|
19
|
+
* @param {string} cwd
|
|
20
|
+
* @returns {number} number of fixed items
|
|
21
|
+
*/
|
|
22
|
+
export function autoFix(checks: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
status: string;
|
|
25
|
+
fixable: boolean;
|
|
26
|
+
autoFix?: string;
|
|
27
|
+
label: string;
|
|
28
|
+
}>, cwd: string): number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Count failures and print status for each check.
|
|
3
|
+
* @param {Array} checks
|
|
4
|
+
* @returns {number}
|
|
5
|
+
*/
|
|
6
|
+
export function printChecks(checks: any[]): number;
|
|
7
|
+
/**
|
|
8
|
+
* @param {Array} checks
|
|
9
|
+
* @param {string} cwd
|
|
10
|
+
* @param {number} failures
|
|
11
|
+
* @param {boolean} fix
|
|
12
|
+
*/
|
|
13
|
+
export function handleResults(checks: any[], cwd: string, failures: number, fix: boolean): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advisory validation run by `doctor`: config shape rules for the named-token
|
|
3
|
+
* system, and a scan for surface/border tokens misused as text colour.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validate a parsed rokkit config for the named-token system. All advisory (warn).
|
|
7
|
+
* @param {Record<string, unknown> | null} config
|
|
8
|
+
* @returns {Array<{ id: string, label: string, status: 'warn', fixable: false, fix: string }>}
|
|
9
|
+
*/
|
|
10
|
+
export function validateConfigShape(config: Record<string, unknown> | null): Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
status: "warn";
|
|
14
|
+
fixable: false;
|
|
15
|
+
fix: string;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Advisory lint: scan src/ for surface/border tokens used as text colour.
|
|
19
|
+
* @param {{ list?: (dir: string) => string[], exists: (p: string) => boolean, read: (p: string) => string, resolve: (p: string) => string }} fs
|
|
20
|
+
* @returns {Array<{ id: string, label: string, status: 'warn', fixable: false, fix: string }>}
|
|
21
|
+
*/
|
|
22
|
+
export function checkTextTokenUsage(fs: {
|
|
23
|
+
list?: (dir: string) => string[];
|
|
24
|
+
exists: (p: string) => boolean;
|
|
25
|
+
read: (p: string) => string;
|
|
26
|
+
resolve: (p: string) => string;
|
|
27
|
+
}): Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
status: "warn";
|
|
31
|
+
fixable: false;
|
|
32
|
+
fix: string;
|
|
33
|
+
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.13",
|
|
4
4
|
"description": "Command line utilities for icon bundling and conversion.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@mdi/svg": "^7.4.47",
|
|
30
|
-
"@rokkit/unocss": "1.3.
|
|
30
|
+
"@rokkit/unocss": "1.3.13",
|
|
31
31
|
"sade": "^1.8.1",
|
|
32
32
|
"@iconify/tools": "5.0.3",
|
|
33
33
|
"@iconify/types": "^2.0.0",
|
package/src/checks.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `doctor` checks: one function per project-setup requirement, plus
|
|
3
|
+
* `runChecks` which runs them all against a filesystem adapter.
|
|
4
|
+
*/
|
|
5
|
+
import { generateUnoConfig } from './init.js'
|
|
6
|
+
|
|
7
|
+
const KNOWN_THEMES = [
|
|
8
|
+
'rokkit',
|
|
9
|
+
'minimal',
|
|
10
|
+
'material',
|
|
11
|
+
'frosted',
|
|
12
|
+
'zen-sumi'
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check config file existence
|
|
17
|
+
* @param {Object} fs
|
|
18
|
+
* @returns {{ id: string, label: string, status: string, fixable: boolean, fix: string, autoFix: string }}
|
|
19
|
+
*/
|
|
20
|
+
function checkConfig(fs) {
|
|
21
|
+
const configExists = fs.exists(fs.resolve('rokkit.config.js'))
|
|
22
|
+
return {
|
|
23
|
+
id: 'config-exists',
|
|
24
|
+
label: 'rokkit.config.js exists',
|
|
25
|
+
status: configExists ? 'pass' : 'fail',
|
|
26
|
+
fixable: true,
|
|
27
|
+
fix: 'Run `rokkit init` to generate config',
|
|
28
|
+
autoFix: 'generate-config'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check uno.config.js uses presetRokkit
|
|
34
|
+
* @param {Object} fs
|
|
35
|
+
* @returns {Object}
|
|
36
|
+
*/
|
|
37
|
+
function checkUnoPreset(fs) {
|
|
38
|
+
const unoPath = fs.resolve('uno.config.js')
|
|
39
|
+
const unoExists = fs.exists(unoPath)
|
|
40
|
+
const unoContent = unoExists ? fs.read(unoPath) : ''
|
|
41
|
+
const unoUsesPreset = unoContent.includes('presetRokkit') && unoContent.includes('rokkit.config')
|
|
42
|
+
return {
|
|
43
|
+
id: 'uno-uses-preset',
|
|
44
|
+
label: 'uno.config.js uses presetRokkit(config)',
|
|
45
|
+
status: unoUsesPreset ? 'pass' : 'fail',
|
|
46
|
+
fixable: false,
|
|
47
|
+
fix: unoExists
|
|
48
|
+
? `Replace uno.config.js contents with:\n${generateUnoConfig()}`
|
|
49
|
+
: `Create uno.config.js with:\n${generateUnoConfig()}`
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check app.css has theme imports
|
|
55
|
+
* @param {Object} fs
|
|
56
|
+
* @returns {Object}
|
|
57
|
+
*/
|
|
58
|
+
function checkCssImports(fs) {
|
|
59
|
+
const cssPath = fs.resolve('src/app.css')
|
|
60
|
+
const cssExists = fs.exists(cssPath)
|
|
61
|
+
const cssHasBase = cssExists && fs.read(cssPath).includes('@rokkit/themes/base.css')
|
|
62
|
+
return {
|
|
63
|
+
id: 'css-imports',
|
|
64
|
+
label: 'app.css imports @rokkit/themes/base.css',
|
|
65
|
+
status: cssHasBase ? 'pass' : 'fail',
|
|
66
|
+
fixable: true,
|
|
67
|
+
fix: 'Append base theme import to src/app.css',
|
|
68
|
+
autoFix: 'patch-css'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check app.css has at least one theme style
|
|
74
|
+
* @param {Object} fs
|
|
75
|
+
* @returns {Object}
|
|
76
|
+
*/
|
|
77
|
+
function checkCssTheme(fs) {
|
|
78
|
+
const cssPath = fs.resolve('src/app.css')
|
|
79
|
+
const cssExists = fs.exists(cssPath)
|
|
80
|
+
const css = cssExists ? fs.read(cssPath) : ''
|
|
81
|
+
const hasTheme = KNOWN_THEMES.some((t) => css.includes(`@rokkit/themes/${t}.css`))
|
|
82
|
+
return {
|
|
83
|
+
id: 'css-theme',
|
|
84
|
+
label: 'app.css has a theme style',
|
|
85
|
+
status: hasTheme ? 'pass' : 'warn',
|
|
86
|
+
fixable: false,
|
|
87
|
+
fix: `Add a theme import to src/app.css, e.g. @import '@rokkit/themes/rokkit.css'\n Available: ${KNOWN_THEMES.join(', ')}, or use a custom theme`
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check rokkit.config.js has a chart section.
|
|
93
|
+
* Missing chart config is a warning (not failure) — defaults work without it.
|
|
94
|
+
* @param {Object} fs
|
|
95
|
+
* @returns {Object}
|
|
96
|
+
*/
|
|
97
|
+
function checkChartConfig(fs) {
|
|
98
|
+
const configPath = fs.resolve('rokkit.config.js')
|
|
99
|
+
const configExists = fs.exists(configPath)
|
|
100
|
+
const hasChart = configExists && /\bchart\s*:/.test(fs.read(configPath))
|
|
101
|
+
return {
|
|
102
|
+
id: 'chart-config',
|
|
103
|
+
label: 'rokkit.config.js has chart configuration',
|
|
104
|
+
status: hasChart ? 'pass' : 'warn',
|
|
105
|
+
fixable: configExists,
|
|
106
|
+
fix: 'Add a chart section to rokkit.config.js (see docs/design/17-chart-preset.md)',
|
|
107
|
+
autoFix: 'patch-chart-config'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check app.html has init script
|
|
113
|
+
* @param {Object} fs
|
|
114
|
+
* @returns {Object}
|
|
115
|
+
*/
|
|
116
|
+
function checkHtmlScript(fs) {
|
|
117
|
+
const htmlPath = fs.resolve('src/app.html')
|
|
118
|
+
const htmlExists = fs.exists(htmlPath)
|
|
119
|
+
const htmlHasScript =
|
|
120
|
+
htmlExists &&
|
|
121
|
+
(fs.read(htmlPath).includes('rokkit-theme') || fs.read(htmlPath).includes('data-mode'))
|
|
122
|
+
return {
|
|
123
|
+
id: 'html-init-script',
|
|
124
|
+
label: 'app.html has theme init script',
|
|
125
|
+
status: htmlHasScript ? 'pass' : 'fail',
|
|
126
|
+
fixable: true,
|
|
127
|
+
fix: 'Add flash-prevention script to src/app.html',
|
|
128
|
+
autoFix: 'patch-html'
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Run all doctor checks against the given filesystem adapter.
|
|
134
|
+
* @param {{ exists: (p: string) => boolean, read: (p: string) => string, resolve: (p: string) => string }} fs
|
|
135
|
+
* @returns {Array<{ id: string, label: string, status: 'pass'|'fail', fixable: boolean, fix: string, autoFix?: string }>}
|
|
136
|
+
*/
|
|
137
|
+
export function runChecks(fs) {
|
|
138
|
+
return [
|
|
139
|
+
checkConfig(fs),
|
|
140
|
+
checkUnoPreset(fs),
|
|
141
|
+
checkCssImports(fs),
|
|
142
|
+
checkCssTheme(fs),
|
|
143
|
+
checkHtmlScript(fs),
|
|
144
|
+
checkChartConfig(fs)
|
|
145
|
+
]
|
|
146
|
+
}
|
package/src/doctor.js
CHANGED
|
@@ -1,477 +1,22 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs'
|
|
3
|
-
import { resolve, join } from 'path'
|
|
4
|
-
import { loadConfig as loadParsedConfig } from './config.js'
|
|
5
|
-
import {
|
|
6
|
-
generateUnoConfig,
|
|
7
|
-
generateAppCssImports,
|
|
8
|
-
generateChartConfig,
|
|
9
|
-
generateConfig,
|
|
10
|
-
serializeRokkitConfig
|
|
11
|
-
} from './init.js'
|
|
12
|
-
import { themeInitScript } from '@rokkit/unocss/hooks'
|
|
13
|
-
import { checkContrastTokens } from './contrast.js'
|
|
14
|
-
|
|
15
|
-
const KNOWN_THEMES = [
|
|
16
|
-
'rokkit',
|
|
17
|
-
'minimal',
|
|
18
|
-
'material',
|
|
19
|
-
'frosted',
|
|
20
|
-
'zen-sumi'
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Build the default named-token starter config source used by `doctor --fix`.
|
|
25
|
-
* @returns {string}
|
|
26
|
-
*/
|
|
27
|
-
export function defaultStarterSource() {
|
|
28
|
-
const config = generateConfig({
|
|
29
|
-
palette: 'default',
|
|
30
|
-
icons: 'rokkit',
|
|
31
|
-
themes: ['rokkit'],
|
|
32
|
-
switcher: 'manual',
|
|
33
|
-
includeChart: true,
|
|
34
|
-
chartColors: 'default',
|
|
35
|
-
chartShades: 'standard'
|
|
36
|
-
})
|
|
37
|
-
return serializeRokkitConfig(config)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Check config file existence
|
|
42
|
-
* @param {Object} fs
|
|
43
|
-
* @returns {{ id: string, label: string, status: string, fixable: boolean, fix: string, autoFix: string }}
|
|
44
|
-
*/
|
|
45
|
-
function checkConfig(fs) {
|
|
46
|
-
const configExists = fs.exists(fs.resolve('rokkit.config.js'))
|
|
47
|
-
return {
|
|
48
|
-
id: 'config-exists',
|
|
49
|
-
label: 'rokkit.config.js exists',
|
|
50
|
-
status: configExists ? 'pass' : 'fail',
|
|
51
|
-
fixable: true,
|
|
52
|
-
fix: 'Run `rokkit init` to generate config',
|
|
53
|
-
autoFix: 'generate-config'
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Check uno.config.js uses presetRokkit
|
|
59
|
-
* @param {Object} fs
|
|
60
|
-
* @returns {Object}
|
|
61
|
-
*/
|
|
62
|
-
function checkUnoPreset(fs) {
|
|
63
|
-
const unoPath = fs.resolve('uno.config.js')
|
|
64
|
-
const unoExists = fs.exists(unoPath)
|
|
65
|
-
const unoContent = unoExists ? fs.read(unoPath) : ''
|
|
66
|
-
const unoUsesPreset = unoContent.includes('presetRokkit') && unoContent.includes('rokkit.config')
|
|
67
|
-
return {
|
|
68
|
-
id: 'uno-uses-preset',
|
|
69
|
-
label: 'uno.config.js uses presetRokkit(config)',
|
|
70
|
-
status: unoUsesPreset ? 'pass' : 'fail',
|
|
71
|
-
fixable: false,
|
|
72
|
-
fix: unoExists
|
|
73
|
-
? `Replace uno.config.js contents with:\n${generateUnoConfig()}`
|
|
74
|
-
: `Create uno.config.js with:\n${generateUnoConfig()}`
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Check app.css has theme imports
|
|
80
|
-
* @param {Object} fs
|
|
81
|
-
* @returns {Object}
|
|
82
|
-
*/
|
|
83
|
-
function checkCssImports(fs) {
|
|
84
|
-
const cssPath = fs.resolve('src/app.css')
|
|
85
|
-
const cssExists = fs.exists(cssPath)
|
|
86
|
-
const cssHasBase = cssExists && fs.read(cssPath).includes('@rokkit/themes/base.css')
|
|
87
|
-
return {
|
|
88
|
-
id: 'css-imports',
|
|
89
|
-
label: 'app.css imports @rokkit/themes/base.css',
|
|
90
|
-
status: cssHasBase ? 'pass' : 'fail',
|
|
91
|
-
fixable: true,
|
|
92
|
-
fix: 'Append base theme import to src/app.css',
|
|
93
|
-
autoFix: 'patch-css'
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Check app.css has at least one theme style
|
|
99
|
-
* @param {Object} fs
|
|
100
|
-
* @returns {Object}
|
|
101
|
-
*/
|
|
102
|
-
function checkCssTheme(fs) {
|
|
103
|
-
const cssPath = fs.resolve('src/app.css')
|
|
104
|
-
const cssExists = fs.exists(cssPath)
|
|
105
|
-
const css = cssExists ? fs.read(cssPath) : ''
|
|
106
|
-
const hasTheme = KNOWN_THEMES.some((t) => css.includes(`@rokkit/themes/${t}.css`))
|
|
107
|
-
return {
|
|
108
|
-
id: 'css-theme',
|
|
109
|
-
label: 'app.css has a theme style',
|
|
110
|
-
status: hasTheme ? 'pass' : 'warn',
|
|
111
|
-
fixable: false,
|
|
112
|
-
fix: `Add a theme import to src/app.css, e.g. @import '@rokkit/themes/rokkit.css'\n Available: ${KNOWN_THEMES.join(', ')}, or use a custom theme`
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Check rokkit.config.js has a chart section.
|
|
118
|
-
* Missing chart config is a warning (not failure) — defaults work without it.
|
|
119
|
-
* @param {Object} fs
|
|
120
|
-
* @returns {Object}
|
|
121
|
-
*/
|
|
122
|
-
function checkChartConfig(fs) {
|
|
123
|
-
const configPath = fs.resolve('rokkit.config.js')
|
|
124
|
-
const configExists = fs.exists(configPath)
|
|
125
|
-
const hasChart = configExists && /\bchart\s*:/.test(fs.read(configPath))
|
|
126
|
-
return {
|
|
127
|
-
id: 'chart-config',
|
|
128
|
-
label: 'rokkit.config.js has chart configuration',
|
|
129
|
-
status: hasChart ? 'pass' : 'warn',
|
|
130
|
-
fixable: configExists,
|
|
131
|
-
fix: 'Add a chart section to rokkit.config.js (see docs/design/17-chart-preset.md)',
|
|
132
|
-
autoFix: 'patch-chart-config'
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Check app.html has init script
|
|
138
|
-
* @param {Object} fs
|
|
139
|
-
* @returns {Object}
|
|
140
|
-
*/
|
|
141
|
-
function checkHtmlScript(fs) {
|
|
142
|
-
const htmlPath = fs.resolve('src/app.html')
|
|
143
|
-
const htmlExists = fs.exists(htmlPath)
|
|
144
|
-
const htmlHasScript =
|
|
145
|
-
htmlExists &&
|
|
146
|
-
(fs.read(htmlPath).includes('rokkit-theme') || fs.read(htmlPath).includes('data-mode'))
|
|
147
|
-
return {
|
|
148
|
-
id: 'html-init-script',
|
|
149
|
-
label: 'app.html has theme init script',
|
|
150
|
-
status: htmlHasScript ? 'pass' : 'fail',
|
|
151
|
-
fixable: true,
|
|
152
|
-
fix: 'Add flash-prevention script to src/app.html',
|
|
153
|
-
autoFix: 'patch-html'
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Run all doctor checks against the given filesystem adapter.
|
|
159
|
-
* @param {{ exists: (p: string) => boolean, read: (p: string) => string, resolve: (p: string) => string }} fs
|
|
160
|
-
* @returns {Array<{ id: string, label: string, status: 'pass'|'fail', fixable: boolean, fix: string, autoFix?: string }>}
|
|
161
|
-
*/
|
|
162
|
-
export function runChecks(fs) {
|
|
163
|
-
return [
|
|
164
|
-
checkConfig(fs),
|
|
165
|
-
checkUnoPreset(fs),
|
|
166
|
-
checkCssImports(fs),
|
|
167
|
-
checkCssTheme(fs),
|
|
168
|
-
checkHtmlScript(fs),
|
|
169
|
-
checkChartConfig(fs)
|
|
170
|
-
]
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const SKIP_DIRS = new Set(['node_modules', '.svelte-kit', 'dist', '.git', 'coverage'])
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Recursively list absolute file paths under `dir`, skipping vendored/build dirs.
|
|
177
|
-
* @param {string} dir
|
|
178
|
-
* @returns {string[]}
|
|
179
|
-
*/
|
|
180
|
-
function listFiles(dir) {
|
|
181
|
-
const out = []
|
|
182
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
183
|
-
if (entry.isDirectory()) {
|
|
184
|
-
if (SKIP_DIRS.has(entry.name)) continue
|
|
185
|
-
out.push(...listFiles(join(dir, entry.name)))
|
|
186
|
-
} else {
|
|
187
|
-
out.push(join(dir, entry.name))
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return out
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Create a real filesystem adapter rooted at cwd.
|
|
195
|
-
* @param {string} cwd
|
|
196
|
-
*/
|
|
197
|
-
function createFsAdapter(cwd) {
|
|
198
|
-
return {
|
|
199
|
-
exists: (p) => existsSync(p),
|
|
200
|
-
read: (p) => readFileSync(p, 'utf-8'),
|
|
201
|
-
resolve: (p) => resolve(cwd, p),
|
|
202
|
-
list: (dir) => listFiles(dir)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Apply the generate-config fix
|
|
208
|
-
* @param {string} cwd
|
|
209
|
-
* @param {string} label
|
|
210
|
-
*/
|
|
211
|
-
function applyGenerateConfig(cwd, label) {
|
|
212
|
-
const configPath = resolve(cwd, 'rokkit.config.js')
|
|
213
|
-
writeFileSync(configPath, defaultStarterSource())
|
|
214
|
-
console.info(` Fixed: ${label}`)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Apply the patch-css fix
|
|
219
|
-
* @param {string} cwd
|
|
220
|
-
* @param {string} label
|
|
221
|
-
*/
|
|
222
|
-
function applyPatchCss(cwd, label) {
|
|
223
|
-
const cssPath = resolve(cwd, 'src/app.css')
|
|
224
|
-
const imports = generateAppCssImports([])
|
|
225
|
-
if (existsSync(cssPath)) {
|
|
226
|
-
const existing = readFileSync(cssPath, 'utf-8')
|
|
227
|
-
const missing = imports.filter((line) => !existing.includes(line))
|
|
228
|
-
if (missing.length > 0) writeFileSync(cssPath, `${missing.join('\n')}\n${existing}`)
|
|
229
|
-
} else {
|
|
230
|
-
writeFileSync(cssPath, `${imports.join('\n')}\n`)
|
|
231
|
-
}
|
|
232
|
-
console.info(` Fixed: ${label}`)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
2
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
3
|
+
* The `rokkit doctor` command — validates project setup, then reports and
|
|
4
|
+
* auto-fixes issues.
|
|
5
|
+
*
|
|
6
|
+
* Check definitions live in `checks.js`, the fs adapter + auto-fix handlers in
|
|
7
|
+
* `fix.js`, console reporting in `report.js`, and advisory shape/lint checks in
|
|
8
|
+
* `validate.js`. This file is the command orchestration + public surface.
|
|
240
9
|
*/
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const marker = storageKey || 'rokkit-theme'
|
|
252
|
-
if (html.includes(marker)) return false
|
|
253
|
-
const script = themeInitScript({ storageKey })
|
|
254
|
-
const patched = html.replace(/(<body[^>]*>)/, `$1\n${script}`)
|
|
255
|
-
writeFileSync(htmlPath, patched)
|
|
256
|
-
console.info(` Fixed: ${label}`)
|
|
257
|
-
return true
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Patch rokkit.config.js to add a default chart section.
|
|
262
|
-
* @param {string} cwd
|
|
263
|
-
* @param {string} label
|
|
264
|
-
*/
|
|
265
|
-
/* v8 ignore next 16 -- chart-config check emits 'warn' (not 'fail'); applyCheckFix
|
|
266
|
-
only processes 'fail' statuses, making this handler unreachable via the normal
|
|
267
|
-
fix flow. Kept for potential future use if the check severity changes. */
|
|
268
|
-
function applyPatchChartConfig(cwd, label) {
|
|
269
|
-
const configPath = resolve(cwd, 'rokkit.config.js')
|
|
270
|
-
if (!existsSync(configPath)) return false
|
|
271
|
-
const content = readFileSync(configPath, 'utf-8')
|
|
272
|
-
if (/\bchart\s*:/.test(content)) return false
|
|
273
|
-
const chartConfig = generateChartConfig({ chartColors: 'default', chartShades: 'standard' })
|
|
274
|
-
const chartJson = JSON.stringify(chartConfig, null, 2).replace(/\n/g, '\n ')
|
|
275
|
-
// Inject before the last closing brace of the export default object
|
|
276
|
-
const patched = content.replace(/(\n?}\s*\n?)$/, `,\n chart: ${chartJson}\n}\n`)
|
|
277
|
-
writeFileSync(configPath, patched)
|
|
278
|
-
console.info(` Fixed: ${label}`)
|
|
279
|
-
return true
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/** @type {Record<string, (cwd: string, label: string) => boolean|void>} */
|
|
283
|
-
const FIX_HANDLERS = {
|
|
284
|
-
'generate-config': (cwd, label) => {
|
|
285
|
-
applyGenerateConfig(cwd, label)
|
|
286
|
-
return true
|
|
287
|
-
},
|
|
288
|
-
'patch-css': (cwd, label) => {
|
|
289
|
-
applyPatchCss(cwd, label)
|
|
290
|
-
return true
|
|
291
|
-
},
|
|
292
|
-
'patch-html': (cwd, label) => applyPatchHtml(cwd, label),
|
|
293
|
-
'patch-chart-config': (cwd, label) => applyPatchChartConfig(cwd, label)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* @param {number} fixed
|
|
298
|
-
* @param {{ status: string, fixable: boolean, autoFix?: string, label: string }} check
|
|
299
|
-
* @param {string} cwd
|
|
300
|
-
* @returns {number}
|
|
301
|
-
*/
|
|
302
|
-
function applyCheckFix(fixed, check, cwd) {
|
|
303
|
-
if (check.status !== 'fail' || !check.fixable) return fixed
|
|
304
|
-
const handler = FIX_HANDLERS[check.autoFix]
|
|
305
|
-
return handler && handler(cwd, check.label) ? fixed + 1 : fixed
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Auto-fix failed checks that are marked fixable.
|
|
310
|
-
* @param {Array<{ id: string, status: string, fixable: boolean, autoFix?: string, label: string }>} checks
|
|
311
|
-
* @param {string} cwd
|
|
312
|
-
* @returns {number} number of fixed items
|
|
313
|
-
*/
|
|
314
|
-
function autoFix(checks, cwd) {
|
|
315
|
-
return checks.reduce((fixed, check) => applyCheckFix(fixed, check, cwd), 0)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Print fix hints for failing checks
|
|
320
|
-
* @param {Array} checks
|
|
321
|
-
*/
|
|
322
|
-
function printFixHints(checks) {
|
|
323
|
-
for (const check of checks) {
|
|
324
|
-
if (check.status === 'fail' || check.status === 'warn') {
|
|
325
|
-
console.info(` ${check.fixable ? '(auto-fixable) ' : ''}${check.fix}`)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Print remaining manual action items
|
|
332
|
-
* @param {Array} checks
|
|
333
|
-
*/
|
|
334
|
-
function printManualItems(checks) {
|
|
335
|
-
for (const check of checks) {
|
|
336
|
-
if (check.status === 'fail' && !check.fixable) {
|
|
337
|
-
console.info(` - ${check.label}: ${check.fix}`)
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Count failures and print status for each check.
|
|
344
|
-
* @param {Array} checks
|
|
345
|
-
* @returns {number}
|
|
346
|
-
*/
|
|
347
|
-
function printChecks(checks) {
|
|
348
|
-
let failures = 0
|
|
349
|
-
for (const check of checks) {
|
|
350
|
-
const icon = check.status === 'pass' ? 'PASS' : check.status === 'warn' ? 'WARN' : 'FAIL'
|
|
351
|
-
console.info(` ${icon} ${check.label}`)
|
|
352
|
-
if (check.status === 'fail') failures++
|
|
353
|
-
}
|
|
354
|
-
return failures
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Handle auto-fix flow and print results.
|
|
359
|
-
* @param {Array} checks
|
|
360
|
-
* @param {string} cwd
|
|
361
|
-
* @param {number} failures
|
|
362
|
-
*/
|
|
363
|
-
function handleAutoFix(checks, cwd, failures) {
|
|
364
|
-
console.info('\nAuto-fixing...\n')
|
|
365
|
-
const fixed = autoFix(checks, cwd)
|
|
366
|
-
const remaining = failures - fixed
|
|
367
|
-
if (remaining > 0) {
|
|
368
|
-
console.info(`\n${fixed} fixed, ${remaining} require manual action:`)
|
|
369
|
-
printManualItems(checks)
|
|
370
|
-
} else {
|
|
371
|
-
console.info(`\nAll ${fixed} issues fixed!`)
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* @param {Array} checks
|
|
377
|
-
* @param {string} cwd
|
|
378
|
-
* @param {number} failures
|
|
379
|
-
* @param {boolean} fix
|
|
380
|
-
*/
|
|
381
|
-
function handleResults(checks, cwd, failures, fix) {
|
|
382
|
-
if (failures === 0) return
|
|
383
|
-
if (fix) {
|
|
384
|
-
handleAutoFix(checks, cwd, failures)
|
|
385
|
-
} else {
|
|
386
|
-
printFixHints(checks)
|
|
387
|
-
process.exitCode = 1
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Validate a parsed rokkit config for the named-token system. All advisory (warn).
|
|
393
|
-
* @param {Record<string, unknown> | null} config
|
|
394
|
-
* @returns {Array<{ id: string, label: string, status: 'warn', fixable: false, fix: string }>}
|
|
395
|
-
*/
|
|
396
|
-
export function validateConfigShape(config) {
|
|
397
|
-
if (!config) return []
|
|
398
|
-
const checks = []
|
|
399
|
-
const usesColorsAlias =
|
|
400
|
-
typeof config.colors === 'object' && config.colors !== null && !Array.isArray(config.colors) && !config.skin
|
|
401
|
-
const colormap = config.skins?.default ?? config.skin ?? config.colors ?? {}
|
|
402
|
-
|
|
403
|
-
if (!('ink' in colormap)) {
|
|
404
|
-
checks.push({
|
|
405
|
-
id: 'skin-ink-role',
|
|
406
|
-
label: 'skin defines an `ink` role',
|
|
407
|
-
status: 'warn',
|
|
408
|
-
fixable: false,
|
|
409
|
-
fix: "Add `ink: '<palette>'` (reusing the surface palette is fine) — without it, ink-* text tokens fall back to the surface palette."
|
|
410
|
-
})
|
|
411
|
-
}
|
|
412
|
-
if (config.colorSpace === 'oklch' && Object.keys(config.palettes ?? {}).length === 0) {
|
|
413
|
-
checks.push({
|
|
414
|
-
id: 'oklch-needs-palettes',
|
|
415
|
-
label: 'colorSpace `oklch` has a `palettes` block',
|
|
416
|
-
status: 'warn',
|
|
417
|
-
fixable: false,
|
|
418
|
-
fix: 'oklch values need a `palettes` block of bare "L C H" components; Tailwind named colors are rgb.'
|
|
419
|
-
})
|
|
420
|
-
}
|
|
421
|
-
if (usesColorsAlias) {
|
|
422
|
-
checks.push({
|
|
423
|
-
id: 'colors-alias',
|
|
424
|
-
label: 'config uses `skin` (not the legacy `colors` alias)',
|
|
425
|
-
status: 'warn',
|
|
426
|
-
fixable: false,
|
|
427
|
-
fix: 'Rename `colors:` to `skin:` — `colors` is a back-compat alias.'
|
|
428
|
-
})
|
|
429
|
-
}
|
|
430
|
-
return checks
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Patterns that flag a surface/border token being used as a text colour.
|
|
435
|
-
* paper-edge / paper-mute are border/surface tones — used as text they're faint
|
|
436
|
-
* in light and invisible in dark. paper / paper-soft are legitimate on-colours.
|
|
437
|
-
*/
|
|
438
|
-
const TEXT_MISUSE_PATTERNS = [
|
|
439
|
-
/text-paper-edge\b/,
|
|
440
|
-
/text-paper-mute\b/,
|
|
441
|
-
/color:\s*var\(\s*--paper-edge\s*\)/,
|
|
442
|
-
/color:\s*var\(\s*--paper-mute\s*\)/
|
|
443
|
-
]
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Advisory lint: scan src/ for surface/border tokens used as text colour.
|
|
447
|
-
* @param {{ list?: (dir: string) => string[], exists: (p: string) => boolean, read: (p: string) => string, resolve: (p: string) => string }} fs
|
|
448
|
-
* @returns {Array<{ id: string, label: string, status: 'warn', fixable: false, fix: string }>}
|
|
449
|
-
*/
|
|
450
|
-
export function checkTextTokenUsage(fs) {
|
|
451
|
-
if (typeof fs.list !== 'function' || !fs.exists(fs.resolve('src'))) return []
|
|
452
|
-
|
|
453
|
-
const cwd = process.cwd()
|
|
454
|
-
const hits = []
|
|
455
|
-
for (const file of fs.list(fs.resolve('src'))) {
|
|
456
|
-
if (!file.endsWith('.css') && !file.endsWith('.svelte')) continue
|
|
457
|
-
const lines = fs.read(file).split('\n')
|
|
458
|
-
const display = file.startsWith(cwd) ? file.slice(cwd.length).replace(/^\//, '') : file
|
|
459
|
-
lines.forEach((line, i) => {
|
|
460
|
-
if (TEXT_MISUSE_PATTERNS.some((re) => re.test(line))) hits.push(`${display}:${i + 1}`)
|
|
461
|
-
})
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (hits.length === 0) return []
|
|
465
|
-
return [
|
|
466
|
-
{
|
|
467
|
-
id: 'surface-token-as-text',
|
|
468
|
-
label: 'no surface/border token used as text colour',
|
|
469
|
-
status: 'warn',
|
|
470
|
-
fixable: false,
|
|
471
|
-
fix: `paper-edge / paper-mute are border/surface tokens — never use them for text (faint in light, invisible in dark). Use the ink scale (ink-mute for readable secondary text). Found at:\n ${hits.join('\n ')}`
|
|
472
|
-
}
|
|
473
|
-
]
|
|
474
|
-
}
|
|
10
|
+
import { loadConfig as loadParsedConfig } from './config.js'
|
|
11
|
+
import { checkContrastTokens } from './contrast.js'
|
|
12
|
+
import { runChecks } from './checks.js'
|
|
13
|
+
import { createFsAdapter } from './fix.js'
|
|
14
|
+
import { handleResults, printChecks } from './report.js'
|
|
15
|
+
import { validateConfigShape, checkTextTokenUsage } from './validate.js'
|
|
16
|
+
|
|
17
|
+
export { runChecks } from './checks.js'
|
|
18
|
+
export { defaultStarterSource } from './fix.js'
|
|
19
|
+
export { validateConfigShape, checkTextTokenUsage } from './validate.js'
|
|
475
20
|
|
|
476
21
|
/**
|
|
477
22
|
* Interactive doctor command — validates project setup.
|
package/src/fix.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/**
|
|
3
|
+
* The filesystem adapter and auto-fix machinery behind `doctor --fix`:
|
|
4
|
+
* apply each fix handler for a failed check and count what was fixed.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs'
|
|
7
|
+
import { resolve, join } from 'path'
|
|
8
|
+
import {
|
|
9
|
+
generateAppCssImports,
|
|
10
|
+
generateChartConfig,
|
|
11
|
+
generateConfig,
|
|
12
|
+
serializeRokkitConfig
|
|
13
|
+
} from './init.js'
|
|
14
|
+
import { themeInitScript } from '@rokkit/unocss/hooks'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the default named-token starter config source used by `doctor --fix`.
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function defaultStarterSource() {
|
|
21
|
+
const config = generateConfig({
|
|
22
|
+
palette: 'default',
|
|
23
|
+
icons: 'rokkit',
|
|
24
|
+
themes: ['rokkit'],
|
|
25
|
+
switcher: 'manual',
|
|
26
|
+
includeChart: true,
|
|
27
|
+
chartColors: 'default',
|
|
28
|
+
chartShades: 'standard'
|
|
29
|
+
})
|
|
30
|
+
return serializeRokkitConfig(config)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SKIP_DIRS = new Set(['node_modules', '.svelte-kit', 'dist', '.git', 'coverage'])
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Recursively list absolute file paths under `dir`, skipping vendored/build dirs.
|
|
37
|
+
* @param {string} dir
|
|
38
|
+
* @returns {string[]}
|
|
39
|
+
*/
|
|
40
|
+
function listFiles(dir) {
|
|
41
|
+
const out = []
|
|
42
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
if (SKIP_DIRS.has(entry.name)) continue
|
|
45
|
+
out.push(...listFiles(join(dir, entry.name)))
|
|
46
|
+
} else {
|
|
47
|
+
out.push(join(dir, entry.name))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a real filesystem adapter rooted at cwd.
|
|
55
|
+
* @param {string} cwd
|
|
56
|
+
*/
|
|
57
|
+
export function createFsAdapter(cwd) {
|
|
58
|
+
return {
|
|
59
|
+
exists: (p) => existsSync(p),
|
|
60
|
+
read: (p) => readFileSync(p, 'utf-8'),
|
|
61
|
+
resolve: (p) => resolve(cwd, p),
|
|
62
|
+
list: (dir) => listFiles(dir)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Apply the generate-config fix
|
|
68
|
+
* @param {string} cwd
|
|
69
|
+
* @param {string} label
|
|
70
|
+
*/
|
|
71
|
+
function applyGenerateConfig(cwd, label) {
|
|
72
|
+
const configPath = resolve(cwd, 'rokkit.config.js')
|
|
73
|
+
writeFileSync(configPath, defaultStarterSource())
|
|
74
|
+
console.info(` Fixed: ${label}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Apply the patch-css fix
|
|
79
|
+
* @param {string} cwd
|
|
80
|
+
* @param {string} label
|
|
81
|
+
*/
|
|
82
|
+
function applyPatchCss(cwd, label) {
|
|
83
|
+
const cssPath = resolve(cwd, 'src/app.css')
|
|
84
|
+
const imports = generateAppCssImports([])
|
|
85
|
+
if (existsSync(cssPath)) {
|
|
86
|
+
const existing = readFileSync(cssPath, 'utf-8')
|
|
87
|
+
const missing = imports.filter((line) => !existing.includes(line))
|
|
88
|
+
if (missing.length > 0) writeFileSync(cssPath, `${missing.join('\n')}\n${existing}`)
|
|
89
|
+
} else {
|
|
90
|
+
writeFileSync(cssPath, `${imports.join('\n')}\n`)
|
|
91
|
+
}
|
|
92
|
+
console.info(` Fixed: ${label}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Apply the patch-html fix
|
|
97
|
+
* @param {string} cwd
|
|
98
|
+
* @param {string} label
|
|
99
|
+
* @returns {boolean} whether fix was applied
|
|
100
|
+
*/
|
|
101
|
+
function applyPatchHtml(cwd, label) {
|
|
102
|
+
const htmlPath = resolve(cwd, 'src/app.html')
|
|
103
|
+
if (!existsSync(htmlPath)) return false
|
|
104
|
+
const html = readFileSync(htmlPath, 'utf-8')
|
|
105
|
+
// Use the project's configured storageKey so the injected script reads the
|
|
106
|
+
// same localStorage slot the app persists to — don't hardcode a key.
|
|
107
|
+
const configPath = resolve(cwd, 'rokkit.config.js')
|
|
108
|
+
const storageKey = existsSync(configPath)
|
|
109
|
+
? readFileSync(configPath, 'utf-8').match(/storageKey:\s*['"]([^'"]+)['"]/)?.[1]
|
|
110
|
+
: undefined
|
|
111
|
+
const marker = storageKey || 'rokkit-theme'
|
|
112
|
+
if (html.includes(marker)) return false
|
|
113
|
+
const script = themeInitScript({ storageKey })
|
|
114
|
+
const patched = html.replace(/(<body[^>]*>)/, `$1\n${script}`)
|
|
115
|
+
writeFileSync(htmlPath, patched)
|
|
116
|
+
console.info(` Fixed: ${label}`)
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Patch rokkit.config.js to add a default chart section.
|
|
122
|
+
* @param {string} cwd
|
|
123
|
+
* @param {string} label
|
|
124
|
+
*/
|
|
125
|
+
/* v8 ignore next 16 -- chart-config check emits 'warn' (not 'fail'); applyCheckFix
|
|
126
|
+
only processes 'fail' statuses, making this handler unreachable via the normal
|
|
127
|
+
fix flow. Kept for potential future use if the check severity changes. */
|
|
128
|
+
function applyPatchChartConfig(cwd, label) {
|
|
129
|
+
const configPath = resolve(cwd, 'rokkit.config.js')
|
|
130
|
+
if (!existsSync(configPath)) return false
|
|
131
|
+
const content = readFileSync(configPath, 'utf-8')
|
|
132
|
+
if (/\bchart\s*:/.test(content)) return false
|
|
133
|
+
const chartConfig = generateChartConfig({ chartColors: 'default', chartShades: 'standard' })
|
|
134
|
+
const chartJson = JSON.stringify(chartConfig, null, 2).replace(/\n/g, '\n ')
|
|
135
|
+
// Inject before the last closing brace of the export default object
|
|
136
|
+
const patched = content.replace(/(\n?}\s*\n?)$/, `,\n chart: ${chartJson}\n}\n`)
|
|
137
|
+
writeFileSync(configPath, patched)
|
|
138
|
+
console.info(` Fixed: ${label}`)
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @type {Record<string, (cwd: string, label: string) => boolean|void>} */
|
|
143
|
+
const FIX_HANDLERS = {
|
|
144
|
+
'generate-config': (cwd, label) => {
|
|
145
|
+
applyGenerateConfig(cwd, label)
|
|
146
|
+
return true
|
|
147
|
+
},
|
|
148
|
+
'patch-css': (cwd, label) => {
|
|
149
|
+
applyPatchCss(cwd, label)
|
|
150
|
+
return true
|
|
151
|
+
},
|
|
152
|
+
'patch-html': (cwd, label) => applyPatchHtml(cwd, label),
|
|
153
|
+
'patch-chart-config': (cwd, label) => applyPatchChartConfig(cwd, label)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {number} fixed
|
|
158
|
+
* @param {{ status: string, fixable: boolean, autoFix?: string, label: string }} check
|
|
159
|
+
* @param {string} cwd
|
|
160
|
+
* @returns {number}
|
|
161
|
+
*/
|
|
162
|
+
function applyCheckFix(fixed, check, cwd) {
|
|
163
|
+
if (check.status !== 'fail' || !check.fixable) return fixed
|
|
164
|
+
const handler = FIX_HANDLERS[check.autoFix]
|
|
165
|
+
return handler && handler(cwd, check.label) ? fixed + 1 : fixed
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Auto-fix failed checks that are marked fixable.
|
|
170
|
+
* @param {Array<{ id: string, status: string, fixable: boolean, autoFix?: string, label: string }>} checks
|
|
171
|
+
* @param {string} cwd
|
|
172
|
+
* @returns {number} number of fixed items
|
|
173
|
+
*/
|
|
174
|
+
export function autoFix(checks, cwd) {
|
|
175
|
+
return checks.reduce((fixed, check) => applyCheckFix(fixed, check, cwd), 0)
|
|
176
|
+
}
|
package/src/report.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/**
|
|
3
|
+
* Console reporting for `doctor`: print per-check status lines, fix hints and
|
|
4
|
+
* manual action items, and orchestrate the auto-fix flow summary.
|
|
5
|
+
*/
|
|
6
|
+
import { autoFix } from './fix.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Print fix hints for failing checks
|
|
10
|
+
* @param {Array} checks
|
|
11
|
+
*/
|
|
12
|
+
function printFixHints(checks) {
|
|
13
|
+
for (const check of checks) {
|
|
14
|
+
if (check.status === 'fail' || check.status === 'warn') {
|
|
15
|
+
console.info(` ${check.fixable ? '(auto-fixable) ' : ''}${check.fix}`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Print remaining manual action items
|
|
22
|
+
* @param {Array} checks
|
|
23
|
+
*/
|
|
24
|
+
function printManualItems(checks) {
|
|
25
|
+
for (const check of checks) {
|
|
26
|
+
if (check.status === 'fail' && !check.fixable) {
|
|
27
|
+
console.info(` - ${check.label}: ${check.fix}`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Count failures and print status for each check.
|
|
34
|
+
* @param {Array} checks
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
export function printChecks(checks) {
|
|
38
|
+
let failures = 0
|
|
39
|
+
for (const check of checks) {
|
|
40
|
+
const icon = check.status === 'pass' ? 'PASS' : check.status === 'warn' ? 'WARN' : 'FAIL'
|
|
41
|
+
console.info(` ${icon} ${check.label}`)
|
|
42
|
+
if (check.status === 'fail') failures++
|
|
43
|
+
}
|
|
44
|
+
return failures
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handle auto-fix flow and print results.
|
|
49
|
+
* @param {Array} checks
|
|
50
|
+
* @param {string} cwd
|
|
51
|
+
* @param {number} failures
|
|
52
|
+
*/
|
|
53
|
+
function handleAutoFix(checks, cwd, failures) {
|
|
54
|
+
console.info('\nAuto-fixing...\n')
|
|
55
|
+
const fixed = autoFix(checks, cwd)
|
|
56
|
+
const remaining = failures - fixed
|
|
57
|
+
if (remaining > 0) {
|
|
58
|
+
console.info(`\n${fixed} fixed, ${remaining} require manual action:`)
|
|
59
|
+
printManualItems(checks)
|
|
60
|
+
} else {
|
|
61
|
+
console.info(`\nAll ${fixed} issues fixed!`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {Array} checks
|
|
67
|
+
* @param {string} cwd
|
|
68
|
+
* @param {number} failures
|
|
69
|
+
* @param {boolean} fix
|
|
70
|
+
*/
|
|
71
|
+
export function handleResults(checks, cwd, failures, fix) {
|
|
72
|
+
if (failures === 0) return
|
|
73
|
+
if (fix) {
|
|
74
|
+
handleAutoFix(checks, cwd, failures)
|
|
75
|
+
} else {
|
|
76
|
+
printFixHints(checks)
|
|
77
|
+
process.exitCode = 1
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/validate.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advisory validation run by `doctor`: config shape rules for the named-token
|
|
3
|
+
* system, and a scan for surface/border tokens misused as text colour.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate a parsed rokkit config for the named-token system. All advisory (warn).
|
|
8
|
+
* @param {Record<string, unknown> | null} config
|
|
9
|
+
* @returns {Array<{ id: string, label: string, status: 'warn', fixable: false, fix: string }>}
|
|
10
|
+
*/
|
|
11
|
+
export function validateConfigShape(config) {
|
|
12
|
+
if (!config) return []
|
|
13
|
+
const checks = []
|
|
14
|
+
const usesColorsAlias =
|
|
15
|
+
typeof config.colors === 'object' && config.colors !== null && !Array.isArray(config.colors) && !config.skin
|
|
16
|
+
const colormap = config.skins?.default ?? config.skin ?? config.colors ?? {}
|
|
17
|
+
|
|
18
|
+
if (!('ink' in colormap)) {
|
|
19
|
+
checks.push({
|
|
20
|
+
id: 'skin-ink-role',
|
|
21
|
+
label: 'skin defines an `ink` role',
|
|
22
|
+
status: 'warn',
|
|
23
|
+
fixable: false,
|
|
24
|
+
fix: "Add `ink: '<palette>'` (reusing the surface palette is fine) — without it, ink-* text tokens fall back to the surface palette."
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
if (config.colorSpace === 'oklch' && Object.keys(config.palettes ?? {}).length === 0) {
|
|
28
|
+
checks.push({
|
|
29
|
+
id: 'oklch-needs-palettes',
|
|
30
|
+
label: 'colorSpace `oklch` has a `palettes` block',
|
|
31
|
+
status: 'warn',
|
|
32
|
+
fixable: false,
|
|
33
|
+
fix: 'oklch values need a `palettes` block of bare "L C H" components; Tailwind named colors are rgb.'
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
if (usesColorsAlias) {
|
|
37
|
+
checks.push({
|
|
38
|
+
id: 'colors-alias',
|
|
39
|
+
label: 'config uses `skin` (not the legacy `colors` alias)',
|
|
40
|
+
status: 'warn',
|
|
41
|
+
fixable: false,
|
|
42
|
+
fix: 'Rename `colors:` to `skin:` — `colors` is a back-compat alias.'
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
return checks
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Patterns that flag a surface/border token being used as a text colour.
|
|
50
|
+
* paper-edge / paper-mute are border/surface tones — used as text they're faint
|
|
51
|
+
* in light and invisible in dark. paper / paper-soft are legitimate on-colours.
|
|
52
|
+
*/
|
|
53
|
+
const TEXT_MISUSE_PATTERNS = [
|
|
54
|
+
/text-paper-edge\b/,
|
|
55
|
+
/text-paper-mute\b/,
|
|
56
|
+
/color:\s*var\(\s*--paper-edge\s*\)/,
|
|
57
|
+
/color:\s*var\(\s*--paper-mute\s*\)/
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Advisory lint: scan src/ for surface/border tokens used as text colour.
|
|
62
|
+
* @param {{ list?: (dir: string) => string[], exists: (p: string) => boolean, read: (p: string) => string, resolve: (p: string) => string }} fs
|
|
63
|
+
* @returns {Array<{ id: string, label: string, status: 'warn', fixable: false, fix: string }>}
|
|
64
|
+
*/
|
|
65
|
+
export function checkTextTokenUsage(fs) {
|
|
66
|
+
if (typeof fs.list !== 'function' || !fs.exists(fs.resolve('src'))) return []
|
|
67
|
+
|
|
68
|
+
const cwd = process.cwd()
|
|
69
|
+
const hits = []
|
|
70
|
+
for (const file of fs.list(fs.resolve('src'))) {
|
|
71
|
+
if (!file.endsWith('.css') && !file.endsWith('.svelte')) continue
|
|
72
|
+
const lines = fs.read(file).split('\n')
|
|
73
|
+
const display = file.startsWith(cwd) ? file.slice(cwd.length).replace(/^\//, '') : file
|
|
74
|
+
lines.forEach((line, i) => {
|
|
75
|
+
if (TEXT_MISUSE_PATTERNS.some((re) => re.test(line))) hits.push(`${display}:${i + 1}`)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (hits.length === 0) return []
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
id: 'surface-token-as-text',
|
|
83
|
+
label: 'no surface/border token used as text colour',
|
|
84
|
+
status: 'warn',
|
|
85
|
+
fixable: false,
|
|
86
|
+
fix: `paper-edge / paper-mute are border/surface tokens — never use them for text (faint in light, invisible in dark). Use the ink scale (ink-mute for readable secondary text). Found at:\n ${hits.join('\n ')}`
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|