@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,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page composition for example pages — the "one copy of every repeated line"
|
|
3
|
+
* half of the examples kit. Runs inside the kmwc Vite plugin (dev and build),
|
|
4
|
+
* before the kit is injected:
|
|
5
|
+
*
|
|
6
|
+
* 1. <kmwc-include src="partials/x.html" any-param="…">inner</kmwc-include>
|
|
7
|
+
* → the partial, with {{anyParam}} / {{content}} substituted. Nested
|
|
8
|
+
* includes are fine; cycles are an error.
|
|
9
|
+
* 2. <kmwc-demo>…markup…</kmwc-demo>
|
|
10
|
+
* → the live markup AND a code block of that exact markup, so a snippet
|
|
11
|
+
* can never drift from its demo. <script data-kmwc-show> inside a demo is
|
|
12
|
+
* executed and shown too.
|
|
13
|
+
* 3. A page without <html> is a FRAGMENT: it gets the devkit layout (head,
|
|
14
|
+
* header with back link / title / subtitle, container, component entry
|
|
15
|
+
* script), driven by <meta name="kmwc:…"> tags:
|
|
16
|
+
* kmwc:title page title (h1 + <title>) — required
|
|
17
|
+
* kmwc:subtitle header subtitle
|
|
18
|
+
* kmwc:layout page (default) | bare (no header / container — e.g. the index)
|
|
19
|
+
* kmwc:entry component entry script; default /<library.entry>, "none" to skip
|
|
20
|
+
* kmwc:body-class classes for <body> (e.g. dark-output)
|
|
21
|
+
* <link> and <style> elements anywhere in a fragment move into <head>
|
|
22
|
+
* (not those inside a <script> — there they are strings, e.g. shadow-DOM styles).
|
|
23
|
+
* Full documents (<html …>) are left as they are — pages migrate one at a time.
|
|
24
|
+
*
|
|
25
|
+
* Pure string work on purpose: unit-testable, no HTML parser dependency. The
|
|
26
|
+
* tag shapes are deliberately strict (no nesting of a tag inside itself).
|
|
27
|
+
*/
|
|
28
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
29
|
+
import { join, resolve } from 'node:path';
|
|
30
|
+
import { DEVKIT_ROOT } from '../config.js';
|
|
31
|
+
import { renderTemplate } from '../template.js';
|
|
32
|
+
|
|
33
|
+
const LAYOUT_TEMPLATE_PATH = join(DEVKIT_ROOT, 'templates', 'examples-layout.html');
|
|
34
|
+
const MAX_INCLUDE_DEPTH = 10;
|
|
35
|
+
const DEFAULT_LAYOUT = 'page';
|
|
36
|
+
const LAYOUTS = ['page', 'bare'];
|
|
37
|
+
|
|
38
|
+
const INCLUDE_PATTERN = /<kmwc-include\b([^>]*?)(?:\/>|>([\s\S]*?)<\/kmwc-include>)/g;
|
|
39
|
+
const DEMO_PATTERN = /<kmwc-demo\b([^>]*)>([\s\S]*?)<\/kmwc-demo>/g;
|
|
40
|
+
const SHOWN_SCRIPT_PATTERN = /<script\b([^>]*\bdata-kmwc-show\b[^>]*)>([\s\S]*?)<\/script>/g;
|
|
41
|
+
const PAGE_META_PATTERN = /<meta\s+name="kmwc:([a-z-]+)"\s+content="([^"]*)"\s*\/?>\s*/g;
|
|
42
|
+
const HEAD_ELEMENT_PATTERN = /<link\b[^>]*>\s*|<style\b[^>]*>[\s\S]*?<\/style>\s*/g;
|
|
43
|
+
const ATTRIBUTE_PATTERN = /([a-zA-Z][\w-]*)(?:\s*=\s*"([^"]*)")?/g;
|
|
44
|
+
|
|
45
|
+
const HTML_COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
|
|
46
|
+
// Comments plus whole <script> elements: a <style> / <link> inside a script is a string
|
|
47
|
+
// (shadow-DOM styles, an SVG's own <style>), never a page element to move into <head>.
|
|
48
|
+
const COMMENT_OR_SCRIPT_PATTERN = /<!--[\s\S]*?-->|<script\b[^>]*>[\s\S]*?<\/script>/gi;
|
|
49
|
+
// A full document STARTS with a doctype or <html> (after a BOM, whitespace, comments). Matching
|
|
50
|
+
// `<html>` anywhere would misread a fragment whose script or code sample mentions it.
|
|
51
|
+
const DOCUMENT_START_PATTERN = /^?(?:\s|<!--[\s\S]*?-->)*<(?:!doctype\b|html[\s>])/i;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Apply `mapper` to the text OUTSIDE HTML comments only, so a usage example in
|
|
55
|
+
* a comment (`<!-- <kmwc-include src="…"> -->`) is never expanded.
|
|
56
|
+
*/
|
|
57
|
+
export function mapOutsideComments(html, mapper, skippedPattern = HTML_COMMENT_PATTERN) {
|
|
58
|
+
let mappedHtml = '';
|
|
59
|
+
let lastIndex = 0;
|
|
60
|
+
for (const skipped of html.matchAll(skippedPattern)) {
|
|
61
|
+
mappedHtml += mapper(html.slice(lastIndex, skipped.index)) + skipped[0];
|
|
62
|
+
lastIndex = skipped.index + skipped[0].length;
|
|
63
|
+
}
|
|
64
|
+
return mappedHtml + mapper(html.slice(lastIndex));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const mapToCamelCase = (kebabName) => kebabName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
68
|
+
|
|
69
|
+
const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"' };
|
|
70
|
+
const mapToEscapedHtml = (text) => String(text).replace(/[&<>"]/g, (character) => HTML_ESCAPES[character]);
|
|
71
|
+
|
|
72
|
+
/** `preview-class="x" code` → { previewClass: 'x', code: '' } */
|
|
73
|
+
export function getAttributes(attributeText) {
|
|
74
|
+
const attributes = {};
|
|
75
|
+
for (const match of attributeText.matchAll(ATTRIBUTE_PATTERN)) {
|
|
76
|
+
attributes[mapToCamelCase(match[1])] = match[2] ?? '';
|
|
77
|
+
}
|
|
78
|
+
return attributes;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Strip the common leading indentation and surrounding blank lines. */
|
|
82
|
+
export function mapToDedentedText(text) {
|
|
83
|
+
const lines = text.replace(/\t/g, ' ').split(/\r?\n/);
|
|
84
|
+
while (lines.length && !lines[0].trim()) lines.shift();
|
|
85
|
+
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
|
|
86
|
+
const indentation = Math.min(...lines.filter((line) => line.trim()).map((line) => line.match(/^ */)[0].length));
|
|
87
|
+
return lines.map((line) => line.slice(Number.isFinite(indentation) ? indentation : 0)).join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Replace every <kmwc-include> (recursively). Parameters are the tag's
|
|
92
|
+
* attributes (kebab → camel: `note-title` → {{noteTitle}}) plus {{content}}
|
|
93
|
+
* (the tag's inner HTML). Unknown {{placeholders}} are left untouched — a
|
|
94
|
+
* partial may legitimately show template syntax.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveIncludes(html, { root, includeChain = [], readFile = defaultReadFile }) {
|
|
97
|
+
if (includeChain.length > MAX_INCLUDE_DEPTH) throw new Error(`kmwc: includes nested deeper than ${MAX_INCLUDE_DEPTH}: ${includeChain.join(' → ')}`);
|
|
98
|
+
return mapOutsideComments(html, (text) =>
|
|
99
|
+
text.replace(INCLUDE_PATTERN, (includeTag, attributeText, innerHtml = '') => {
|
|
100
|
+
const { src: partialPath, ...parameters } = getAttributes(attributeText);
|
|
101
|
+
if (!partialPath) throw new Error(`kmwc: <kmwc-include> without src: ${includeTag.slice(0, 80)}`);
|
|
102
|
+
if (includeChain.includes(partialPath)) throw new Error(`kmwc: include cycle: ${[...includeChain, partialPath].join(' → ')}`);
|
|
103
|
+
const partialHtml = readFile(resolve(root, partialPath), partialPath);
|
|
104
|
+
const variables = { ...parameters, content: innerHtml.trim() };
|
|
105
|
+
const filledHtml = partialHtml.replace(/\{\{\s*([a-zA-Z][\w]*)\s*\}\}/g, (placeholder, name) => (name in variables ? variables[name] : placeholder));
|
|
106
|
+
return resolveIncludes(filledHtml, { root, includeChain: [...includeChain, partialPath], readFile });
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function defaultReadFile(absolutePath, displayPath) {
|
|
112
|
+
if (!existsSync(absolutePath)) throw new Error(`kmwc: <kmwc-include src="${displayPath}"> — file not found (${absolutePath})`);
|
|
113
|
+
return readFileSync(absolutePath, 'utf8');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderCodeBlock(codeText, language) {
|
|
117
|
+
return `<div class="code-block kmwc-demo__code" data-language="${language}"><pre><code>${mapToEscapedHtml(codeText)}</code></pre></div>`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Turn every <kmwc-demo> into live markup + the code that produced it.
|
|
122
|
+
* Attributes: `preview-class` (default "demo-area"), `markup-hidden` (show
|
|
123
|
+
* only the scripts' code), `code-hidden` (live only — rarely what you want).
|
|
124
|
+
*/
|
|
125
|
+
export function renderDemos(html) {
|
|
126
|
+
return mapOutsideComments(html, (text) => text.replace(DEMO_PATTERN, renderDemo));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderDemo(demoTag, attributeText, innerHtml) {
|
|
130
|
+
const attributes = getAttributes(attributeText);
|
|
131
|
+
const shownScripts = [];
|
|
132
|
+
const markupHtml = innerHtml.replace(SHOWN_SCRIPT_PATTERN, (scriptTag, scriptAttributeText, scriptBody) => {
|
|
133
|
+
shownScripts.push({ attributeText: scriptAttributeText.replace(/\s*\bdata-kmwc-show\b(="[^"]*")?/, ''), body: scriptBody });
|
|
134
|
+
return '';
|
|
135
|
+
});
|
|
136
|
+
const codeBlocks = [];
|
|
137
|
+
if (!('markupHidden' in attributes) && markupHtml.trim()) codeBlocks.push(renderCodeBlock(mapToDedentedText(markupHtml), 'html'));
|
|
138
|
+
for (const script of shownScripts) codeBlocks.push(renderCodeBlock(mapToDedentedText(script.body), 'js'));
|
|
139
|
+
|
|
140
|
+
const previewClass = attributes.previewClass ?? 'demo-area';
|
|
141
|
+
const scriptsHtml = shownScripts.map((script) => `<script${script.attributeText}>${script.body}</script>`).join('\n');
|
|
142
|
+
return (
|
|
143
|
+
`<div class="kmwc-demo">\n` +
|
|
144
|
+
`<div class="kmwc-demo__preview ${previewClass}">${markupHtml}</div>\n` +
|
|
145
|
+
('codeHidden' in attributes ? '' : codeBlocks.join('\n')) +
|
|
146
|
+
`\n</div>\n${scriptsHtml}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Read and remove the page's <meta name="kmwc:…"> tags (`kmwc:body-class` → `bodyClass`). */
|
|
151
|
+
export function getPageMeta(html) {
|
|
152
|
+
const pageMeta = {};
|
|
153
|
+
const htmlWithoutMeta = mapOutsideComments(html, (text) =>
|
|
154
|
+
text.replace(PAGE_META_PATTERN, (metaTag, name, content) => {
|
|
155
|
+
pageMeta[mapToCamelCase(name)] = content;
|
|
156
|
+
return '';
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
return { pageMeta, htmlWithoutMeta };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const checkIsFragment = (html) => !DOCUMENT_START_PATTERN.test(html);
|
|
163
|
+
|
|
164
|
+
/** Wrap a fragment page in the devkit layout. */
|
|
165
|
+
export function renderLayout(fragmentHtml, { config, pagePath }) {
|
|
166
|
+
const { pageMeta, htmlWithoutMeta } = getPageMeta(fragmentHtml);
|
|
167
|
+
const layout = pageMeta.layout || DEFAULT_LAYOUT;
|
|
168
|
+
if (!LAYOUTS.includes(layout)) throw new Error(`kmwc: ${pagePath}: unknown kmwc:layout "${layout}" (${LAYOUTS.join(' | ')})`);
|
|
169
|
+
if (layout === 'page' && !pageMeta.title) throw new Error(`kmwc: ${pagePath}: a fragment page needs <meta name="kmwc:title" content="…">`);
|
|
170
|
+
|
|
171
|
+
const headElements = [];
|
|
172
|
+
const bodyContent = mapOutsideComments(
|
|
173
|
+
htmlWithoutMeta,
|
|
174
|
+
(text) =>
|
|
175
|
+
text.replace(HEAD_ELEMENT_PATTERN, (element) => {
|
|
176
|
+
headElements.push(` ${element.trim()}`);
|
|
177
|
+
return '';
|
|
178
|
+
}),
|
|
179
|
+
COMMENT_OR_SCRIPT_PATTERN
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const entry = pageMeta.entry ?? `/${config.library.entry.replace(/^\.?\//, '')}`;
|
|
183
|
+
const pageTitle = pageMeta.title || config.title;
|
|
184
|
+
const bodyHtml =
|
|
185
|
+
layout === 'bare'
|
|
186
|
+
? bodyContent.trim()
|
|
187
|
+
: [
|
|
188
|
+
'<div class="container">',
|
|
189
|
+
' <header>',
|
|
190
|
+
' <a href="index.html" class="back-link">← Back to Examples</a>',
|
|
191
|
+
` <h1>${pageTitle}</h1>`,
|
|
192
|
+
pageMeta.subtitle ? ` <p class="subtitle">${pageMeta.subtitle}</p>` : '',
|
|
193
|
+
' </header>',
|
|
194
|
+
bodyContent.trim(),
|
|
195
|
+
'</div>',
|
|
196
|
+
]
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.join('\n');
|
|
199
|
+
|
|
200
|
+
return renderTemplate(readFileSync(LAYOUT_TEMPLATE_PATH, 'utf8'), {
|
|
201
|
+
DOCUMENT_TITLE: layout === 'bare' ? pageTitle : `${pageTitle} — ${mapToEscapedHtml(config.title)}`,
|
|
202
|
+
HEAD_HTML: headElements.join('\n'),
|
|
203
|
+
BODY_ATTRIBUTES: pageMeta.bodyClass ? ` class="${pageMeta.bodyClass}"` : '',
|
|
204
|
+
BODY_HTML: bodyHtml,
|
|
205
|
+
// In <head>: module scripts run in document order, so the element is
|
|
206
|
+
// defined before the page's own scripts run.
|
|
207
|
+
ENTRY_SCRIPT_HTML: entry === 'none' ? '' : ` <script type="module" src="${entry}"></script>`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The whole pipeline: includes → demos → layout (fragments only). */
|
|
212
|
+
export function composePage(html, { config, pagePath, readFile }) {
|
|
213
|
+
let composedHtml = resolveIncludes(html, { root: config.root, readFile });
|
|
214
|
+
composedHtml = renderDemos(composedHtml);
|
|
215
|
+
return checkIsFragment(composedHtml) ? renderLayout(composedHtml, { config, pagePath }) : composedHtml;
|
|
216
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
{{HEADER}}
|
|
3
|
+
#
|
|
4
|
+
# Examples site for {{PACKAGE_NAME}}: Vite multi-page build → static nginx.
|
|
5
|
+
|
|
6
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
# Stage 1 — build: `npm run build:examples` (= vite build --mode examples)
|
|
8
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
FROM node:{{NODE_IMAGE}} AS build
|
|
10
|
+
WORKDIR /app
|
|
11
|
+
|
|
12
|
+
# Latest npm first — the bundled one can lag by weeks; newer npm ships registry
|
|
13
|
+
# hardening that reduces supply-chain exposure. Own layer, cached per base image.
|
|
14
|
+
RUN npm install -g npm@latest
|
|
15
|
+
|
|
16
|
+
# NOT `npm ci`: local lockfiles may resolve sibling packages (core, devkit) to
|
|
17
|
+
# file: links that don't exist in this build context. A plain install
|
|
18
|
+
# re-resolves them from the registry. Manifests first so this layer stays
|
|
19
|
+
# cached until dependencies change.
|
|
20
|
+
{{COPY_PACKAGE_JSONS}}
|
|
21
|
+
RUN {{INSTALL_COMMAND}}
|
|
22
|
+
|
|
23
|
+
# Source + build. The kmwc Vite plugin bundles each example page, injects the
|
|
24
|
+
# shared kit, stamps the version badge (BUILD_TIME, default: now) and adds the
|
|
25
|
+
# analytics snippet — no post-build sed.
|
|
26
|
+
COPY . .
|
|
27
|
+
ARG BUILD_TIME
|
|
28
|
+
ENV BUILD_TIME=${BUILD_TIME}
|
|
29
|
+
RUN npm run build:examples
|
|
30
|
+
|
|
31
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
# Stage 2 — serve
|
|
33
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
FROM nginx:alpine AS serve
|
|
35
|
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
36
|
+
COPY --from=build /app/{{OUT_DIR}} /usr/share/nginx/html
|
|
37
|
+
|
|
38
|
+
EXPOSE 80
|
|
39
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
{{HEADER}}
|
|
2
|
+
#
|
|
3
|
+
# Repo-specific targets go in Makefile.local (included at the end; a target
|
|
4
|
+
# redefined there wins, with a make "overriding recipe" warning). Per-developer
|
|
5
|
+
# overrides (container runner, image name, port) go in .makefile.env.
|
|
6
|
+
|
|
7
|
+
.PHONY: help setup dev kill-port build build-examples preview-examples package publish-dry publish publish-rc clean check-version check-vars doctor sync test test-e2e test-e2e-ui test-e2e-headed test-e2e-install image-build image-run image-stop image-clean
|
|
8
|
+
|
|
9
|
+
-include .makefile.env
|
|
10
|
+
DOCKER_RUNNER ?= {{RUNNER}}
|
|
11
|
+
IMAGE_NAME ?= {{IMAGE_NAME}}
|
|
12
|
+
CONTAINER_NAME ?= {{CONTAINER_NAME}}
|
|
13
|
+
IMAGE_PORT ?= {{IMAGE_PORT}}
|
|
14
|
+
DEV_PORT := {{DEV_PORT}}
|
|
15
|
+
PREVIEW_PORT := {{PREVIEW_PORT}}
|
|
16
|
+
|
|
17
|
+
help: ## Show this help message
|
|
18
|
+
@echo "Available targets:"
|
|
19
|
+
@grep -hE '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-18s %s\n", $$1, $$2}'
|
|
20
|
+
|
|
21
|
+
setup: ## Install dependencies
|
|
22
|
+
npm install
|
|
23
|
+
|
|
24
|
+
dev: ## Start the dev server (examples on http://localhost:{{DEV_PORT}})
|
|
25
|
+
@rm -rf node_modules/.vite
|
|
26
|
+
npm run dev
|
|
27
|
+
|
|
28
|
+
# Free the dev + preview ports. Vite runs with strictPort, so a stale server is
|
|
29
|
+
# the only thing that can hold them. On Windows a PowerShell one-liner covers
|
|
30
|
+
# IPv4 + IPv6 listeners; `$$_` escapes make's `$` and the single quotes stop sh
|
|
31
|
+
# from expanding `$_` before PowerShell sees it.
|
|
32
|
+
kill-port: ## Free the dev/preview ports ({{DEV_PORT}}, {{PREVIEW_PORT}})
|
|
33
|
+
ifeq ($(OS),Windows_NT)
|
|
34
|
+
-@powershell -NoProfile -Command '$(DEV_PORT),$(PREVIEW_PORT) | ForEach-Object { Get-NetTCPConnection -LocalPort $$_ -State Listen -ErrorAction SilentlyContinue } | Select-Object -ExpandProperty OwningProcess -Unique | ForEach-Object { Stop-Process -Id $$_ -Force -ErrorAction SilentlyContinue }'
|
|
35
|
+
else
|
|
36
|
+
-@for p in $(DEV_PORT) $(PREVIEW_PORT); do lsof -ti tcp:$$p | xargs -r kill -9; done
|
|
37
|
+
endif
|
|
38
|
+
@echo "Ports $(DEV_PORT), $(PREVIEW_PORT) are free"
|
|
39
|
+
|
|
40
|
+
build: ## Build the library (dist/)
|
|
41
|
+
npm run build
|
|
42
|
+
|
|
43
|
+
build-examples: ## Build the static examples site ({{OUT_DIR}}/)
|
|
44
|
+
npm run build:examples
|
|
45
|
+
|
|
46
|
+
preview-examples: build-examples ## Serve the built examples site (http://localhost:{{PREVIEW_PORT}})
|
|
47
|
+
npm run preview:examples
|
|
48
|
+
|
|
49
|
+
package: build ## Create an npm tarball
|
|
50
|
+
npm pack
|
|
51
|
+
|
|
52
|
+
publish-dry: ## Publish dry run (clean build first)
|
|
53
|
+
npm run clean:dist
|
|
54
|
+
npm run build
|
|
55
|
+
npm publish --dry-run
|
|
56
|
+
|
|
57
|
+
# Release gates (publish-command.md): the version must fit the dist-tag
|
|
58
|
+
# (latest = no pre-release suffix, rc = -rcN), must not be on npm yet, and the
|
|
59
|
+
# e2e suite must pass. `kmwc check-release` does the first two.
|
|
60
|
+
publish: ## Publish to npm as 'latest' (release versions only)
|
|
61
|
+
npx kmwc check-release --tag latest
|
|
62
|
+
@echo "WARNING: publishing to npm under the 'latest' dist-tag. Use 'make publish-rc' for pre-releases."
|
|
63
|
+
@$(CONFIRM)
|
|
64
|
+
npm run clean:dist
|
|
65
|
+
npm run build
|
|
66
|
+
ifneq ($(wildcard playwright.config.*),)
|
|
67
|
+
npm run test:e2e
|
|
68
|
+
endif
|
|
69
|
+
npm publish
|
|
70
|
+
|
|
71
|
+
publish-rc: ## Publish to npm under the 'rc' dist-tag (does NOT touch 'latest')
|
|
72
|
+
npx kmwc check-release --tag rc
|
|
73
|
+
@echo "WARNING: publishing to npm under the 'rc' dist-tag."
|
|
74
|
+
@$(CONFIRM)
|
|
75
|
+
npm run clean:dist
|
|
76
|
+
npm run build
|
|
77
|
+
ifneq ($(wildcard playwright.config.*),)
|
|
78
|
+
npm run test:e2e
|
|
79
|
+
endif
|
|
80
|
+
npm publish --tag rc
|
|
81
|
+
|
|
82
|
+
clean: ## Clean build artifacts
|
|
83
|
+
npm run clean
|
|
84
|
+
|
|
85
|
+
check-version: ## Show the current package version
|
|
86
|
+
@node -p "require('./package.json').version"
|
|
87
|
+
|
|
88
|
+
check-vars: ## Validate component-variables.manifest.json against the CSS
|
|
89
|
+
npx kmwc check-vars
|
|
90
|
+
|
|
91
|
+
doctor: ## Check this repo against the Keenmate component standard
|
|
92
|
+
npx kmwc doctor
|
|
93
|
+
|
|
94
|
+
sync: ## Regenerate devkit-owned files (Dockerfile, nginx.conf, Makefile, …)
|
|
95
|
+
npx kmwc sync
|
|
96
|
+
|
|
97
|
+
test: ## Run unit tests
|
|
98
|
+
npm test
|
|
99
|
+
|
|
100
|
+
test-e2e: ## Run Playwright e2e tests (headless)
|
|
101
|
+
npm run test:e2e
|
|
102
|
+
|
|
103
|
+
test-e2e-ui: ## Run Playwright e2e tests in UI mode
|
|
104
|
+
npm run test:e2e:ui
|
|
105
|
+
|
|
106
|
+
test-e2e-headed: ## Run Playwright e2e tests headed
|
|
107
|
+
npm run test:e2e:headed
|
|
108
|
+
|
|
109
|
+
test-e2e-install: ## Install the chromium browser binary (one-time)
|
|
110
|
+
npm run test:e2e:install
|
|
111
|
+
|
|
112
|
+
# ── Container image (examples site) ──────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
image-build: ## Build the examples container image
|
|
115
|
+
$(DOCKER_RUNNER) build --build-arg BUILD_TIME=$$(date -u +%Y-%m-%dT%H:%MZ) -t $(IMAGE_NAME) .
|
|
116
|
+
|
|
117
|
+
image-run: ## Run the examples image on IMAGE_PORT (default {{IMAGE_PORT}})
|
|
118
|
+
-@$(DOCKER_RUNNER) rm -f $(CONTAINER_NAME) >/dev/null 2>&1
|
|
119
|
+
$(DOCKER_RUNNER) run -d --name $(CONTAINER_NAME) -p $(IMAGE_PORT):80 $(IMAGE_NAME)
|
|
120
|
+
@echo "Serving examples at http://localhost:$(IMAGE_PORT)"
|
|
121
|
+
|
|
122
|
+
image-stop: ## Stop and remove the examples container
|
|
123
|
+
-@$(DOCKER_RUNNER) rm -f $(CONTAINER_NAME) >/dev/null 2>&1
|
|
124
|
+
|
|
125
|
+
image-clean: image-stop ## Remove the examples container and image
|
|
126
|
+
-@$(DOCKER_RUNNER) rmi $(IMAGE_NAME) >/dev/null 2>&1
|
|
127
|
+
|
|
128
|
+
ifeq ($(OS),Windows_NT)
|
|
129
|
+
CONFIRM = powershell -NoProfile -Command "Write-Host 'Press Enter to continue, Ctrl+C to cancel...'; Read-Host | Out-Null"
|
|
130
|
+
else
|
|
131
|
+
CONFIRM = read -p "Press Enter to continue, Ctrl+C to cancel... " _
|
|
132
|
+
endif
|
|
133
|
+
|
|
134
|
+
-include Makefile.local
|
|
135
|
+
|
|
136
|
+
.DEFAULT_GOAL := help
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
## Shared infrastructure — `@keenmate/web-components-devkit` (read before touching examples/build files)
|
|
2
|
+
|
|
3
|
+
This repo follows the Keenmate component standard: the BlissFramework rulebook
|
|
4
|
+
(`C:\Git\BlissFramework\guidelines\web-components\` — structure, naming, CSS,
|
|
5
|
+
theming, README, CEM, publish) plus the devkit (`../web-components`, published
|
|
6
|
+
as `@keenmate/web-components-devkit`), which owns the example-site kit, the
|
|
7
|
+
Vite/Playwright configs and the deploy files. Do not re-create or patch those
|
|
8
|
+
locally — every local fork is how they drifted apart.
|
|
9
|
+
|
|
10
|
+
- **Never add** `examples-chapter-nav.js`, `examples-copy-link.js`,
|
|
11
|
+
`examples-controls-store.js`, `examples-shared.css`, `plausible-snippet.html`
|
|
12
|
+
or `scripts/check-variable-manifest.mjs`. The chapter nav, copy-link buttons,
|
|
13
|
+
shared styles, theme switch, version badge and analytics are injected into
|
|
14
|
+
every root `*.html` page by the Vite plugin automatically.
|
|
15
|
+
- **Generated — do not edit:** `Dockerfile`, `nginx.conf`, `Makefile`,
|
|
16
|
+
`.dockerignore` (they start with a `GENERATED by kmwc sync` header). Change
|
|
17
|
+
`package.json#kmwc` and run `npx kmwc sync`, or fix the template in the devkit.
|
|
18
|
+
Repo-specific make targets go in `Makefile.local`.
|
|
19
|
+
- **Per-component settings** (title, tagline, CSS prefix, ports, accent colours,
|
|
20
|
+
analytics id, image name, canonical origin) live **only** in `package.json#kmwc`.
|
|
21
|
+
- **Example page styles:** shared rules are in `@layer kmwc` and read
|
|
22
|
+
`--base-*` tokens (pages support light / dark / auto via the theme switch), so
|
|
23
|
+
any unlayered rule wins. Put component-only rules in `examples-local.css`.
|
|
24
|
+
Re-theme with `kmwc.theme.accentColor`, never by copying CSS. If a second
|
|
25
|
+
component needs a pattern, it belongs in the devkit.
|
|
26
|
+
- **Example pages are fragments:** no `<html>`/`<head>`/`<header>` — start with
|
|
27
|
+
`<meta name="kmwc:title" content="…">`, the devkit layout adds the rest. Shared text
|
|
28
|
+
goes in `partials/` via `<kmwc-include>`; show code with `<kmwc-demo>` (the live
|
|
29
|
+
markup IS the snippet) — never hand-copy a demo into a `.code-block`.
|
|
30
|
+
- **Controls persistence:**
|
|
31
|
+
`import { attachControlsPersistence } from '@keenmate/web-components-devkit/examples'`.
|
|
32
|
+
**Landing page:** `renderExamplesIndex({ pages })` from the same module.
|
|
33
|
+
- **Found a bug in a shared helper?** Fix it in `../web-components` (so every
|
|
34
|
+
component gets it), not here. Say so in your summary if you can't.
|
|
35
|
+
- **Never ship AI-assistant files:** `package.json` `files` must keep
|
|
36
|
+
`"!**/CLAUDE.md"` and `"!**/.claude"` (a `CLAUDE.md` you add under `src/` is
|
|
37
|
+
otherwise published with it).
|
|
38
|
+
- Before finishing: `npx kmwc doctor` must pass — it runs the devkit checks,
|
|
39
|
+
checks the `npm pack` file list for dev-only files, and runs the rulebook's
|
|
40
|
+
`*.checks.sh` suites.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{DOCUMENT_TITLE}}</title>
|
|
7
|
+
{{ENTRY_SCRIPT_HTML}}
|
|
8
|
+
{{HEAD_HTML}}
|
|
9
|
+
</head>
|
|
10
|
+
<body{{BODY_ATTRIBUTES}}>
|
|
11
|
+
{{BODY_HTML}}
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|