@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.
Files changed (109) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/LICENSE +21 -0
  3. package/README.md +434 -2
  4. package/components.json +206 -0
  5. package/package.json +77 -4
  6. package/src/cli/args.js +16 -0
  7. package/src/cli/commands/check-release.js +66 -0
  8. package/src/cli/commands/check-vars.js +116 -0
  9. package/src/cli/commands/doctor.js +289 -0
  10. package/src/cli/commands/new.js +323 -0
  11. package/src/cli/commands/readme.js +111 -0
  12. package/src/cli/commands/sync.js +48 -0
  13. package/src/cli/kmwc.js +73 -0
  14. package/src/cli/sync-plan.js +163 -0
  15. package/src/config.d.ts +149 -0
  16. package/src/config.js +279 -0
  17. package/src/examples/auto.js +20 -0
  18. package/src/examples/chapter-nav.js +246 -0
  19. package/src/examples/constants.js +80 -0
  20. package/src/examples/controls-persistence.js +97 -0
  21. package/src/examples/copy-link.js +89 -0
  22. package/src/examples/css/base.css +215 -0
  23. package/src/examples/css/chapter-nav.css +179 -0
  24. package/src/examples/css/content.css +251 -0
  25. package/src/examples/css/controls.css +205 -0
  26. package/src/examples/css/copy-link.css +82 -0
  27. package/src/examples/css/demo.css +50 -0
  28. package/src/examples/css/index-page.css +118 -0
  29. package/src/examples/css/style.css +30 -0
  30. package/src/examples/css/theme-switch.css +38 -0
  31. package/src/examples/css/tokens.css +124 -0
  32. package/src/examples/css/widgets.css +133 -0
  33. package/src/examples/demo-code.js +30 -0
  34. package/src/examples/index-page.js +78 -0
  35. package/src/examples/index.d.ts +107 -0
  36. package/src/examples/index.js +18 -0
  37. package/src/examples/kit.js +22 -0
  38. package/src/examples/page-config.js +30 -0
  39. package/src/examples/sections.js +53 -0
  40. package/src/examples/theme-switch.js +72 -0
  41. package/src/examples/version-badge.js +23 -0
  42. package/src/playwright/index.d.ts +15 -0
  43. package/src/playwright/index.js +66 -0
  44. package/src/template.js +11 -0
  45. package/src/vite/index.d.ts +28 -0
  46. package/src/vite/index.js +243 -0
  47. package/src/vite/page-composer.js +216 -0
  48. package/templates/Dockerfile.tmpl +39 -0
  49. package/templates/Makefile.tmpl +136 -0
  50. package/templates/claude-block.md +40 -0
  51. package/templates/dockerignore.tmpl +10 -0
  52. package/templates/examples-layout.html +13 -0
  53. package/templates/new/.claude/commands/publish.md +226 -0
  54. package/templates/new/CHANGELOG.md +13 -0
  55. package/templates/new/LICENSE +21 -0
  56. package/templates/new/README.md +71 -0
  57. package/templates/new/_gitignore +7 -0
  58. package/templates/new/component-variables.manifest.json +32 -0
  59. package/templates/new/custom-elements-manifest.config.mjs +23 -0
  60. package/templates/new/docs/accessibility.md +24 -0
  61. package/templates/new/docs/examples.md +33 -0
  62. package/templates/new/docs/theming.md +54 -0
  63. package/templates/new/docs/usage.md +51 -0
  64. package/templates/new/e2e/dark-mode.spec.ts +30 -0
  65. package/templates/new/e2e/smoke.spec.ts +26 -0
  66. package/templates/new/examples-basic.html +57 -0
  67. package/templates/new/examples-theming.html +28 -0
  68. package/templates/new/index.html +16 -0
  69. package/templates/new/partials/note.html +5 -0
  70. package/templates/new/playwright.config.ts +4 -0
  71. package/templates/new/src/__FEATURE__.ts +55 -0
  72. package/templates/new/src/css/animations.css +1 -0
  73. package/templates/new/src/css/base.css +43 -0
  74. package/templates/new/src/css/controls.css +20 -0
  75. package/templates/new/src/css/dark-mode.css +36 -0
  76. package/templates/new/src/css/floating.css +1 -0
  77. package/templates/new/src/css/main.css +27 -0
  78. package/templates/new/src/css/states.css +22 -0
  79. package/templates/new/src/css/variables.css +42 -0
  80. package/templates/new/src/env.d.ts +7 -0
  81. package/templates/new/src/index.ts +17 -0
  82. package/templates/new/src/logger.ts +11 -0
  83. package/templates/new/src/types.ts +25 -0
  84. package/templates/new/src/web-component.ts +73 -0
  85. package/templates/new/test/dark-mode.html +18 -0
  86. package/templates/new/test/fixture.html +11 -0
  87. package/templates/new/tsconfig.json +15 -0
  88. package/templates/new/vite.config.ts +5 -0
  89. package/templates/new-minimal/CHANGELOG.md +13 -0
  90. package/templates/new-minimal/README.md +71 -0
  91. package/templates/new-minimal/component-variables.manifest.json +24 -0
  92. package/templates/new-minimal/docs/accessibility.md +20 -0
  93. package/templates/new-minimal/docs/examples.md +17 -0
  94. package/templates/new-minimal/docs/theming.md +53 -0
  95. package/templates/new-minimal/docs/usage.md +43 -0
  96. package/templates/new-minimal/e2e/smoke.spec.ts +10 -0
  97. package/templates/new-minimal/examples-basic.html +23 -0
  98. package/templates/new-minimal/examples-theming.html +28 -0
  99. package/templates/new-minimal/src/__FEATURE__.ts +37 -0
  100. package/templates/new-minimal/src/css/base.css +48 -0
  101. package/templates/new-minimal/src/css/controls.css +1 -0
  102. package/templates/new-minimal/src/css/states.css +1 -0
  103. package/templates/new-minimal/src/css/variables.css +28 -0
  104. package/templates/new-minimal/src/index.ts +17 -0
  105. package/templates/new-minimal/src/types.ts +10 -0
  106. package/templates/new-minimal/src/web-component.ts +48 -0
  107. package/templates/new-minimal/test/dark-mode.html +18 -0
  108. package/templates/new-minimal/test/fixture.html +11 -0
  109. package/templates/nginx.conf.tmpl +43 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * kmwc readme — regenerate the README catalogue from components.json.
3
+ *
4
+ * Descriptions and devkit adoption are read from the sibling
5
+ * checkouts (offline — no npm calls), so the table reflects the working copies
6
+ * next to this repo. Rewrites only the region between the catalogue markers.
7
+ */
8
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { join, resolve } from 'node:path';
10
+ import { DEVKIT_ROOT, getRegistry, getJsonFile, PORT_RANGES } from '../../config.js';
11
+
12
+ export const CATALOGUE_START = '<!-- kmwc:catalogue:start — generated by `npm run readme`, do not edit inside -->';
13
+ export const CATALOGUE_END = '<!-- kmwc:catalogue:end -->';
14
+
15
+ const DEVKIT_PACKAGE_NAME = '@keenmate/web-components-devkit';
16
+ const SHORT_TEXT_LENGTH = 110;
17
+
18
+ /** Read the package.json that represents a registry entry (monorepo-aware). */
19
+ function getSiblingPackageInfo(entry) {
20
+ const repoPath = resolve(DEVKIT_ROOT, entry.path || `../${entry.id}`);
21
+ if (!existsSync(repoPath)) return { isPresent: false };
22
+ const rootPackageJsonPath = join(repoPath, 'package.json');
23
+ const mainPackageJsonPath = [join(repoPath, 'packages', entry.id, 'package.json'), rootPackageJsonPath].find((path) => existsSync(path));
24
+ const mainPackageJson = mainPackageJsonPath ? getJsonFile(mainPackageJsonPath) : null;
25
+ const rootPackageJson = existsSync(rootPackageJsonPath) ? getJsonFile(rootPackageJsonPath) : null;
26
+ const dependencies = { ...rootPackageJson?.dependencies, ...rootPackageJson?.devDependencies };
27
+ return {
28
+ isPresent: true,
29
+ description: mainPackageJson?.description || rootPackageJson?.description || '',
30
+ isDevkitAdopted: !!(rootPackageJson?.kmwc && dependencies[DEVKIT_PACKAGE_NAME]),
31
+ };
32
+ }
33
+
34
+ const mapToTableCell = (text) => String(text ?? '').replace(/\|/g, '\\|').replace(/\s+/g, ' ').trim();
35
+ const renderLink = (label, href) => (href ? `[${label}](${href})` : '');
36
+ const mapToShortText = (text, maxLength = SHORT_TEXT_LENGTH) =>
37
+ text.length > maxLength ? text.slice(0, maxLength - 1).replace(/\s+\S*$/, '') + '…' : text;
38
+ const renderPackageName = (entry) => (entry.npm ? `\`${entry.npm}\`` : '—');
39
+ const renderPort = (entry) => (entry.port ? String(entry.port) : '—');
40
+
41
+ export function renderCatalogue(registry = getRegistry()) {
42
+ const entries = registry.components;
43
+ const getEntriesOfKind = (kind) => entries.filter((entry) => entry.kind === kind);
44
+ const lines = [];
45
+
46
+ lines.push('### Components', '');
47
+ lines.push('| Component | Package | Prefix | Dev port | Links | Devkit | About |');
48
+ lines.push('|---|---|---|---:|---|:---:|---|');
49
+ for (const entry of getEntriesOfKind('component')) {
50
+ const siblingInfo = getSiblingPackageInfo(entry);
51
+ const companions = entries.filter((companion) => ['showcase', 'docs'].includes(companion.kind) && companion.for === entry.id);
52
+ const links = [
53
+ renderLink('repo', entry.repo),
54
+ renderLink('examples', entry.examples),
55
+ renderLink('docs', entry.docs),
56
+ ...companions.map((companion) => (companion.url ? renderLink(companion.kind, companion.url) : `\`${companion.id}\``)),
57
+ ].filter(Boolean);
58
+ lines.push(
59
+ `| **${mapToTableCell(entry.id)}** | ${renderPackageName(entry)} | ${entry.prefix ? `\`${entry.prefix}\`` : '—'} | ${renderPort(entry)} | ${links.join(' · ') || '—'} | ${siblingInfo.isDevkitAdopted ? '✅' : '—'} | ${mapToTableCell(mapToShortText(siblingInfo.description || entry.note || ''))} |`
60
+ );
61
+ }
62
+
63
+ lines.push('', '### Shared libraries', '');
64
+ lines.push('| Library | Package | Dev port | About |');
65
+ lines.push('|---|---|---:|---|');
66
+ for (const entry of getEntriesOfKind('library')) {
67
+ const siblingInfo = getSiblingPackageInfo(entry);
68
+ lines.push(`| **${mapToTableCell(entry.id)}** | ${renderPackageName(entry)} | ${renderPort(entry)} | ${mapToTableCell(entry.note || siblingInfo.description)} |`);
69
+ }
70
+
71
+ lines.push('', '### Showcases, docs & test harnesses', '');
72
+ lines.push('| Repo | Kind | For | Dev port | About |');
73
+ lines.push('|---|---|---|---:|---|');
74
+ for (const entry of entries.filter((candidate) => ['showcase', 'docs', 'test'].includes(candidate.kind))) {
75
+ const about = entry.url ? `${renderLink(entry.url, entry.url)} ${entry.note || ''}` : entry.note || '';
76
+ lines.push(`| ${mapToTableCell(entry.id)} | ${entry.kind} | ${entry.for || '—'} | ${renderPort(entry)} | ${mapToTableCell(about)} |`);
77
+ }
78
+
79
+ const rangesText = PORT_RANGES.map((range) => `${range.kinds.join(' & ')} ${range.firstBlock}–${range.lastBlock}`).join(' · ');
80
+ lines.push(
81
+ '',
82
+ `**Ports** — a block of 100 per repo: dev server = **Dev port**, preview = +1, examples image = +10. Each kind numbers its blocks by 100 inside its own range (${rangesText}), ` +
83
+ 'so any number of repos run side by side without collisions and a new component lands next to the others. ' +
84
+ '`kmwc doctor` checks a repo against its block; `kmwc new` takes the next free one.'
85
+ );
86
+
87
+ return lines.join('\n');
88
+ }
89
+
90
+ export async function run(args) {
91
+ const readmePath = join(DEVKIT_ROOT, 'README.md');
92
+ const currentText = readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
93
+ const startIndex = currentText.indexOf(CATALOGUE_START);
94
+ const endIndex = currentText.indexOf(CATALOGUE_END);
95
+ if (startIndex === -1 || endIndex < startIndex) {
96
+ throw new Error(`README.md is missing the catalogue markers:\n${CATALOGUE_START}\n${CATALOGUE_END}`);
97
+ }
98
+
99
+ const nextText = currentText.slice(0, startIndex + CATALOGUE_START.length) + '\n' + renderCatalogue() + '\n' + currentText.slice(endIndex);
100
+ if (nextText === currentText) {
101
+ console.log('✓ README catalogue up to date');
102
+ return 0;
103
+ }
104
+ if (args.check) {
105
+ console.log('✗ README catalogue is stale — run `npm run readme`');
106
+ return 1;
107
+ }
108
+ writeFileSync(readmePath, nextText);
109
+ console.log('✓ README catalogue regenerated');
110
+ return 0;
111
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * kmwc sync — bring devkit-owned files in line with package.json#kmwc.
3
+ * Generated files are overwritten wholesale; CLAUDE.md only inside its managed
4
+ * block; .gitignore and package.json scripts only gain what's missing (an
5
+ * existing script is never rewritten — doctor reports the differences).
6
+ */
7
+ import { writeFileSync, readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { getKmwcConfig } from '../../config.js';
10
+ import { getSyncPlan, getScriptsPlan } from '../sync-plan.js';
11
+
12
+ export async function run(args) {
13
+ const config = getKmwcConfig(args.root);
14
+ const isCheckOnly = !!args.check;
15
+ if (!config.hasPackageConfig) {
16
+ console.error('kmwc sync: package.json has no "kmwc" section — add one first (see the devkit README).');
17
+ return 1;
18
+ }
19
+
20
+ const changedItems = getSyncPlan(config).filter((planItem) => planItem.isChanged);
21
+ const { missingScripts, differingScripts } = getScriptsPlan(config);
22
+ const missingScriptNames = Object.keys(missingScripts);
23
+
24
+ for (const planItem of changedItems) {
25
+ const action = planItem.currentText == null ? 'create' : 'update';
26
+ console.log(`${isCheckOnly ? '✗ would' : '✓'} ${action} ${planItem.path}`);
27
+ if (!isCheckOnly) writeFileSync(join(config.root, planItem.path), planItem.expectedText);
28
+ }
29
+
30
+ if (missingScriptNames.length) {
31
+ console.log(`${isCheckOnly ? '✗ would add' : '✓ added'} scripts: ${missingScriptNames.join(', ')}`);
32
+ if (!isCheckOnly) {
33
+ const packageJsonPath = join(config.root, 'package.json');
34
+ const packageJsonText = readFileSync(packageJsonPath, 'utf8');
35
+ const indentation = packageJsonText.match(/^[ \t]+(?=")/m)?.[0] || ' ';
36
+ const packageJson = JSON.parse(packageJsonText);
37
+ packageJson.scripts = { ...(packageJson.scripts || {}), ...missingScripts };
38
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, indentation) + '\n');
39
+ }
40
+ }
41
+
42
+ for (const [scriptName, difference] of Object.entries(differingScripts)) {
43
+ console.log(`⚠ script "${scriptName}" is "${difference.currentCommand}" — standard is "${difference.standardCommand}" (left as is)`);
44
+ }
45
+
46
+ if (!changedItems.length && !missingScriptNames.length) console.log('✓ everything in sync');
47
+ return isCheckOnly && (changedItems.length || missingScriptNames.length) ? 1 : 0;
48
+ }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * kmwc — the Keenmate web-components devkit CLI.
4
+ *
5
+ * kmwc sync [--check] regenerate devkit-owned files from package.json#kmwc
6
+ * kmwc doctor [--json] check this repo against the devkit + rulebook standard
7
+ * kmwc check-vars [--strict] validate component-variables.manifest.json against the CSS
8
+ * kmwc check-release --tag <tag> gate a publish (version ⇔ dist-tag, not yet on npm)
9
+ * kmwc new <name> [options] scaffold a new component repo next to the devkit
10
+ * kmwc readme [--check] regenerate the devkit README catalogue (devkit repo only)
11
+ *
12
+ * All commands act on the current directory (override with --root <dir>).
13
+ * Each command module exports `run(args)` returning the process exit code.
14
+ */
15
+ import { argv, exit } from 'node:process';
16
+ import { parseArgs } from './args.js';
17
+
18
+ const COMMAND_LOADERS = {
19
+ sync: () => import('./commands/sync.js'),
20
+ doctor: () => import('./commands/doctor.js'),
21
+ 'check-vars': () => import('./commands/check-vars.js'),
22
+ 'check-release': () => import('./commands/check-release.js'),
23
+ new: () => import('./commands/new.js'),
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,
33
+ the CLAUDE.md devkit block, .gitignore entries and missing
34
+ standard scripts. --check: report drift, write nothing.
35
+ doctor [--json] Check this repo against the devkit standard and the
36
+ BlissFramework rulebook (*.checks.sh), and that npm pack ships no
37
+ dev-only files. --verbose, --no-rulebook, --no-pack.
38
+ check-vars [--strict] Validate component-variables.manifest.json against the CSS.
39
+ check-release --tag <t> Gate a publish: latest ⇔ release version, rc ⇔ -rcN,
40
+ version not on npm yet. --offline skips the npm lookup.
41
+ new <name> Scaffold a new component (e.g. \`kmwc new web-colorpicker --prefix cpk\`).
42
+ --title "…" --tag … --prefix … --dir … --port … --register
43
+ --minimal: empty skeleton (no inputs/events) instead of the sample
44
+ --adopt: an already registered component (migration scratch scaffold)
45
+ readme [--check] Regenerate the README catalogue from components.json (devkit repo).
46
+
47
+ Global: --root <dir> Act on <dir> instead of the current directory.`);
48
+ }
49
+
50
+ async function main() {
51
+ const [commandName, ...commandArgs] = argv.slice(2);
52
+ if (!commandName || ['help', '--help', '-h'].includes(commandName)) {
53
+ renderUsage();
54
+ return 0;
55
+ }
56
+ const loadCommand = COMMAND_LOADERS[commandName];
57
+ if (!loadCommand) {
58
+ console.error(`kmwc: unknown command "${commandName}"\n`);
59
+ renderUsage();
60
+ return 2;
61
+ }
62
+ const command = await loadCommand();
63
+ return (await command.run(parseArgs(commandArgs))) ?? 0;
64
+ }
65
+
66
+ main().then(
67
+ (exitCode) => exit(exitCode),
68
+ (error) => {
69
+ console.error(`kmwc: ${error?.message || error}`);
70
+ if (process.env.KMWC_DEBUG) console.error(error);
71
+ exit(1);
72
+ }
73
+ );
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The sync plan: what every devkit-owned file in a component SHOULD contain.
3
+ * `kmwc sync` writes it, `kmwc sync --check` and `kmwc doctor` diff against it
4
+ * — one computation, so "what sync would do" and "what doctor complains about"
5
+ * can never disagree.
6
+ */
7
+ import { readFileSync, existsSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { DEVKIT_ROOT } from '../config.js';
10
+ import { renderTemplate } from '../template.js';
11
+
12
+ export { renderTemplate };
13
+
14
+ export const GENERATED_MARK = 'GENERATED by `kmwc sync`';
15
+
16
+ const GENERATED_HEADER =
17
+ `# ${GENERATED_MARK} from @keenmate/web-components-devkit — do not edit.\n` +
18
+ `# Change package.json#kmwc (or the devkit template) and re-run \`npx kmwc sync\`.`;
19
+
20
+ export const CLAUDE_BLOCK_START = '<!-- kmwc:devkit:start — managed by `kmwc sync`, do not edit inside -->';
21
+ export const CLAUDE_BLOCK_END = '<!-- kmwc:devkit:end -->';
22
+
23
+ /** Files rendered wholesale from templates. */
24
+ export const GENERATED_FILES = [
25
+ { path: 'Dockerfile', template: 'Dockerfile.tmpl' },
26
+ { path: 'nginx.conf', template: 'nginx.conf.tmpl' },
27
+ { path: 'Makefile', template: 'Makefile.tmpl' },
28
+ { path: '.dockerignore', template: 'dockerignore.tmpl' },
29
+ ];
30
+
31
+ const PLAYWRIGHT_CONFIG_FILES = ['playwright.config.ts', 'playwright.config.js', 'playwright.config.mjs'];
32
+ const CEM_CONFIG_FILE = 'custom-elements-manifest.config.mjs';
33
+
34
+ export function getTemplate(templateName) {
35
+ return readFileSync(join(DEVKIT_ROOT, 'templates', templateName), 'utf8');
36
+ }
37
+
38
+ export function getTemplateVariables(config) {
39
+ return {
40
+ HEADER: GENERATED_HEADER,
41
+ ID: config.id,
42
+ PACKAGE_NAME: config.packageName,
43
+ NODE_IMAGE: config.image.nodeImage,
44
+ INSTALL_COMMAND: config.image.installCommand,
45
+ COPY_PACKAGE_JSONS: config.image.packageJsonPaths
46
+ .map((packageJsonPath) => {
47
+ const targetDirectory = packageJsonPath.includes('/') ? packageJsonPath.slice(0, packageJsonPath.lastIndexOf('/') + 1) : './';
48
+ return `COPY ${packageJsonPath} ${targetDirectory}`;
49
+ })
50
+ .join('\n'),
51
+ OUT_DIR: config.examples.outDir,
52
+ RUNNER: config.image.runner,
53
+ IMAGE_NAME: config.image.name,
54
+ CONTAINER_NAME: config.image.container,
55
+ IMAGE_PORT: config.ports.image,
56
+ DEV_PORT: config.ports.dev,
57
+ PREVIEW_PORT: config.ports.preview,
58
+ };
59
+ }
60
+
61
+ /** A `node -e` one-liner that removes the given paths (works in npm scripts on every OS). */
62
+ function generateRemoveCommand(paths) {
63
+ return `node -e "for (const p of ${JSON.stringify(paths).replace(/"/g, "'")}) require('fs').rmSync(p,{recursive:true,force:true})"`;
64
+ }
65
+
66
+ const hasAnyFile = (root, fileNames) => fileNames.some((fileName) => existsSync(join(root, fileName)));
67
+
68
+ /** Scripts every component should expose (only added when missing). */
69
+ export function getStandardScripts(config) {
70
+ const scripts = {
71
+ dev: 'vite',
72
+ build: 'vite build',
73
+ 'build:examples': 'vite build --mode examples',
74
+ 'preview:examples': 'vite preview --mode examples',
75
+ 'clean:dist': generateRemoveCommand(['dist']),
76
+ clean: generateRemoveCommand(['dist', config.examples.outDir]),
77
+ doctor: 'kmwc doctor',
78
+ };
79
+ if (existsSync(join(config.root, config.cssVariables.manifest))) scripts['check:vars'] = 'kmwc check-vars';
80
+ if (existsSync(join(config.root, CEM_CONFIG_FILE))) scripts.analyze = 'cem analyze';
81
+ if (hasAnyFile(config.root, PLAYWRIGHT_CONFIG_FILES)) {
82
+ Object.assign(scripts, {
83
+ 'test:e2e': 'playwright test',
84
+ 'test:e2e:ui': 'playwright test --ui',
85
+ 'test:e2e:headed': 'playwright test --headed',
86
+ 'test:e2e:install': 'playwright install chromium',
87
+ });
88
+ }
89
+ return scripts;
90
+ }
91
+
92
+ /** Insert or replace the managed block in a CLAUDE.md body (creates the file body when absent). */
93
+ export function updateClaudeBlock(claudeMdText) {
94
+ const block = `${CLAUDE_BLOCK_START}\n${getTemplate('claude-block.md').trim()}\n${CLAUDE_BLOCK_END}`;
95
+ if (claudeMdText == null) return `# CLAUDE.md\n\n${block}\n`;
96
+ const startIndex = claudeMdText.indexOf(CLAUDE_BLOCK_START);
97
+ const endIndex = claudeMdText.indexOf(CLAUDE_BLOCK_END);
98
+ if (startIndex !== -1 && endIndex > startIndex) {
99
+ return claudeMdText.slice(0, startIndex) + block + claudeMdText.slice(endIndex + CLAUDE_BLOCK_END.length);
100
+ }
101
+ // No block yet: place it right after the first H1 (or at the top).
102
+ const firstHeading = claudeMdText.match(/^# .*$/m);
103
+ if (!firstHeading) return `${block}\n\n${claudeMdText}`;
104
+ const insertIndex = firstHeading.index + firstHeading[0].length;
105
+ return `${claudeMdText.slice(0, insertIndex)}\n\n${block}\n${claudeMdText.slice(insertIndex)}`;
106
+ }
107
+
108
+ function getFileTextOrNull(filePath) {
109
+ return existsSync(filePath) ? readFileSync(filePath, 'utf8') : null;
110
+ }
111
+
112
+ /** Normalize line endings so a CRLF checkout doesn't read as drift. */
113
+ const normalizeLineEndings = (text) => (text == null ? text : text.replace(/\r\n/g, '\n'));
114
+
115
+ /**
116
+ * @returns {Array<{ path: string, kind: 'generated'|'claude'|'gitignore', expectedText: string, currentText: string|null, isChanged: boolean }>}
117
+ */
118
+ export function getSyncPlan(config) {
119
+ const variables = getTemplateVariables(config);
120
+ const skippedFiles = new Set(config.sync.skippedFiles);
121
+ const planItems = [];
122
+
123
+ for (const generatedFile of GENERATED_FILES) {
124
+ if (skippedFiles.has(generatedFile.path)) continue;
125
+ const expectedText = renderTemplate(getTemplate(generatedFile.template), variables);
126
+ const currentText = normalizeLineEndings(getFileTextOrNull(join(config.root, generatedFile.path)));
127
+ planItems.push({ path: generatedFile.path, kind: 'generated', expectedText, currentText, isChanged: currentText !== expectedText });
128
+ }
129
+
130
+ if (!skippedFiles.has('CLAUDE.md')) {
131
+ const currentText = normalizeLineEndings(getFileTextOrNull(join(config.root, 'CLAUDE.md')));
132
+ const expectedText = updateClaudeBlock(currentText);
133
+ planItems.push({ path: 'CLAUDE.md', kind: 'claude', expectedText, currentText, isChanged: currentText !== expectedText });
134
+ }
135
+
136
+ if (!skippedFiles.has('.gitignore')) {
137
+ const currentText = normalizeLineEndings(getFileTextOrNull(join(config.root, '.gitignore')));
138
+ const currentLines = (currentText || '').split('\n').map((line) => line.trim());
139
+ const wantedLines = [`${config.examples.outDir}/`, '.makefile.env'];
140
+ const missingLines = wantedLines.filter((line) => !currentLines.includes(line) && !currentLines.includes(line.replace(/\/$/, '')));
141
+ const expectedText = missingLines.length
142
+ ? (currentText ? currentText.replace(/\n?$/, '\n') : '') + `# kmwc\n${missingLines.join('\n')}\n`
143
+ : currentText;
144
+ planItems.push({ path: '.gitignore', kind: 'gitignore', expectedText, currentText, isChanged: missingLines.length > 0 });
145
+ }
146
+
147
+ return planItems;
148
+ }
149
+
150
+ /** Missing and differing standard scripts. */
151
+ export function getScriptsPlan(config) {
152
+ const currentScripts = config.packageJson.scripts || {};
153
+ const standardScripts = getStandardScripts(config);
154
+ const missingScripts = {};
155
+ const differingScripts = {};
156
+ for (const [scriptName, command] of Object.entries(standardScripts)) {
157
+ if (!(scriptName in currentScripts)) missingScripts[scriptName] = command;
158
+ else if (['dev', 'build:examples', 'preview:examples'].includes(scriptName) && currentScripts[scriptName] !== command) {
159
+ differingScripts[scriptName] = { currentCommand: currentScripts[scriptName], standardCommand: command };
160
+ }
161
+ }
162
+ return { missingScripts, differingScripts };
163
+ }
@@ -0,0 +1,149 @@
1
+ /** The `kmwc` section of a component's package.json — every field optional (defaults in `mapKmwcConfig`). */
2
+ export interface KmwcPackageConfig {
3
+ /** Registry id; defaults to the unscoped package name. */
4
+ id?: string;
5
+ /** Human title shown on the examples index header. */
6
+ title?: string;
7
+ /** One-line pitch under the title; defaults to package.json `description`. */
8
+ tagline?: string;
9
+ /** Custom-element tag the package registers. */
10
+ tag?: string;
11
+ /** Reserved CSS prefix (base-variables.md "Existing prefixes"), e.g. `ms`. */
12
+ prefix?: string;
13
+ library?: {
14
+ entry?: string;
15
+ /** UMD global name. */
16
+ globalName?: string;
17
+ /** Output basename → `dist/<fileName>.js` + `dist/<fileName>.umd.js`. */
18
+ fileName?: string;
19
+ formats?: Array<'es' | 'umd' | 'cjs' | 'iife'>;
20
+ /** Lib-mode CSS basename (default `style` → dist/style.css). */
21
+ cssFileName?: string;
22
+ /** Emit rolled-up `.d.ts` via vite-plugin-dts. */
23
+ shouldEmitTypes?: boolean;
24
+ /** Extra vite-plugin-dts config. */
25
+ typesPluginConfig?: Record<string, unknown>;
26
+ shouldEmitSourcemap?: boolean;
27
+ external?: string[];
28
+ };
29
+ ports?: { dev?: number; image?: number };
30
+ theme?: { accentColor?: string; accentEndColor?: string };
31
+ examples?: {
32
+ outDir?: string;
33
+ /** Production origin for copy-link deep links. */
34
+ canonicalOrigin?: string;
35
+ /** Extra dirs/files copied verbatim into the examples build (fetched at runtime). */
36
+ staticPaths?: string[];
37
+ /** Root HTML files NOT to build as example pages. */
38
+ excludedPages?: string[];
39
+ /** localStorage namespace for the controls persistence; defaults to `id`. */
40
+ storagePrefix?: string;
41
+ };
42
+ analytics?: { plausibleSiteId?: string; plausibleHostUrl?: string };
43
+ image?: {
44
+ name?: string;
45
+ container?: string;
46
+ runner?: 'podman' | 'docker';
47
+ nodeImage?: string;
48
+ /** package.json files copied before install (monorepos list workspace manifests). */
49
+ packageJsonPaths?: string[];
50
+ installCommand?: string;
51
+ };
52
+ cssVariables?: {
53
+ manifest?: string;
54
+ /** Directories (stylesheets scanned) or explicit files (any type, e.g. a built bundle). */
55
+ sources?: string[];
56
+ /** Prefixed names (without `--`) that are runtime internals, not theming knobs. */
57
+ internalVariables?: string[];
58
+ /** `declared`: manifest ⇔ `--x:` declarations; `consumed`: manifest ⇔ `var(--x)` reads. */
59
+ checkMode?: 'declared' | 'consumed';
60
+ };
61
+ rulebook?: {
62
+ /** Path to BlissFramework `guidelines/web-components` (env KMWC_RULEBOOK_PATH wins). */
63
+ path?: string;
64
+ /** Rulebook check suites to skip, e.g. `["responsive-overlay"]`. */
65
+ skippedChecks?: string[];
66
+ };
67
+ sync?: { skippedFiles?: string[] };
68
+ }
69
+
70
+ export interface KmwcConfig {
71
+ root: string;
72
+ hasPackageConfig: boolean;
73
+ packageJson: Record<string, any>;
74
+ id: string;
75
+ packageName: string;
76
+ version: string;
77
+ title: string;
78
+ tagline: string;
79
+ tag: string;
80
+ prefix: string;
81
+ repository: string;
82
+ homepage: string;
83
+ author: string;
84
+ license: string;
85
+ library: {
86
+ entry: string;
87
+ globalName: string;
88
+ fileName: string;
89
+ formats: Array<'es' | 'umd' | 'cjs' | 'iife'>;
90
+ cssFileName: string;
91
+ shouldEmitTypes: boolean;
92
+ typesPluginConfig: Record<string, unknown>;
93
+ shouldEmitSourcemap: boolean;
94
+ external: string[];
95
+ };
96
+ ports: { dev: number; preview: number; image: number };
97
+ theme: { accentColor: string; accentEndColor: string };
98
+ examples: {
99
+ outDir: string;
100
+ canonicalOrigin: string;
101
+ staticPaths: string[];
102
+ excludedPages: string[];
103
+ storagePrefix: string;
104
+ };
105
+ analytics: { plausibleSiteId: string; plausibleHostUrl: string };
106
+ image: {
107
+ name: string;
108
+ container: string;
109
+ runner: string;
110
+ nodeImage: string;
111
+ packageJsonPaths: string[];
112
+ installCommand: string;
113
+ };
114
+ cssVariables: {
115
+ manifest: string;
116
+ sources: string[];
117
+ internalVariables: string[];
118
+ checkMode: 'declared' | 'consumed';
119
+ };
120
+ rulebook: { path: string; skippedChecks: string[] };
121
+ sync: { skippedFiles: string[] };
122
+ }
123
+
124
+ export const DEVKIT_ROOT: string;
125
+ export const DEFAULT_RULEBOOK_PATH: string;
126
+ /** The nearest `<ancestor>/BlissFramework/guidelines/web-components` above `startPath`, or null. */
127
+ export function getRulebookPath(startPath: string): string | null;
128
+ export const LEGACY_FILES: string[];
129
+ export const PACKAGE_LEAK_RULES: Array<{ pattern: RegExp; label: string }>;
130
+ export const PACKAGE_FILES_EXCLUSIONS: string[];
131
+ export const NON_PAGE_HTML_FILES: string[];
132
+ export const PORT_BLOCK_SIZE: number;
133
+ export const PORT_RANGES: Array<{ kinds: string[]; firstBlock: number; lastBlock: number }>;
134
+ export const PREVIEW_PORT_OFFSET: number;
135
+ export const IMAGE_PORT_OFFSET: number;
136
+ export function getPortsForBlock(portBlock: number): { dev: number; preview: number; image: number };
137
+ export function getPortRange(kind: string): { kinds: string[]; firstBlock: number; lastBlock: number };
138
+ export function getNextPortBlock(registry?: { components: Array<Record<string, any>> }, kind?: string): number;
139
+ export function getPeerModule(root: string, moduleId: string): any;
140
+ export function loadPeerModule(root: string, moduleId: string): Promise<any>;
141
+ export function getJsonFile(filePath: string): any;
142
+ export function getDevkitPackageJson(): Record<string, any>;
143
+ export function getRegistry(): { components: Array<Record<string, any>> };
144
+ export function getUnscopedName(packageName: string): string;
145
+ export function mapToTitleCase(id: string): string;
146
+ export function mapToPascalCase(id: string): string;
147
+ export function getKmwcConfig(root?: string): KmwcConfig;
148
+ export function mapKmwcConfig(packageJson: Record<string, any>, root?: string): KmwcConfig;
149
+ export function generateBuildDefines(config: KmwcConfig): Record<string, string>;