@keenmate/web-components-devkit 1.0.0-rc05 → 1.0.0-rc07
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/CHANGELOG.md +114 -0
- package/README.md +81 -23
- package/package.json +1 -1
- package/src/cli/args.js +32 -9
- package/src/cli/commands/check-pages.js +189 -0
- package/src/cli/commands/check-vars.js +24 -8
- package/src/cli/commands/doctor.js +129 -16
- package/src/cli/kmwc.js +80 -33
- package/src/cli/repo-checks.js +249 -0
- package/src/cli/sync-plan.js +3 -2
- package/src/config.d.ts +14 -4
- package/src/config.js +13 -2
- package/src/examples/css/base.css +28 -0
- package/src/examples/css/content.css +44 -0
- package/src/examples/css/form-controls.css +4 -0
- package/src/examples/css/index-page.css +4 -0
- package/src/examples/css/widgets.css +12 -4
- package/src/vite/index.js +10 -7
- package/src/vite/page-composer.js +30 -1
- package/templates/Makefile.tmpl +4 -1
- package/templates/claude-block.md +6 -1
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Contract (prefix = kmwc.prefix or manifest.prefix, e.g. `ms`):
|
|
10
10
|
* - every `--base-*` read via var() must be in baseVariables, and vice versa
|
|
11
|
-
* - checkMode "declared" (default): every `--<prefix>-*` DECLARED (`--x:`)
|
|
12
|
-
* must be in componentVariables, and vice versa
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* - checkMode "declared" (default): every `--<prefix>-*` DECLARED (`--x:`) or
|
|
12
|
+
* READ (`var(--x)`) must be in componentVariables, and vice versa. A read
|
|
13
|
+
* without a declaration is an input variable — a public knob that is unset by
|
|
14
|
+
* design, e.g. one forwarded to core's menu so a page-wide value still applies
|
|
15
|
+
* - checkMode "consumed": only `var(--<prefix>-*)` reads count (for components
|
|
16
|
+
* whose knobs are all read-with-fallback; a declared-but-unread name is dead)
|
|
15
17
|
* - names in kmwc.cssVariables.internalVariables are runtime internals — exempt
|
|
16
18
|
*
|
|
17
19
|
* Undocumented component variables are a warning unless --strict; everything else errors.
|
|
@@ -25,7 +27,9 @@ const STYLESHEET_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less', '.pcss
|
|
|
25
27
|
/**
|
|
26
28
|
* Directories are scanned for stylesheets; a file listed explicitly is read
|
|
27
29
|
* whatever its type — e.g. a built bundle that inlines the compiled CSS, for
|
|
28
|
-
* components whose SCSS generates variable names via interpolation
|
|
30
|
+
* components whose SCSS generates variable names via interpolation, or a core
|
|
31
|
+
* module whose CSS the component renders (`@keenmate/web-components-core/dist/menu/styles.js`
|
|
32
|
+
* — a package path resolves from node_modules).
|
|
29
33
|
*/
|
|
30
34
|
function getStylesheetTexts(sourcePath, stylesheetTexts, isExplicit = true) {
|
|
31
35
|
if (!existsSync(sourcePath)) throw new Error(`cssVariables source not found: ${sourcePath}`);
|
|
@@ -37,6 +41,14 @@ function getStylesheetTexts(sourcePath, stylesheetTexts, isExplicit = true) {
|
|
|
37
41
|
return stylesheetTexts;
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/** A source relative to the repo, else a package path (`@scope/pkg/dist/x.js`) under node_modules. */
|
|
45
|
+
function resolveSourcePath(root, sourcePath) {
|
|
46
|
+
const repoPath = resolve(root, sourcePath);
|
|
47
|
+
if (existsSync(repoPath) || sourcePath.startsWith('.')) return repoPath;
|
|
48
|
+
const packagePath = resolve(root, 'node_modules', sourcePath);
|
|
49
|
+
return existsSync(packagePath) ? packagePath : repoPath;
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
const mapToRegExpSource = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
41
53
|
|
|
42
54
|
/** Pure verdict — exported for tests. */
|
|
@@ -48,7 +60,7 @@ export function checkManifestVariables({ cssText, manifest, prefix, internalVari
|
|
|
48
60
|
const consumedVariables = new Set([...cssText.matchAll(new RegExp(`var\\(\\s*--(${prefixSource}-[a-z0-9-]+)`, 'gi'))].map((match) => match[1]));
|
|
49
61
|
const consumedBaseVariables = new Set([...cssText.matchAll(/var\(\s*--(base-[a-z0-9-]+)/gi)].map((match) => match[1]));
|
|
50
62
|
|
|
51
|
-
const componentVariablesInCss = checkMode === 'consumed' ? consumedVariables : declaredVariables;
|
|
63
|
+
const componentVariablesInCss = checkMode === 'consumed' ? consumedVariables : new Set([...declaredVariables, ...consumedVariables]);
|
|
52
64
|
const internalVariableSet = new Set(internalVariables);
|
|
53
65
|
const manifestComponentVariables = new Set((manifest.componentVariables || []).map((entry) => entry.name));
|
|
54
66
|
const manifestBaseVariables = new Set((manifest.baseVariables || []).map((entry) => entry.name));
|
|
@@ -68,6 +80,10 @@ export function checkManifestVariables({ cssText, manifest, prefix, internalVari
|
|
|
68
80
|
export async function run(args) {
|
|
69
81
|
const config = getKmwcConfig(args.root);
|
|
70
82
|
const isStrict = !!args.strict;
|
|
83
|
+
if (!config.cssVariables.isEnabled) {
|
|
84
|
+
console.log('kmwc check-vars: off (kmwc.cssVariables is false) — nothing to check.');
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
71
87
|
const manifestPath = resolve(config.root, config.cssVariables.manifest);
|
|
72
88
|
if (!existsSync(manifestPath)) {
|
|
73
89
|
console.log(`kmwc check-vars: no ${config.cssVariables.manifest} — nothing to check.`);
|
|
@@ -77,7 +93,7 @@ export async function run(args) {
|
|
|
77
93
|
const prefix = config.prefix || manifest.prefix;
|
|
78
94
|
if (!prefix) throw new Error('no variable prefix — set kmwc.prefix or "prefix" in the manifest');
|
|
79
95
|
|
|
80
|
-
const cssText = config.cssVariables.sources.flatMap((sourcePath) => getStylesheetTexts(
|
|
96
|
+
const cssText = config.cssVariables.sources.flatMap((sourcePath) => getStylesheetTexts(resolveSourcePath(config.root, sourcePath), [])).join('\n');
|
|
81
97
|
const verdict = checkManifestVariables({
|
|
82
98
|
cssText,
|
|
83
99
|
manifest,
|
|
@@ -86,7 +102,7 @@ export async function run(args) {
|
|
|
86
102
|
checkMode: config.cssVariables.checkMode,
|
|
87
103
|
});
|
|
88
104
|
|
|
89
|
-
const presenceWord = config.cssVariables.checkMode === 'consumed' ? 'consumed via var()' : 'declared';
|
|
105
|
+
const presenceWord = config.cssVariables.checkMode === 'consumed' ? 'consumed via var()' : 'declared or read';
|
|
90
106
|
const renderSection = (title, names) => `${title} (${names.length}):\n` + names.map((name) => ` --${name}`).join('\n');
|
|
91
107
|
const errors = [];
|
|
92
108
|
const warnings = [];
|
|
@@ -19,27 +19,56 @@ import { spawnSync } from 'node:child_process';
|
|
|
19
19
|
import { getKmwcConfig, getPortsForBlock, getRegistry, LEGACY_FILES, NON_PAGE_HTML_FILES, PACKAGE_LEAK_RULES, PACKAGE_FILES_EXCLUSIONS } from '../../config.js';
|
|
20
20
|
import { getSyncPlan, getScriptsPlan, CLAUDE_BLOCK_START } from '../sync-plan.js';
|
|
21
21
|
import { checkIsFragment } from '../../vite/page-composer.js';
|
|
22
|
+
import {
|
|
23
|
+
VALIDATION_NOTES_FILE,
|
|
24
|
+
loadAcceptedCheckIds,
|
|
25
|
+
getLibraryOutputMismatches,
|
|
26
|
+
getLibraryOutputPaths,
|
|
27
|
+
getLegacyGlobalName,
|
|
28
|
+
getPortBlockRange,
|
|
29
|
+
getScriptPortMismatches,
|
|
30
|
+
loadStalePortMentions,
|
|
31
|
+
loadSiblingPortCollisions,
|
|
32
|
+
loadOwnPortsOutsideBlock,
|
|
33
|
+
loadPlausibleSiteId,
|
|
34
|
+
loadSharedPlausibleSiteIds,
|
|
35
|
+
loadKitClassNames,
|
|
36
|
+
getKitDuplicates,
|
|
37
|
+
} from '../repo-checks.js';
|
|
22
38
|
|
|
23
39
|
const DEVKIT_PACKAGE_NAME = '@keenmate/web-components-devkit';
|
|
40
|
+
const LOCAL_STYLESHEET_FILE = 'examples-local.css';
|
|
24
41
|
const LEGACY_PAGE_REFERENCES = ['examples-shared.css', 'examples-chapter-nav.js', 'examples-copy-link.js', 'examples-controls-store.js'];
|
|
25
42
|
const VITE_CONFIG_FILES = ['vite.config.ts', 'vite.config.mjs', 'vite.config.js'];
|
|
26
43
|
const PLAYWRIGHT_CONFIG_FILES = ['playwright.config.ts', 'playwright.config.mjs', 'playwright.config.js'];
|
|
27
44
|
|
|
28
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Rulebook suites in the rulebook's canonical reading order; the prefix is
|
|
47
|
+
* passed where the script takes one. `isComponentOnly` suites check what a
|
|
48
|
+
* custom element ships (its CSS, manifest, overlay) — a `library` (registry
|
|
49
|
+
* kind, or kmwc.kind) skips them.
|
|
50
|
+
*/
|
|
29
51
|
export const RULEBOOK_SUITES = [
|
|
30
|
-
{ name: 'component-structure', isPrefixed: false },
|
|
31
|
-
{ name: 'naming-conventions', isPrefixed: true },
|
|
32
|
-
{ name: 'css-structure', isPrefixed: true },
|
|
33
|
-
{ name: 'theme-container', isPrefixed: true },
|
|
34
|
-
{ name: 'base-variables', isPrefixed: true },
|
|
35
|
-
{ name: 'color-scheme', isPrefixed: true },
|
|
36
|
-
{ name: 'readme-structure', isPrefixed: false },
|
|
37
|
-
{ name: 'custom-elements-manifest', isPrefixed: false },
|
|
38
|
-
{ name: 'responsive-overlay', isPrefixed: true },
|
|
39
|
-
{ name: 'publish-command', isPrefixed: false },
|
|
40
|
-
{ name: 'example-pages', isPrefixed: false },
|
|
52
|
+
{ name: 'component-structure', isPrefixed: false, isComponentOnly: true },
|
|
53
|
+
{ name: 'naming-conventions', isPrefixed: true, isComponentOnly: false },
|
|
54
|
+
{ name: 'css-structure', isPrefixed: true, isComponentOnly: true },
|
|
55
|
+
{ name: 'theme-container', isPrefixed: true, isComponentOnly: true },
|
|
56
|
+
{ name: 'base-variables', isPrefixed: true, isComponentOnly: true },
|
|
57
|
+
{ name: 'color-scheme', isPrefixed: true, isComponentOnly: true },
|
|
58
|
+
{ name: 'readme-structure', isPrefixed: false, isComponentOnly: false },
|
|
59
|
+
{ name: 'custom-elements-manifest', isPrefixed: false, isComponentOnly: true },
|
|
60
|
+
{ name: 'responsive-overlay', isPrefixed: true, isComponentOnly: true },
|
|
61
|
+
{ name: 'publish-command', isPrefixed: false, isComponentOnly: false },
|
|
62
|
+
{ name: 'example-pages', isPrefixed: false, isComponentOnly: false },
|
|
41
63
|
];
|
|
42
64
|
|
|
65
|
+
const LIBRARY_KIND = 'library';
|
|
66
|
+
|
|
67
|
+
/** The suites that apply to a repo of `kind`. Pure — exported for tests. */
|
|
68
|
+
export function getRulebookSuites(kind) {
|
|
69
|
+
return RULEBOOK_SUITES.filter((suite) => kind !== LIBRARY_KIND || !suite.isComponentOnly);
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
function getMajorVersion(versionRange) {
|
|
44
73
|
const match = String(versionRange || '').match(/(\d+)/);
|
|
45
74
|
return match ? Number(match[1]) : null;
|
|
@@ -85,7 +114,7 @@ export function checkComponent(config, doctorConfig = {}) {
|
|
|
85
114
|
} else {
|
|
86
115
|
addPass('config', 'package.json#kmwc present');
|
|
87
116
|
if (!config.packageJson.kmwc.title) addWarning('config', 'kmwc.title not set (falls back to a title-cased id)', 'set kmwc.title');
|
|
88
|
-
if (!config.prefix) addWarning('config', 'kmwc.prefix not set — rulebook suites run without a CSS prefix', 'set kmwc.prefix to the prefix reserved in base-variables.md');
|
|
117
|
+
if (!config.prefix && config.kind !== LIBRARY_KIND) addWarning('config', 'kmwc.prefix not set — rulebook suites run without a CSS prefix', 'set kmwc.prefix to the prefix reserved in base-variables.md');
|
|
89
118
|
if (!config.packageJson.kmwc.ports?.dev) addWarning('config', 'kmwc.ports.dev not set (falls back to 5173)', 'take the port from the devkit components.json');
|
|
90
119
|
}
|
|
91
120
|
|
|
@@ -102,6 +131,11 @@ export function checkComponent(config, doctorConfig = {}) {
|
|
|
102
131
|
if (!viteConfigFile) addError('vite', 'no vite.config.*', 'create one: export default defineComponentConfig()');
|
|
103
132
|
else if (!/defineComponentConfig|createExamplesPlugin/.test(readFileSync(join(config.root, viteConfigFile), 'utf8'))) {
|
|
104
133
|
addError('vite', `${viteConfigFile} doesn't use the devkit preset`, "import { defineComponentConfig } from '@keenmate/web-components-devkit/vite'");
|
|
134
|
+
// Before the switch: the preset derives the UMD global from kmwc — a silent rename breaks script-tag users.
|
|
135
|
+
const legacyGlobalName = getLegacyGlobalName(readFileSync(join(config.root, viteConfigFile), 'utf8'));
|
|
136
|
+
if (legacyGlobalName && config.library.shouldBuild && legacyGlobalName !== config.library.globalName) {
|
|
137
|
+
addError('vite', `the UMD global is "${legacyGlobalName}" today; the preset would make it "${config.library.globalName}"`, `set kmwc.library.globalName to "${legacyGlobalName}"`);
|
|
138
|
+
}
|
|
105
139
|
} else addPass('vite', `${viteConfigFile} uses the devkit preset`);
|
|
106
140
|
|
|
107
141
|
const playwrightConfigFile = getFirstExistingFile(config.root, PLAYWRIGHT_CONFIG_FILES);
|
|
@@ -148,6 +182,21 @@ export function checkComponent(config, doctorConfig = {}) {
|
|
|
148
182
|
);
|
|
149
183
|
}
|
|
150
184
|
|
|
185
|
+
// Page CSS that re-implements the kit (a local .kbd after the kit styled <kbd>, inline widths …).
|
|
186
|
+
const kitClassNames = loadKitClassNames();
|
|
187
|
+
const localStylesheetPath = join(config.root, LOCAL_STYLESHEET_FILE);
|
|
188
|
+
const kitDuplicateSources = [
|
|
189
|
+
...pageFiles.map((pageFile) => ({ file: pageFile, duplicates: getKitDuplicates(readFileSync(join(config.root, pageFile), 'utf8'), kitClassNames) })),
|
|
190
|
+
...(existsSync(localStylesheetPath)
|
|
191
|
+
? [{ file: LOCAL_STYLESHEET_FILE, duplicates: getKitDuplicates(readFileSync(localStylesheetPath, 'utf8'), kitClassNames, { isStylesheet: true }) }]
|
|
192
|
+
: []),
|
|
193
|
+
].filter((source) => source.duplicates.length);
|
|
194
|
+
for (const { file, duplicates } of kitDuplicateSources) {
|
|
195
|
+
const shownDuplicates = duplicates.slice(0, 3).map((duplicate) => `${duplicate.found} → ${duplicate.advice}`).join('; ');
|
|
196
|
+
addWarning('pages', `${file}: ${duplicates.length} rule(s) / style(s) the kit already covers — ${shownDuplicates}${duplicates.length > 3 ? ' …' : ''}`, 'use the kit (devkit README "Examples kit")');
|
|
197
|
+
}
|
|
198
|
+
if (pageFiles.length && !kitDuplicateSources.length) addPass('pages', 'no page CSS re-implements the kit');
|
|
199
|
+
|
|
151
200
|
// ── generated files ──────────────────────────────────────────────────────
|
|
152
201
|
if (config.hasPackageConfig) {
|
|
153
202
|
for (const planItem of getSyncPlan(config)) {
|
|
@@ -183,12 +232,37 @@ export function checkComponent(config, doctorConfig = {}) {
|
|
|
183
232
|
`set package.json#kmwc.ports to { "dev": ${registryPorts.dev}, "image": ${registryPorts.image} } (components.json port block ${registryEntry.port})`
|
|
184
233
|
);
|
|
185
234
|
} else addPass('registry', `ports match the registry (dev ${config.ports.dev}, image ${config.ports.image})`);
|
|
235
|
+
checkPorts(config, registryEntry.port, { addPass, addWarning, addError });
|
|
186
236
|
}
|
|
187
237
|
if (registryEntry.prefix && config.prefix && registryEntry.prefix !== config.prefix) {
|
|
188
238
|
addError('registry', `kmwc.prefix "${config.prefix}" ≠ registry prefix "${registryEntry.prefix}"`, 'use the reserved prefix');
|
|
189
239
|
}
|
|
190
240
|
}
|
|
191
241
|
|
|
242
|
+
// ── library output names ─────────────────────────────────────────────────
|
|
243
|
+
if (config.hasPackageConfig && config.library.shouldBuild) {
|
|
244
|
+
const outputMismatches = getLibraryOutputMismatches(config.packageJson, config.library);
|
|
245
|
+
for (const { field, target } of outputMismatches) {
|
|
246
|
+
addError(
|
|
247
|
+
'library',
|
|
248
|
+
`package.json#${field} → ${target}, but the library build emits ${getLibraryOutputPaths(config.library).join(', ')}`,
|
|
249
|
+
'set kmwc.library.fileName / cssFileName to the published names (or fix the package.json target)'
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (!outputMismatches.length) addPass('library', 'package.json targets match the library build outputs');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── analytics ────────────────────────────────────────────────────────────
|
|
256
|
+
const plausibleSiteId = loadPlausibleSiteId(config.root);
|
|
257
|
+
const sharingRepoIds = loadSharedPlausibleSiteIds(config, getRegistry(), plausibleSiteId);
|
|
258
|
+
if (sharingRepoIds.length) {
|
|
259
|
+
addError(
|
|
260
|
+
'analytics',
|
|
261
|
+
`Plausible site id ${plausibleSiteId} is also used by ${sharingRepoIds.join(', ')} — one site's traffic is counted as the other's`,
|
|
262
|
+
"set kmwc.analytics.plausibleSiteId to this site's own id"
|
|
263
|
+
);
|
|
264
|
+
} else if (plausibleSiteId) addPass('analytics', `Plausible site id ${plausibleSiteId} is this repo's own`);
|
|
265
|
+
|
|
192
266
|
// ── package contents ─────────────────────────────────────────────────────
|
|
193
267
|
if (doctorConfig.shouldCheckPackage !== false) checkPackageContents(config, { addPass, addWarning, addError });
|
|
194
268
|
|
|
@@ -198,6 +272,33 @@ export function checkComponent(config, doctorConfig = {}) {
|
|
|
198
272
|
return results;
|
|
199
273
|
}
|
|
200
274
|
|
|
275
|
+
/** Ports: script --port flags, own configs and docs outside the block, siblings inside it. */
|
|
276
|
+
function checkPorts(config, portBlock, { addPass, addWarning, addError }) {
|
|
277
|
+
const [firstPort, lastPort] = getPortBlockRange(portBlock);
|
|
278
|
+
const blockLabel = `${firstPort}–${lastPort}`;
|
|
279
|
+
const scriptPortMismatches = getScriptPortMismatches(config.packageJson.scripts, config.ports);
|
|
280
|
+
for (const { scriptName, port, expectedPort } of scriptPortMismatches) {
|
|
281
|
+
addError('ports', `script "${scriptName}" pins --port ${port}; kmwc.ports gives ${expectedPort}`, `drop the --port flag (the preset reads kmwc.ports)`);
|
|
282
|
+
}
|
|
283
|
+
const ownStrayPorts = loadOwnPortsOutsideBlock(config, portBlock).filter(({ file }) => !file.startsWith('package.json#scripts.'));
|
|
284
|
+
for (const { port, file } of ownStrayPorts) addError('ports', `${file} uses port ${port}, outside this repo's block ${blockLabel}`, 'take ports from kmwc.ports (the devkit presets do)');
|
|
285
|
+
const siblingCollisions = loadSiblingPortCollisions(config, getRegistry());
|
|
286
|
+
for (const { id, port, file } of siblingCollisions) {
|
|
287
|
+
addError('ports', `${id} uses port ${port} (${file}) — inside this repo's block ${blockLabel}`, `move ${id} to its own components.json block`);
|
|
288
|
+
}
|
|
289
|
+
const stalePortMentions = loadStalePortMentions(config.root, portBlock);
|
|
290
|
+
if (stalePortMentions.length) {
|
|
291
|
+
addWarning(
|
|
292
|
+
'ports',
|
|
293
|
+
`docs mention ports outside ${blockLabel}: ${stalePortMentions.map(({ file, port }) => `${file} ${port}`).join(', ')}`,
|
|
294
|
+
`update them to ${getPortsForBlock(portBlock).dev} (dev) / ${getPortsForBlock(portBlock).image} (image)`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (!scriptPortMismatches.length && !ownStrayPorts.length && !siblingCollisions.length && !stalePortMentions.length) {
|
|
298
|
+
addPass('ports', `scripts, configs, docs and sibling repos keep to block ${blockLabel}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
201
302
|
/** Packed paths that break PACKAGE_LEAK_RULES → [{ path, label }]. Pure — exported for tests. */
|
|
202
303
|
export function getPackageLeaks(packedPaths) {
|
|
203
304
|
return packedPaths
|
|
@@ -248,7 +349,13 @@ function checkRulebook(config, { addPass, addWarning, addError }) {
|
|
|
248
349
|
return;
|
|
249
350
|
}
|
|
250
351
|
const skippedSuites = new Set(config.rulebook.skippedChecks);
|
|
251
|
-
|
|
352
|
+
const acceptedCheckIds = loadAcceptedCheckIds(config.root);
|
|
353
|
+
const suites = getRulebookSuites(config.kind);
|
|
354
|
+
if (suites.length < RULEBOOK_SUITES.length) {
|
|
355
|
+
const componentOnlySuiteNames = RULEBOOK_SUITES.filter((suite) => !suites.includes(suite)).map((suite) => suite.name);
|
|
356
|
+
addPass('rulebook', `${config.kind}: component-only suites not run (${componentOnlySuiteNames.join(', ')})`);
|
|
357
|
+
}
|
|
358
|
+
for (const suite of suites) {
|
|
252
359
|
if (skippedSuites.has(suite.name)) continue;
|
|
253
360
|
if (!existsSync(join(config.rulebook.path, `${suite.name}.checks.sh`))) continue;
|
|
254
361
|
const suiteResult = runRulebookSuite(config, suite);
|
|
@@ -263,7 +370,11 @@ function checkRulebook(config, { addPass, addWarning, addError }) {
|
|
|
263
370
|
continue;
|
|
264
371
|
}
|
|
265
372
|
const failures = getSuiteFailures(suiteResult.stdout);
|
|
266
|
-
for (const failure of failures)
|
|
373
|
+
for (const failure of failures) {
|
|
374
|
+
// An architectural decision recorded in VALIDATION-NOTES.md (the rulebook's accepted-deviation file).
|
|
375
|
+
if (acceptedCheckIds.has(failure.checkId)) addPass(checkName, `${failure.checkId} accepted deviation (${VALIDATION_NOTES_FILE}): ${failure.reason}`);
|
|
376
|
+
else addError(checkName, `${failure.checkId} ${failure.reason}`, `see ${suite.name}.checks.md → ${failure.checkId}`);
|
|
377
|
+
}
|
|
267
378
|
if (!failures.length) addPass(checkName, suiteResult.status === 0 ? 'all auto checks pass' : `exit ${suiteResult.status}`);
|
|
268
379
|
}
|
|
269
380
|
}
|
|
@@ -280,11 +391,13 @@ export async function run(args) {
|
|
|
280
391
|
}
|
|
281
392
|
|
|
282
393
|
const LEVEL_ICONS = { pass: '✓', warning: '⚠', error: '✗' };
|
|
283
|
-
console.log(`kmwc doctor — ${config.packageName} (${config.root})\n`);
|
|
394
|
+
console.log(`kmwc doctor — ${config.packageName}${config.kind === 'component' ? '' : ` [${config.kind}]`} (${config.root})\n`);
|
|
284
395
|
for (const result of results) {
|
|
285
396
|
if (result.level === 'pass' && !args.verbose) continue;
|
|
286
397
|
console.log(`${LEVEL_ICONS[result.level]} [${result.checkName}] ${result.message}${result.fixHint ? `\n → ${result.fixHint}` : ''}`);
|
|
287
398
|
}
|
|
399
|
+
const acceptedCount = results.filter((result) => result.level === 'pass' && result.message.includes(`accepted deviation (${VALIDATION_NOTES_FILE})`)).length;
|
|
400
|
+
if (acceptedCount && !args.verbose) console.log(`ℹ ${acceptedCount} rulebook failure(s) accepted in ${VALIDATION_NOTES_FILE} (--verbose lists them)`);
|
|
288
401
|
console.log(`\n${errorCount ? '✗' : '✓'} ${errorCount} error(s), ${warningCount} warning(s)${args.verbose ? '' : ' (--verbose lists passing checks)'}`);
|
|
289
402
|
return errorCount ? 1 : 0;
|
|
290
403
|
}
|
package/src/cli/kmwc.js
CHANGED
|
@@ -5,62 +5,109 @@
|
|
|
5
5
|
* kmwc sync [--check] regenerate devkit-owned files from package.json#kmwc
|
|
6
6
|
* kmwc doctor [--json] check this repo against the devkit + rulebook standard
|
|
7
7
|
* kmwc check-vars [--strict] validate component-variables.manifest.json against the CSS
|
|
8
|
+
* kmwc check-pages [--url <u>] every example page in Chromium: errors, undefined elements, overflow
|
|
8
9
|
* kmwc check-release --tag <tag> gate a publish (version ⇔ dist-tag, not yet on npm)
|
|
9
10
|
* kmwc new <name> [options] scaffold a new component repo next to the devkit
|
|
10
11
|
* kmwc readme [--check] regenerate the devkit README catalogue (devkit repo only)
|
|
12
|
+
* kmwc <command> --help that command's usage (never runs it)
|
|
11
13
|
*
|
|
12
14
|
* All commands act on the current directory (override with --root <dir>).
|
|
13
|
-
* Each command module exports `run(args)` returning the process exit code
|
|
15
|
+
* Each command module exports `run(args)` returning the process exit code; an
|
|
16
|
+
* unknown option is an error (exit 2), never ignored.
|
|
14
17
|
*/
|
|
15
18
|
import { argv, exit } from 'node:process';
|
|
16
|
-
import { parseArgs } from './args.js';
|
|
19
|
+
import { parseArgs, checkIsHelpRequested } from './args.js';
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
readme: () => import('./commands/readme.js'),
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function renderUsage() {
|
|
28
|
-
console.log(`kmwc — Keenmate web-components devkit
|
|
29
|
-
|
|
30
|
-
Usage: kmwc <command> [options]
|
|
31
|
-
|
|
32
|
-
sync [--check] Regenerate Dockerfile, nginx.conf, Makefile, .dockerignore,
|
|
21
|
+
/** Every command: its module, the flags it takes and its usage text. */
|
|
22
|
+
const COMMANDS = {
|
|
23
|
+
sync: {
|
|
24
|
+
load: () => import('./commands/sync.js'),
|
|
25
|
+
flags: { booleans: ['check'] },
|
|
26
|
+
usage: `sync [--check] Regenerate Dockerfile, nginx.conf, Makefile, .dockerignore,
|
|
33
27
|
the CLAUDE.md devkit block, .gitignore entries and missing
|
|
34
|
-
standard scripts. --check: report drift, write nothing
|
|
35
|
-
|
|
28
|
+
standard scripts. --check: report drift, write nothing.`,
|
|
29
|
+
},
|
|
30
|
+
doctor: {
|
|
31
|
+
load: () => import('./commands/doctor.js'),
|
|
32
|
+
flags: { booleans: ['json', 'verbose', 'no-rulebook', 'no-pack'] },
|
|
33
|
+
usage: `doctor [--json] Check this repo against the devkit standard and the
|
|
36
34
|
BlissFramework rulebook (*.checks.sh), and that npm pack ships no
|
|
37
|
-
dev-only files. --verbose, --no-rulebook, --no-pack
|
|
38
|
-
|
|
39
|
-
check-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
dev-only files. --verbose, --no-rulebook, --no-pack.`,
|
|
36
|
+
},
|
|
37
|
+
'check-vars': {
|
|
38
|
+
load: () => import('./commands/check-vars.js'),
|
|
39
|
+
flags: { booleans: ['strict'] },
|
|
40
|
+
usage: `check-vars [--strict] Validate component-variables.manifest.json against the CSS.`,
|
|
41
|
+
},
|
|
42
|
+
'check-pages': {
|
|
43
|
+
load: () => import('./commands/check-pages.js'),
|
|
44
|
+
flags: { values: ['url', 'widths'] },
|
|
45
|
+
usage: `check-pages [--url <u>] Load every example page in Chromium at 1440 / 390 / 320px and report
|
|
46
|
+
page/console errors, failed requests, undefined elements and a page
|
|
47
|
+
wider than the viewport. Starts the dev server unless --url is given.
|
|
48
|
+
--widths 1440,390,320.`,
|
|
49
|
+
},
|
|
50
|
+
'check-release': {
|
|
51
|
+
load: () => import('./commands/check-release.js'),
|
|
52
|
+
flags: { booleans: ['offline'], values: ['tag'] },
|
|
53
|
+
usage: `check-release --tag <t> Gate a publish: latest ⇔ release version, rc ⇔ -rcN,
|
|
54
|
+
version not on npm yet. --offline skips the npm lookup.`,
|
|
55
|
+
},
|
|
56
|
+
new: {
|
|
57
|
+
load: () => import('./commands/new.js'),
|
|
58
|
+
flags: { booleans: ['register', 'minimal', 'adopt', 'offline'], values: ['title', 'tagline', 'tag', 'prefix', 'dir', 'port'] },
|
|
59
|
+
usage: `new <name> Scaffold a new component (e.g. \`kmwc new web-colorpicker --prefix cpk\`).
|
|
60
|
+
--title "…" --tagline "…" --tag … --prefix … --dir … --port …
|
|
61
|
+
--register --offline
|
|
43
62
|
--minimal: empty skeleton (no inputs/events) instead of the sample
|
|
44
|
-
--adopt: an already registered component (migration scratch scaffold)
|
|
45
|
-
|
|
63
|
+
--adopt: an already registered component (migration scratch scaffold)`,
|
|
64
|
+
},
|
|
65
|
+
readme: {
|
|
66
|
+
load: () => import('./commands/readme.js'),
|
|
67
|
+
flags: { booleans: ['check'] },
|
|
68
|
+
usage: `readme [--check] Regenerate the README catalogue from components.json (devkit repo).`,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const GLOBAL_USAGE = `Global: --root <dir> Act on <dir> instead of the current directory.
|
|
73
|
+
--help, -h Show the usage (of one command: kmwc <command> --help).`;
|
|
46
74
|
|
|
47
|
-
|
|
75
|
+
function renderUsage(commandName) {
|
|
76
|
+
if (commandName) {
|
|
77
|
+
console.log(`Usage: kmwc ${COMMANDS[commandName].usage}\n\n${GLOBAL_USAGE}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const commandLines = Object.values(COMMANDS).map((command) => ` ${command.usage}`).join('\n');
|
|
81
|
+
console.log(`kmwc — Keenmate web-components devkit\n\nUsage: kmwc <command> [options]\n\n${commandLines}\n\n${GLOBAL_USAGE}`);
|
|
48
82
|
}
|
|
49
83
|
|
|
50
84
|
async function main() {
|
|
51
85
|
const [commandName, ...commandArgs] = argv.slice(2);
|
|
52
86
|
if (!commandName || ['help', '--help', '-h'].includes(commandName)) {
|
|
53
|
-
renderUsage();
|
|
87
|
+
renderUsage(COMMANDS[commandArgs[0]] ? commandArgs[0] : undefined);
|
|
54
88
|
return 0;
|
|
55
89
|
}
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
90
|
+
const command = COMMANDS[commandName];
|
|
91
|
+
if (!command) {
|
|
58
92
|
console.error(`kmwc: unknown command "${commandName}"\n`);
|
|
59
93
|
renderUsage();
|
|
60
94
|
return 2;
|
|
61
95
|
}
|
|
62
|
-
|
|
63
|
-
|
|
96
|
+
// Help never runs the command (`kmwc sync --help` once overwrote a Makefile).
|
|
97
|
+
if (checkIsHelpRequested(commandArgs)) {
|
|
98
|
+
renderUsage(commandName);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
let args;
|
|
102
|
+
try {
|
|
103
|
+
args = parseArgs(commandArgs, command.flags);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(`kmwc ${commandName}: ${error.message}\n`);
|
|
106
|
+
renderUsage(commandName);
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
const commandModule = await command.load();
|
|
110
|
+
return (await commandModule.run(args)) ?? 0;
|
|
64
111
|
}
|
|
65
112
|
|
|
66
113
|
main().then(
|