@keenmate/web-components-devkit 0.0.0-stage → 1.0.0-rc02
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 +126 -0
- package/LICENSE +21 -0
- package/README.md +434 -2
- package/components.json +206 -0
- package/package.json +77 -4
- package/src/cli/args.js +16 -0
- package/src/cli/commands/check-release.js +66 -0
- package/src/cli/commands/check-vars.js +116 -0
- package/src/cli/commands/doctor.js +289 -0
- package/src/cli/commands/new.js +323 -0
- package/src/cli/commands/readme.js +111 -0
- package/src/cli/commands/sync.js +48 -0
- package/src/cli/kmwc.js +73 -0
- package/src/cli/sync-plan.js +163 -0
- package/src/config.d.ts +149 -0
- package/src/config.js +279 -0
- package/src/examples/auto.js +20 -0
- package/src/examples/chapter-nav.js +246 -0
- package/src/examples/constants.js +80 -0
- package/src/examples/controls-persistence.js +97 -0
- package/src/examples/copy-link.js +89 -0
- package/src/examples/css/base.css +215 -0
- package/src/examples/css/chapter-nav.css +179 -0
- package/src/examples/css/content.css +251 -0
- package/src/examples/css/controls.css +205 -0
- package/src/examples/css/copy-link.css +82 -0
- package/src/examples/css/demo.css +50 -0
- package/src/examples/css/index-page.css +118 -0
- package/src/examples/css/style.css +30 -0
- package/src/examples/css/theme-switch.css +38 -0
- package/src/examples/css/tokens.css +124 -0
- package/src/examples/css/widgets.css +133 -0
- package/src/examples/demo-code.js +30 -0
- package/src/examples/index-page.js +78 -0
- package/src/examples/index.d.ts +107 -0
- package/src/examples/index.js +18 -0
- package/src/examples/kit.js +22 -0
- package/src/examples/page-config.js +30 -0
- package/src/examples/sections.js +53 -0
- package/src/examples/theme-switch.js +72 -0
- package/src/examples/version-badge.js +23 -0
- package/src/playwright/index.d.ts +15 -0
- package/src/playwright/index.js +66 -0
- package/src/template.js +11 -0
- package/src/vite/index.d.ts +28 -0
- package/src/vite/index.js +243 -0
- package/src/vite/page-composer.js +216 -0
- package/templates/Dockerfile.tmpl +39 -0
- package/templates/Makefile.tmpl +136 -0
- package/templates/claude-block.md +40 -0
- package/templates/dockerignore.tmpl +10 -0
- package/templates/examples-layout.html +13 -0
- package/templates/new/.claude/commands/publish.md +226 -0
- package/templates/new/CHANGELOG.md +13 -0
- package/templates/new/LICENSE +21 -0
- package/templates/new/README.md +71 -0
- package/templates/new/_gitignore +7 -0
- package/templates/new/component-variables.manifest.json +32 -0
- package/templates/new/custom-elements-manifest.config.mjs +23 -0
- package/templates/new/docs/accessibility.md +24 -0
- package/templates/new/docs/examples.md +33 -0
- package/templates/new/docs/theming.md +54 -0
- package/templates/new/docs/usage.md +51 -0
- package/templates/new/e2e/dark-mode.spec.ts +30 -0
- package/templates/new/e2e/smoke.spec.ts +26 -0
- package/templates/new/examples-basic.html +57 -0
- package/templates/new/examples-theming.html +28 -0
- package/templates/new/index.html +16 -0
- package/templates/new/partials/note.html +5 -0
- package/templates/new/playwright.config.ts +4 -0
- package/templates/new/src/__FEATURE__.ts +55 -0
- package/templates/new/src/css/animations.css +1 -0
- package/templates/new/src/css/base.css +43 -0
- package/templates/new/src/css/controls.css +20 -0
- package/templates/new/src/css/dark-mode.css +36 -0
- package/templates/new/src/css/floating.css +1 -0
- package/templates/new/src/css/main.css +27 -0
- package/templates/new/src/css/states.css +22 -0
- package/templates/new/src/css/variables.css +42 -0
- package/templates/new/src/env.d.ts +7 -0
- package/templates/new/src/index.ts +17 -0
- package/templates/new/src/logger.ts +11 -0
- package/templates/new/src/types.ts +25 -0
- package/templates/new/src/web-component.ts +73 -0
- package/templates/new/test/dark-mode.html +18 -0
- package/templates/new/test/fixture.html +11 -0
- package/templates/new/tsconfig.json +15 -0
- package/templates/new/vite.config.ts +5 -0
- package/templates/new-minimal/CHANGELOG.md +13 -0
- package/templates/new-minimal/README.md +71 -0
- package/templates/new-minimal/component-variables.manifest.json +24 -0
- package/templates/new-minimal/docs/accessibility.md +20 -0
- package/templates/new-minimal/docs/examples.md +17 -0
- package/templates/new-minimal/docs/theming.md +53 -0
- package/templates/new-minimal/docs/usage.md +43 -0
- package/templates/new-minimal/e2e/smoke.spec.ts +10 -0
- package/templates/new-minimal/examples-basic.html +23 -0
- package/templates/new-minimal/examples-theming.html +28 -0
- package/templates/new-minimal/src/__FEATURE__.ts +37 -0
- package/templates/new-minimal/src/css/base.css +48 -0
- package/templates/new-minimal/src/css/controls.css +1 -0
- package/templates/new-minimal/src/css/states.css +1 -0
- package/templates/new-minimal/src/css/variables.css +28 -0
- package/templates/new-minimal/src/index.ts +17 -0
- package/templates/new-minimal/src/types.ts +10 -0
- package/templates/new-minimal/src/web-component.ts +48 -0
- package/templates/new-minimal/test/dark-mode.html +18 -0
- package/templates/new-minimal/test/fixture.html +11 -0
- package/templates/nginx.conf.tmpl +43 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kmwc doctor — check a component repo against the Keenmate standard.
|
|
3
|
+
*
|
|
4
|
+
* Two halves:
|
|
5
|
+
* 1. Devkit checks — drift prevention: every check is something a past
|
|
6
|
+
* session did locally that the devkit now owns (a forked helper script, a
|
|
7
|
+
* hand-edited Dockerfile, a port picked at random…).
|
|
8
|
+
* 2. Rulebook checks — runs the BlissFramework `guidelines/web-components/
|
|
9
|
+
* *.checks.sh` suites (path: kmwc.rulebook.path / KMWC_RULEBOOK_PATH) and
|
|
10
|
+
* reports their FAILs. The rulebook stays the single source of truth; when
|
|
11
|
+
* it isn't checked out (or bash is missing) the suites are skipped with a
|
|
12
|
+
* warning. `--no-rulebook` skips them on purpose.
|
|
13
|
+
*
|
|
14
|
+
* Errors fail (exit 1); warnings don't.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { getKmwcConfig, getPortsForBlock, getRegistry, LEGACY_FILES, NON_PAGE_HTML_FILES, PACKAGE_LEAK_RULES, PACKAGE_FILES_EXCLUSIONS } from '../../config.js';
|
|
20
|
+
import { getSyncPlan, getScriptsPlan, CLAUDE_BLOCK_START } from '../sync-plan.js';
|
|
21
|
+
import { checkIsFragment } from '../../vite/page-composer.js';
|
|
22
|
+
|
|
23
|
+
const DEVKIT_PACKAGE_NAME = '@keenmate/web-components-devkit';
|
|
24
|
+
const LEGACY_PAGE_REFERENCES = ['examples-shared.css', 'examples-chapter-nav.js', 'examples-copy-link.js', 'examples-controls-store.js'];
|
|
25
|
+
const VITE_CONFIG_FILES = ['vite.config.ts', 'vite.config.mjs', 'vite.config.js'];
|
|
26
|
+
const PLAYWRIGHT_CONFIG_FILES = ['playwright.config.ts', 'playwright.config.mjs', 'playwright.config.js'];
|
|
27
|
+
|
|
28
|
+
/** Rulebook suites in the rulebook's canonical reading order; the prefix is passed where the script takes one. */
|
|
29
|
+
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
|
+
];
|
|
41
|
+
|
|
42
|
+
function getMajorVersion(versionRange) {
|
|
43
|
+
const match = String(versionRange || '').match(/(\d+)/);
|
|
44
|
+
return match ? Number(match[1]) : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getFirstExistingFile(root, fileNames) {
|
|
48
|
+
return fileNames.find((fileName) => existsSync(join(root, fileName))) || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Strip ANSI colour codes (the suites colour output when they think they're on a TTY). */
|
|
52
|
+
const mapToPlainText = (text) => text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse a suite's output into failures: lines shaped `❌ FAIL <id> — <reason>`.
|
|
56
|
+
* Exported for tests.
|
|
57
|
+
*/
|
|
58
|
+
export function getSuiteFailures(suiteOutput) {
|
|
59
|
+
return mapToPlainText(suiteOutput)
|
|
60
|
+
.split(/\r?\n/)
|
|
61
|
+
.map((line) => line.match(/FAIL\s+(\S+)\s+[—-]+\s+(.*)$/))
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((match) => ({ checkId: match[1], reason: match[2].trim() }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runRulebookSuite(config, suite) {
|
|
67
|
+
const scriptPath = join(config.rulebook.path, `${suite.name}.checks.sh`).replace(/\\/g, '/');
|
|
68
|
+
const scriptArgs = [scriptPath, config.root.replace(/\\/g, '/')];
|
|
69
|
+
if (suite.isPrefixed && config.prefix) scriptArgs.push(config.prefix);
|
|
70
|
+
return spawnSync('bash', scriptArgs, { encoding: 'utf8', env: { ...process.env, NO_COLOR: '1' } });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Run every check; returns [{ level: 'pass'|'warning'|'error', checkName, message, fixHint? }]. */
|
|
74
|
+
export function checkComponent(config, doctorConfig = {}) {
|
|
75
|
+
const results = [];
|
|
76
|
+
const addPass = (checkName, message) => results.push({ level: 'pass', checkName, message });
|
|
77
|
+
const addWarning = (checkName, message, fixHint) => results.push({ level: 'warning', checkName, message, fixHint });
|
|
78
|
+
const addError = (checkName, message, fixHint) => results.push({ level: 'error', checkName, message, fixHint });
|
|
79
|
+
const dependencies = { ...config.packageJson.dependencies, ...config.packageJson.devDependencies };
|
|
80
|
+
|
|
81
|
+
// ── kmwc section ─────────────────────────────────────────────────────────
|
|
82
|
+
if (!config.hasPackageConfig) {
|
|
83
|
+
addError('config', 'package.json has no "kmwc" section', 'add { "kmwc": { "title", "prefix", "ports", … } } — see the devkit README');
|
|
84
|
+
} else {
|
|
85
|
+
addPass('config', 'package.json#kmwc present');
|
|
86
|
+
if (!config.packageJson.kmwc.title) addWarning('config', 'kmwc.title not set (falls back to a title-cased id)', 'set kmwc.title');
|
|
87
|
+
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');
|
|
88
|
+
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');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── dependencies ─────────────────────────────────────────────────────────
|
|
92
|
+
if (!dependencies[DEVKIT_PACKAGE_NAME]) addError('deps', `${DEVKIT_PACKAGE_NAME} is not a devDependency`, `npm i -D ${DEVKIT_PACKAGE_NAME}`);
|
|
93
|
+
else addPass('deps', `${DEVKIT_PACKAGE_NAME} ${dependencies[DEVKIT_PACKAGE_NAME]}`);
|
|
94
|
+
const viteMajorVersion = getMajorVersion(dependencies.vite);
|
|
95
|
+
if (viteMajorVersion == null) addError('deps', 'vite is not a dependency', 'npm i -D vite@^7');
|
|
96
|
+
else if (viteMajorVersion < 7) addError('deps', `vite ${dependencies.vite} — the standard is ^7`, 'npm i -D vite@^7');
|
|
97
|
+
else addPass('deps', `vite ${dependencies.vite}`);
|
|
98
|
+
|
|
99
|
+
// ── configs use the presets ──────────────────────────────────────────────
|
|
100
|
+
const viteConfigFile = getFirstExistingFile(config.root, VITE_CONFIG_FILES);
|
|
101
|
+
if (!viteConfigFile) addError('vite', 'no vite.config.*', 'create one: export default defineComponentConfig()');
|
|
102
|
+
else if (!/defineComponentConfig|createExamplesPlugin/.test(readFileSync(join(config.root, viteConfigFile), 'utf8'))) {
|
|
103
|
+
addError('vite', `${viteConfigFile} doesn't use the devkit preset`, "import { defineComponentConfig } from '@keenmate/web-components-devkit/vite'");
|
|
104
|
+
} else addPass('vite', `${viteConfigFile} uses the devkit preset`);
|
|
105
|
+
|
|
106
|
+
const playwrightConfigFile = getFirstExistingFile(config.root, PLAYWRIGHT_CONFIG_FILES);
|
|
107
|
+
if (playwrightConfigFile && !/definePlaywrightConfig/.test(readFileSync(join(config.root, playwrightConfigFile), 'utf8'))) {
|
|
108
|
+
addWarning(
|
|
109
|
+
'playwright',
|
|
110
|
+
`${playwrightConfigFile} doesn't use the devkit preset (port may drift from kmwc.ports.dev)`,
|
|
111
|
+
"import { definePlaywrightConfig } from '@keenmate/web-components-devkit/playwright'"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── legacy copies of devkit-owned files ──────────────────────────────────
|
|
116
|
+
const legacyFiles = LEGACY_FILES.filter((fileName) => existsSync(join(config.root, fileName)));
|
|
117
|
+
for (const legacyFile of legacyFiles) addError('legacy', `${legacyFile} is a local copy of a devkit-owned file`, 'delete it — the devkit provides it');
|
|
118
|
+
if (!legacyFiles.length) addPass('legacy', 'no local copies of devkit-owned files');
|
|
119
|
+
|
|
120
|
+
const pageFiles = readdirSync(config.root).filter((fileName) => fileName.endsWith('.html') && !NON_PAGE_HTML_FILES.includes(fileName));
|
|
121
|
+
let legacyPageCount = 0;
|
|
122
|
+
for (const pageFile of pageFiles) {
|
|
123
|
+
const pageHtml = readFileSync(join(config.root, pageFile), 'utf8');
|
|
124
|
+
const legacyReferences = LEGACY_PAGE_REFERENCES.filter((reference) => pageHtml.includes(reference));
|
|
125
|
+
if (legacyReferences.length) {
|
|
126
|
+
legacyPageCount++;
|
|
127
|
+
addError(
|
|
128
|
+
'pages',
|
|
129
|
+
`${pageFile} still references ${legacyReferences.join(', ')}`,
|
|
130
|
+
'remove the <link>/<script> tags (the plugin injects the kit); controls → attachControlsPersistence from the devkit'
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (pageFiles.length && !legacyPageCount) addPass('pages', `${pageFiles.length} page(s), none reference legacy helpers`);
|
|
135
|
+
|
|
136
|
+
// Pages still carrying their own <head> + header chrome — the copy-paste
|
|
137
|
+
// that fragment pages (shared layout) remove.
|
|
138
|
+
const boilerplatePages = pageFiles.filter((pageFile) => {
|
|
139
|
+
const pageHtml = readFileSync(join(config.root, pageFile), 'utf8');
|
|
140
|
+
return !checkIsFragment(pageHtml) && /class="back-link"/.test(pageHtml);
|
|
141
|
+
});
|
|
142
|
+
if (boilerplatePages.length) {
|
|
143
|
+
addWarning(
|
|
144
|
+
'pages',
|
|
145
|
+
`${boilerplatePages.length} page(s) repeat the head/header boilerplate: ${boilerplatePages.join(', ')}`,
|
|
146
|
+
'make them fragment pages — <meta name="kmwc:title">, no <html>/<head>/<header> (see the devkit README "Fragment pages")'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── generated files ──────────────────────────────────────────────────────
|
|
151
|
+
if (config.hasPackageConfig) {
|
|
152
|
+
for (const planItem of getSyncPlan(config)) {
|
|
153
|
+
if (!planItem.isChanged) {
|
|
154
|
+
addPass('sync', `${planItem.path} in sync`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const state = planItem.currentText == null ? 'missing' : 'out of sync (edited locally or template changed)';
|
|
158
|
+
if (planItem.kind === 'generated') addError('sync', `${planItem.path} ${state}`, 'npx kmwc sync (repo-specific make targets → Makefile.local)');
|
|
159
|
+
else if (planItem.kind === 'claude') {
|
|
160
|
+
(planItem.currentText?.includes(CLAUDE_BLOCK_START) ? addWarning : addError)('sync', `CLAUDE.md devkit block ${state}`, 'npx kmwc sync');
|
|
161
|
+
} else addWarning('sync', `${planItem.path} ${state}`, 'npx kmwc sync');
|
|
162
|
+
}
|
|
163
|
+
const { missingScripts, differingScripts } = getScriptsPlan(config);
|
|
164
|
+
if (Object.keys(missingScripts).length) addWarning('scripts', `missing standard scripts: ${Object.keys(missingScripts).join(', ')}`, 'npx kmwc sync');
|
|
165
|
+
for (const [scriptName, difference] of Object.entries(differingScripts)) {
|
|
166
|
+
addWarning('scripts', `"${scriptName}" is "${difference.currentCommand}", standard is "${difference.standardCommand}"`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── registry cross-check ─────────────────────────────────────────────────
|
|
171
|
+
const registryEntry = getRegistry().components.find((component) => component.id === config.id);
|
|
172
|
+
if (!registryEntry) {
|
|
173
|
+
addWarning('registry', `"${config.id}" is not in the devkit components.json`, 'add an entry (README catalogue + port allocation)');
|
|
174
|
+
} else {
|
|
175
|
+
if (registryEntry.port) {
|
|
176
|
+
const registryPorts = getPortsForBlock(registryEntry.port);
|
|
177
|
+
const mismatchedPorts = ['dev', 'image'].filter((portName) => registryPorts[portName] !== config.ports[portName]);
|
|
178
|
+
if (mismatchedPorts.length) {
|
|
179
|
+
addError(
|
|
180
|
+
'registry',
|
|
181
|
+
mismatchedPorts.map((portName) => `ports.${portName} ${config.ports[portName]} ≠ registry ${registryPorts[portName]}`).join('; '),
|
|
182
|
+
`set package.json#kmwc.ports to { "dev": ${registryPorts.dev}, "image": ${registryPorts.image} } (components.json port block ${registryEntry.port})`
|
|
183
|
+
);
|
|
184
|
+
} else addPass('registry', `ports match the registry (dev ${config.ports.dev}, image ${config.ports.image})`);
|
|
185
|
+
}
|
|
186
|
+
if (registryEntry.prefix && config.prefix && registryEntry.prefix !== config.prefix) {
|
|
187
|
+
addError('registry', `kmwc.prefix "${config.prefix}" ≠ registry prefix "${registryEntry.prefix}"`, 'use the reserved prefix');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── package contents ─────────────────────────────────────────────────────
|
|
192
|
+
if (doctorConfig.shouldCheckPackage !== false) checkPackageContents(config, { addPass, addWarning, addError });
|
|
193
|
+
|
|
194
|
+
// ── rulebook suites ──────────────────────────────────────────────────────
|
|
195
|
+
if (doctorConfig.shouldRunRulebook !== false) checkRulebook(config, { addPass, addWarning, addError });
|
|
196
|
+
|
|
197
|
+
return results;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Packed paths that break PACKAGE_LEAK_RULES → [{ path, label }]. Pure — exported for tests. */
|
|
201
|
+
export function getPackageLeaks(packedPaths) {
|
|
202
|
+
return packedPaths
|
|
203
|
+
.map((packedPath) => packedPath.replace(/\\/g, '/').replace(/^package\//, ''))
|
|
204
|
+
.flatMap((packedPath) => {
|
|
205
|
+
const leakRule = PACKAGE_LEAK_RULES.find((rule) => rule.pattern.test(packedPath));
|
|
206
|
+
return leakRule ? [{ path: packedPath, label: leakRule.label }] : [];
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The file list `npm publish` would ship (no lifecycle scripts, nothing written). */
|
|
211
|
+
function getPackedPaths(config) {
|
|
212
|
+
const packResult = spawnSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'], {
|
|
213
|
+
cwd: config.root,
|
|
214
|
+
encoding: 'utf8',
|
|
215
|
+
shell: process.platform === 'win32',
|
|
216
|
+
});
|
|
217
|
+
if (packResult.error || packResult.status !== 0) return null;
|
|
218
|
+
const packReport = JSON.parse(packResult.stdout.slice(packResult.stdout.indexOf('[')));
|
|
219
|
+
return packReport[0].files.map((file) => file.path);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function checkPackageContents(config, { addPass, addWarning, addError }) {
|
|
223
|
+
let packedPaths;
|
|
224
|
+
try {
|
|
225
|
+
packedPaths = getPackedPaths(config);
|
|
226
|
+
} catch {
|
|
227
|
+
packedPaths = null;
|
|
228
|
+
}
|
|
229
|
+
if (!packedPaths) {
|
|
230
|
+
addWarning('package', 'npm pack --dry-run failed — package contents not checked');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const packageLeaks = getPackageLeaks(packedPaths);
|
|
234
|
+
for (const packageLeak of packageLeaks) {
|
|
235
|
+
addError(
|
|
236
|
+
'package',
|
|
237
|
+
`${packageLeak.path} would be published (${packageLeak.label})`,
|
|
238
|
+
`exclude it in package.json "files" (e.g. ${PACKAGE_FILES_EXCLUSIONS.map((exclusion) => `"${exclusion}"`).join(', ')}) or narrow the glob`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (!packageLeaks.length) addPass('package', `${packedPaths.length} packed file(s), no dev-only files`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function checkRulebook(config, { addPass, addWarning, addError }) {
|
|
245
|
+
if (!existsSync(config.rulebook.path)) {
|
|
246
|
+
addWarning('rulebook', `rulebook not found at ${config.rulebook.path} — rulebook checks skipped`, 'check out BlissFramework or set KMWC_RULEBOOK_PATH / kmwc.rulebook.path');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const skippedSuites = new Set(config.rulebook.skippedChecks);
|
|
250
|
+
for (const suite of RULEBOOK_SUITES) {
|
|
251
|
+
if (skippedSuites.has(suite.name)) continue;
|
|
252
|
+
if (!existsSync(join(config.rulebook.path, `${suite.name}.checks.sh`))) continue;
|
|
253
|
+
const suiteResult = runRulebookSuite(config, suite);
|
|
254
|
+
if (suiteResult.error) {
|
|
255
|
+
addWarning('rulebook', `cannot run bash (${suiteResult.error.code}) — rulebook checks skipped`, 'install Git Bash / run from a POSIX shell');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const checkName = `rulebook:${suite.name}`;
|
|
259
|
+
if (suiteResult.status === 2) {
|
|
260
|
+
const reason = mapToPlainText(suiteResult.stderr || suiteResult.stdout).trim().split(/\r?\n/)[0];
|
|
261
|
+
addWarning(checkName, `suite could not run: ${reason}`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const failures = getSuiteFailures(suiteResult.stdout);
|
|
265
|
+
for (const failure of failures) addError(checkName, `${failure.checkId} ${failure.reason}`, `see ${suite.name}.checks.md → ${failure.checkId}`);
|
|
266
|
+
if (!failures.length) addPass(checkName, suiteResult.status === 0 ? 'all auto checks pass' : `exit ${suiteResult.status}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function run(args) {
|
|
271
|
+
const config = getKmwcConfig(args.root);
|
|
272
|
+
const results = checkComponent(config, { shouldRunRulebook: !args['no-rulebook'], shouldCheckPackage: !args['no-pack'] });
|
|
273
|
+
const errorCount = results.filter((result) => result.level === 'error').length;
|
|
274
|
+
const warningCount = results.filter((result) => result.level === 'warning').length;
|
|
275
|
+
|
|
276
|
+
if (args.json) {
|
|
277
|
+
console.log(JSON.stringify({ id: config.id, errorCount, warningCount, results }, null, 2));
|
|
278
|
+
return errorCount ? 1 : 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const LEVEL_ICONS = { pass: '✓', warning: '⚠', error: '✗' };
|
|
282
|
+
console.log(`kmwc doctor — ${config.packageName} (${config.root})\n`);
|
|
283
|
+
for (const result of results) {
|
|
284
|
+
if (result.level === 'pass' && !args.verbose) continue;
|
|
285
|
+
console.log(`${LEVEL_ICONS[result.level]} [${result.checkName}] ${result.message}${result.fixHint ? `\n → ${result.fixHint}` : ''}`);
|
|
286
|
+
}
|
|
287
|
+
console.log(`\n${errorCount ? '✗' : '✓'} ${errorCount} error(s), ${warningCount} warning(s)${args.verbose ? '' : ' (--verbose lists passing checks)'}`);
|
|
288
|
+
return errorCount ? 1 : 0;
|
|
289
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kmwc new <name> — scaffold a component repo in the rulebook shape.
|
|
3
|
+
*
|
|
4
|
+
* kmwc new web-colorpicker --prefix cpk [--title "Web Color Picker"] [--tag web-colorpicker]
|
|
5
|
+
* [--dir ../web-colorpicker] [--port 13200] [--minimal] [--register] [--offline]
|
|
6
|
+
* kmwc new web-editor --adopt --dir /tmp/web-editor [--prefix …]
|
|
7
|
+
*
|
|
8
|
+
* Default: a small working sample (a toggle with `label` / `active` inputs and
|
|
9
|
+
* a `change` event) to rename or replace. `--minimal`: an empty skeleton — no
|
|
10
|
+
* inputs, no events, a slotted root, stub feature CSS — with the same
|
|
11
|
+
* structure and the same passing rulebook checks.
|
|
12
|
+
*
|
|
13
|
+
* Runs the intake (component-intake.md): the npm name must be free, and the
|
|
14
|
+
* CSS prefix must be 2–4 lowercase letters not reserved in the rulebook's
|
|
15
|
+
* base-variables.md table or in components.json. A prefix is derived only for
|
|
16
|
+
* hyphen-separated names (initials) or short single words (tech letter + first
|
|
17
|
+
* letter); anything ambiguous needs --prefix, because word count is a meaning
|
|
18
|
+
* question ("daterangepicker" is three words) a script can't answer.
|
|
19
|
+
*
|
|
20
|
+
* Produces Element / Logic / Side-layer sources, the canonical CSS set, a
|
|
21
|
+
* Custom Elements Manifest config, README + docs/, CHANGELOG, LICENSE,
|
|
22
|
+
* `.claude/commands/publish.md`, examples, fixtures + e2e specs, then the
|
|
23
|
+
* `kmwc sync` files. The port block (dev = block, image = +10) is the next free
|
|
24
|
+
* one in the component range (see PORT_RANGES in config.js; `--port` overrides).
|
|
25
|
+
*
|
|
26
|
+
* `--adopt` is for MIGRATING a component that is already registered: the id must
|
|
27
|
+
* be in components.json, the port block (and prefix, unless the entry has none —
|
|
28
|
+
* then `--prefix`) come from its entry, the npm-name check is skipped (the package
|
|
29
|
+
* may exist), and the registry is left alone. Scaffold into a scratch `--dir`,
|
|
30
|
+
* then move the component's own code into the generated shape.
|
|
31
|
+
*/
|
|
32
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
34
|
+
import { spawnSync } from 'node:child_process';
|
|
35
|
+
import {
|
|
36
|
+
DEFAULT_RULEBOOK_PATH,
|
|
37
|
+
DEVKIT_ROOT,
|
|
38
|
+
getDevkitPackageJson,
|
|
39
|
+
getJsonFile,
|
|
40
|
+
getNextPortBlock,
|
|
41
|
+
getPortsForBlock,
|
|
42
|
+
getRegistry,
|
|
43
|
+
PORT_BLOCK_SIZE,
|
|
44
|
+
PACKAGE_FILES_EXCLUSIONS,
|
|
45
|
+
getUnscopedName,
|
|
46
|
+
mapToPascalCase,
|
|
47
|
+
} from '../../config.js';
|
|
48
|
+
import { renderTemplate } from '../sync-plan.js';
|
|
49
|
+
import { run as runSync } from './sync.js';
|
|
50
|
+
|
|
51
|
+
const SCAFFOLD_ROOT = join(DEVKIT_ROOT, 'templates', 'new');
|
|
52
|
+
/** --minimal: rendered over the full scaffold, replacing only the files that differ. */
|
|
53
|
+
const MINIMAL_OVERLAY_ROOT = join(DEVKIT_ROOT, 'templates', 'new-minimal');
|
|
54
|
+
const PREFIX_PATTERN = /^[a-z]{2,4}$/;
|
|
55
|
+
/** Prefixes used outside component CSS that must not be taken either. */
|
|
56
|
+
const INFRASTRUCTURE_PREFIXES = ['kmwc', 'base'];
|
|
57
|
+
const SHORT_SINGLE_WORD_LENGTH = 6;
|
|
58
|
+
/** Template file names → generated names. */
|
|
59
|
+
const FILE_NAME_PLACEHOLDERS = { _gitignore: '.gitignore' };
|
|
60
|
+
|
|
61
|
+
/** Prefixes reserved in the rulebook (base-variables.md "Existing prefixes" table) — empty when not checked out. */
|
|
62
|
+
export function getRulebookPrefixes(rulebookPath = DEFAULT_RULEBOOK_PATH) {
|
|
63
|
+
const baseVariablesPath = join(rulebookPath, 'base-variables.md');
|
|
64
|
+
if (!existsSync(baseVariablesPath)) return [];
|
|
65
|
+
const tableRows = readFileSync(baseVariablesPath, 'utf8').matchAll(/^\|\s*`@keenmate\/[^`]+`\s*\|\s*`([a-z]+)`\s*\|/gm);
|
|
66
|
+
return [...new Set([...tableRows].map((match) => match[1]))];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getReservedPrefixes(registry = getRegistry(), rulebookPath = DEFAULT_RULEBOOK_PATH) {
|
|
70
|
+
const registryPrefixes = registry.components.map((entry) => entry.prefix).filter(Boolean);
|
|
71
|
+
return new Set([...getRulebookPrefixes(rulebookPath), ...registryPrefixes, ...INFRASTRUCTURE_PREFIXES]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* component-intake.md "C — CSS prefix derivation", for the cases a script can
|
|
76
|
+
* decide. Returns null when the word count is ambiguous (the caller asks for --prefix).
|
|
77
|
+
*/
|
|
78
|
+
export function generatePrefix(componentId) {
|
|
79
|
+
const nameWords = componentId.replace(/^(web|svelte)-/, '').split('-').filter(Boolean);
|
|
80
|
+
const techLetter = componentId.startsWith('svelte-') ? 's' : 'w';
|
|
81
|
+
if (nameWords.length > 1) return nameWords.map((word) => word[0]).join('');
|
|
82
|
+
if (nameWords[0].length <= SHORT_SINGLE_WORD_LENGTH) return techLetter + nameWords[0][0];
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Error message for an unusable prefix, or null. */
|
|
87
|
+
export function checkPrefix(prefix, reservedPrefixes) {
|
|
88
|
+
if (!PREFIX_PATTERN.test(prefix)) return `prefix "${prefix}" must be 2–4 lowercase letters (component-intake.md)`;
|
|
89
|
+
if (reservedPrefixes.has(prefix)) return `prefix "${prefix}" is already reserved — pick another (base-variables.md "Existing prefixes")`;
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function checkIsPackageNameTaken(packageName) {
|
|
94
|
+
const result = spawnSync('npm', ['view', packageName, 'name'], { encoding: 'utf8', shell: true });
|
|
95
|
+
return result.status === 0 && result.stdout.trim() === packageName;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getFilesRecursive(directoryPath, filePaths = []) {
|
|
99
|
+
for (const entryName of readdirSync(directoryPath)) {
|
|
100
|
+
const entryPath = join(directoryPath, entryName);
|
|
101
|
+
if (statSync(entryPath).isDirectory()) getFilesRecursive(entryPath, filePaths);
|
|
102
|
+
else filePaths.push(entryPath);
|
|
103
|
+
}
|
|
104
|
+
return filePaths;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Render every template under `templateRoot` into `targetPath` (later calls overwrite earlier files). */
|
|
108
|
+
function writeScaffoldFiles(templateRoot, targetPath, variables, featureName) {
|
|
109
|
+
for (const templatePath of getFilesRecursive(templateRoot)) {
|
|
110
|
+
const relativePath = relative(templateRoot, templatePath)
|
|
111
|
+
.replace(/__FEATURE__/g, featureName)
|
|
112
|
+
.split(/[\\/]/)
|
|
113
|
+
.map((segment) => FILE_NAME_PLACEHOLDERS[segment] || segment)
|
|
114
|
+
.join('/');
|
|
115
|
+
const targetFilePath = join(targetPath, relativePath);
|
|
116
|
+
mkdirSync(dirname(targetFilePath), { recursive: true });
|
|
117
|
+
writeFileSync(targetFilePath, renderTemplate(readFileSync(templatePath, 'utf8'), variables));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function generatePackageJson({ componentId, packageName, tagline, tag, prefix, title, devPort, coreVersion, devkitVersion, logicClass }) {
|
|
122
|
+
return {
|
|
123
|
+
name: packageName,
|
|
124
|
+
version: '0.1.0',
|
|
125
|
+
description: tagline,
|
|
126
|
+
type: 'module',
|
|
127
|
+
license: 'MIT',
|
|
128
|
+
author: 'Keenmate s.r.o.',
|
|
129
|
+
homepage: `https://github.com/keenmate/${componentId}`,
|
|
130
|
+
repository: { type: 'git', url: `git+https://github.com/keenmate/${componentId}.git` },
|
|
131
|
+
keywords: ['web-components', 'custom-elements', 'keenmate', 'bliss', tag],
|
|
132
|
+
main: `./dist/${componentId}.umd.js`,
|
|
133
|
+
module: `./dist/${componentId}.js`,
|
|
134
|
+
types: './dist/index.d.ts',
|
|
135
|
+
customElements: 'custom-elements.json',
|
|
136
|
+
'web-types': 'web-types.json',
|
|
137
|
+
exports: {
|
|
138
|
+
'.': { types: './dist/index.d.ts', import: `./dist/${componentId}.js`, require: `./dist/${componentId}.umd.js` },
|
|
139
|
+
'./style.css': './dist/style.css',
|
|
140
|
+
'./manifest': './component-variables.manifest.json',
|
|
141
|
+
'./component-variables.manifest.json': './component-variables.manifest.json',
|
|
142
|
+
'./custom-elements.json': './custom-elements.json',
|
|
143
|
+
'./package.json': './package.json',
|
|
144
|
+
},
|
|
145
|
+
files: [
|
|
146
|
+
'dist',
|
|
147
|
+
'docs',
|
|
148
|
+
'component-variables.manifest.json',
|
|
149
|
+
'custom-elements.json',
|
|
150
|
+
'web-types.json',
|
|
151
|
+
'vscode.html-custom-data.json',
|
|
152
|
+
'vscode.css-custom-data.json',
|
|
153
|
+
// Never ship AI-assistant guidance, even if a directory above gets added.
|
|
154
|
+
...PACKAGE_FILES_EXCLUSIONS,
|
|
155
|
+
],
|
|
156
|
+
sideEffects: ['./dist/*.js', './dist/*.css', './src/index.ts', './src/web-component.ts', './src/css/*.css'],
|
|
157
|
+
kmwc: {
|
|
158
|
+
title,
|
|
159
|
+
tagline,
|
|
160
|
+
tag,
|
|
161
|
+
prefix,
|
|
162
|
+
library: { globalName: logicClass, fileName: componentId },
|
|
163
|
+
ports: { dev: devPort, image: getPortsForBlock(devPort).image },
|
|
164
|
+
},
|
|
165
|
+
scripts: {
|
|
166
|
+
dev: 'vite',
|
|
167
|
+
analyze: 'cem analyze',
|
|
168
|
+
build: 'npm run check:vars && npm run analyze && tsc --noEmit && vite build',
|
|
169
|
+
'build:examples': 'vite build --mode examples',
|
|
170
|
+
'preview:examples': 'vite preview --mode examples',
|
|
171
|
+
typecheck: 'tsc --noEmit',
|
|
172
|
+
'check:vars': 'kmwc check-vars --strict',
|
|
173
|
+
doctor: 'kmwc doctor',
|
|
174
|
+
prepublishOnly: 'npm run clean:dist && npm run build',
|
|
175
|
+
},
|
|
176
|
+
dependencies: {
|
|
177
|
+
'@keenmate/web-components-core': `^${coreVersion}`,
|
|
178
|
+
},
|
|
179
|
+
devDependencies: {
|
|
180
|
+
'@custom-elements-manifest/analyzer': '^0.11.0',
|
|
181
|
+
'@keenmate/web-components-devkit': `^${devkitVersion}`,
|
|
182
|
+
'@playwright/test': '^1.60.0',
|
|
183
|
+
'custom-element-jet-brains-integration': '^1.7.0',
|
|
184
|
+
'custom-element-vs-code-integration': '^1.5.0',
|
|
185
|
+
typescript: '^5.9.3',
|
|
186
|
+
vite: '^7.3.7',
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function run(args) {
|
|
192
|
+
const requestedName = args._[0];
|
|
193
|
+
if (!requestedName) {
|
|
194
|
+
throw new Error('usage: kmwc new <name> --prefix <2-4 letters> [--title "…"] [--tag …] [--dir …] [--port …] [--register] [--offline] [--adopt]');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const unscopedName = getUnscopedName(requestedName);
|
|
198
|
+
const componentId = unscopedName.startsWith('web-') ? unscopedName : `web-${unscopedName}`;
|
|
199
|
+
const packageName = `@keenmate/${componentId}`;
|
|
200
|
+
const tag = args.tag || componentId;
|
|
201
|
+
if (!/^[a-z][a-z0-9]*-[a-z0-9-]+$/.test(tag)) throw new Error(`"${tag}" is not a valid custom-element tag (needs a dash)`);
|
|
202
|
+
const featureName = tag.replace(/^web-/, '');
|
|
203
|
+
const elementClass = `${mapToPascalCase(featureName)}Element`;
|
|
204
|
+
const logicClass = mapToPascalCase(componentId);
|
|
205
|
+
const title = args.title || mapToPascalCase(componentId).replace(/^Web/, 'Web ');
|
|
206
|
+
const tagline = args.tagline || `${title} — a framework-agnostic web component.`;
|
|
207
|
+
const targetPath = resolve(args.dir || join(DEVKIT_ROOT, '..', componentId));
|
|
208
|
+
|
|
209
|
+
// ── intake ───────────────────────────────────────────────────────────────
|
|
210
|
+
const registry = getRegistry();
|
|
211
|
+
const isAdopting = !!args.adopt;
|
|
212
|
+
const adoptedEntry = registry.components.find((entry) => entry.id === componentId);
|
|
213
|
+
if (isAdopting && !adoptedEntry) throw new Error(`--adopt: "${componentId}" is not in components.json — register it, or drop --adopt for a new component`);
|
|
214
|
+
if (!isAdopting && adoptedEntry) throw new Error(`"${componentId}" is already in components.json (migrating it? add --adopt)`);
|
|
215
|
+
if (isAdopting && args.register) throw new Error('--adopt never edits the registry — drop --register');
|
|
216
|
+
if (existsSync(targetPath) && readdirSync(targetPath).length) throw new Error(`${targetPath} exists and is not empty`);
|
|
217
|
+
|
|
218
|
+
if (isAdopting && adoptedEntry.prefix && args.prefix && args.prefix !== adoptedEntry.prefix) {
|
|
219
|
+
throw new Error(`--prefix ${args.prefix} ≠ the registered prefix "${adoptedEntry.prefix}"`);
|
|
220
|
+
}
|
|
221
|
+
const prefix = (isAdopting && adoptedEntry.prefix) || args.prefix || generatePrefix(componentId);
|
|
222
|
+
if (!prefix) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`cannot derive a CSS prefix for "${componentId}" — is "${featureName}" one English word or several? ` +
|
|
225
|
+
`Pass --prefix (initials of each word, e.g. daterangepicker → drp; component-intake.md "C — CSS prefix derivation").`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
// An adopted component's own registered prefix isn't a clash with itself.
|
|
229
|
+
const reservedPrefixes = getReservedPrefixes(registry);
|
|
230
|
+
if (isAdopting && adoptedEntry.prefix) reservedPrefixes.delete(adoptedEntry.prefix);
|
|
231
|
+
const prefixError = checkPrefix(prefix, reservedPrefixes);
|
|
232
|
+
if (prefixError) throw new Error(prefixError);
|
|
233
|
+
|
|
234
|
+
if (!isAdopting && !args.offline && checkIsPackageNameTaken(packageName)) throw new Error(`${packageName} already exists on npm (component-intake.md "B")`);
|
|
235
|
+
|
|
236
|
+
if (isAdopting && args.port && Number(args.port) !== adoptedEntry.port) throw new Error(`--port ${args.port} ≠ the registered port block ${adoptedEntry.port}`);
|
|
237
|
+
const devPort = isAdopting ? adoptedEntry.port : Number(args.port) || getNextPortBlock(registry);
|
|
238
|
+
if (devPort % PORT_BLOCK_SIZE !== 0) throw new Error(`--port ${devPort} is not a port block (a multiple of ${PORT_BLOCK_SIZE})`);
|
|
239
|
+
const portOwner = registry.components.find((entry) => entry.port === devPort && entry.id !== componentId);
|
|
240
|
+
if (portOwner) throw new Error(`port block ${devPort} belongs to ${portOwner.id} — omit --port to take the next free block`);
|
|
241
|
+
const corePackageJsonPath = join(DEVKIT_ROOT, '..', 'web-components-core', 'package.json');
|
|
242
|
+
const coreVersion = existsSync(corePackageJsonPath) ? getJsonFile(corePackageJsonPath).version : '1.0.0-rc12';
|
|
243
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
244
|
+
|
|
245
|
+
const variables = {
|
|
246
|
+
ID: componentId,
|
|
247
|
+
TAG: tag,
|
|
248
|
+
FEATURE: featureName,
|
|
249
|
+
ELEMENT_CLASS: elementClass,
|
|
250
|
+
LOGIC_CLASS: logicClass,
|
|
251
|
+
CONFIG_TYPE: `${mapToPascalCase(featureName)}Config`,
|
|
252
|
+
EVENT_DETAIL_TYPE: `${mapToPascalCase(featureName)}EventDetail`,
|
|
253
|
+
EVENTS_TYPE: `${mapToPascalCase(featureName)}Events`,
|
|
254
|
+
PREFIX: prefix,
|
|
255
|
+
LOG_NAME: featureName.replace(/-/g, '_').toUpperCase(),
|
|
256
|
+
TITLE: title,
|
|
257
|
+
TITLE_UPPER: title.toUpperCase(),
|
|
258
|
+
TAGLINE: tagline,
|
|
259
|
+
PACKAGE_NAME: packageName,
|
|
260
|
+
FILE_NAME: componentId,
|
|
261
|
+
VERSION: '0.1.0',
|
|
262
|
+
TODAY: today,
|
|
263
|
+
YEAR: today.slice(0, 4),
|
|
264
|
+
DEV_PORT: devPort,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// ── scaffold files ───────────────────────────────────────────────────────
|
|
268
|
+
const isMinimal = !!args.minimal;
|
|
269
|
+
writeScaffoldFiles(SCAFFOLD_ROOT, targetPath, variables, featureName);
|
|
270
|
+
if (isMinimal) writeScaffoldFiles(MINIMAL_OVERLAY_ROOT, targetPath, variables, featureName);
|
|
271
|
+
|
|
272
|
+
const packageJson = generatePackageJson({
|
|
273
|
+
componentId,
|
|
274
|
+
packageName,
|
|
275
|
+
tagline,
|
|
276
|
+
tag,
|
|
277
|
+
prefix,
|
|
278
|
+
title,
|
|
279
|
+
devPort,
|
|
280
|
+
coreVersion,
|
|
281
|
+
devkitVersion: getDevkitPackageJson().version,
|
|
282
|
+
logicClass,
|
|
283
|
+
});
|
|
284
|
+
writeFileSync(join(targetPath, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n');
|
|
285
|
+
console.log(`✓ scaffolded ${componentId}${isMinimal ? ' (minimal)' : ''} in ${targetPath} (prefix ${prefix}, dev port ${devPort})`);
|
|
286
|
+
|
|
287
|
+
// ── generated files (Dockerfile, Makefile, CLAUDE.md block, scripts) ─────
|
|
288
|
+
await runSync({ root: targetPath });
|
|
289
|
+
|
|
290
|
+
// ── registry ─────────────────────────────────────────────────────────────
|
|
291
|
+
const registryEntry = {
|
|
292
|
+
id: componentId,
|
|
293
|
+
kind: 'component',
|
|
294
|
+
prefix,
|
|
295
|
+
path: relative(DEVKIT_ROOT, targetPath).replace(/\\/g, '/'),
|
|
296
|
+
npm: packageName,
|
|
297
|
+
repo: `https://github.com/keenmate/${componentId}`,
|
|
298
|
+
port: devPort,
|
|
299
|
+
};
|
|
300
|
+
if (isAdopting) {
|
|
301
|
+
if (!adoptedEntry.prefix) console.log(`
|
|
302
|
+
The registry entry has no prefix — add "prefix": "${prefix}" to its components.json entry.`);
|
|
303
|
+
} else if (args.register) {
|
|
304
|
+
const registryPath = join(DEVKIT_ROOT, 'components.json');
|
|
305
|
+
const registryJson = getJsonFile(registryPath);
|
|
306
|
+
const lastComponentIndex = registryJson.components.map((entry) => entry.kind).lastIndexOf('component');
|
|
307
|
+
registryJson.components.splice(lastComponentIndex + 1, 0, registryEntry);
|
|
308
|
+
writeFileSync(registryPath, JSON.stringify(registryJson, null, 2) + '\n');
|
|
309
|
+
console.log('✓ registered in components.json — run `npm run readme` in the devkit to refresh the catalogue');
|
|
310
|
+
} else {
|
|
311
|
+
console.log(`\nAdd it to the devkit registry (components.json), or re-run with --register:\n${JSON.stringify(registryEntry, null, 2)}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log(
|
|
315
|
+
`\nNext:\n` +
|
|
316
|
+
` 1. Reserve the prefix "${prefix}" in BlissFramework guidelines/web-components/base-variables.md (and component-intake.md).\n` +
|
|
317
|
+
` 2. cd ${relative(process.cwd(), targetPath) || '.'} && npm install\n` +
|
|
318
|
+
` 3. npm run build # check:vars + CEM analyze + typecheck + vite — commit the generated custom-elements.json & co.\n` +
|
|
319
|
+
` 4. npm run dev # http://localhost:${devPort}\n` +
|
|
320
|
+
` 5. npx kmwc doctor`
|
|
321
|
+
);
|
|
322
|
+
return 0;
|
|
323
|
+
}
|