tkeron 5.3.0 → 6.0.0
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/README.md +17 -16
- package/bun.lock +13 -196
- package/changelog.md +37 -0
- package/examples/init_sample/websrc/api-service.ts +24 -24
- package/examples/init_sample/websrc/counter-card.com.html +5 -2
- package/examples/init_sample/websrc/hero-section.com.html +35 -5
- package/examples/init_sample/websrc/html-components-card.com.html +6 -2
- package/examples/init_sample/websrc/index.html +58 -25
- package/examples/init_sample/websrc/index.pre.ts +20 -15
- package/examples/init_sample/websrc/ts-components-card.com.html +7 -2
- package/index.ts +15 -0
- package/package.json +9 -13
- package/skills/tkeron/SKILL.md +286 -0
- package/skills/tkeron-components/SKILL.md +785 -0
- package/skills/tkeron-organization/SKILL.md +230 -0
- package/skills/tkeron-patterns/SKILL.md +495 -0
- package/skills/tkeron-testing/SKILL.md +194 -0
- package/skills/tkeron-troubleshooting/SKILL.md +259 -0
- package/src/build.ts +4 -6
- package/src/develop.ts +1 -2
- package/src/init.ts +1 -2
- package/src/skills.ts +139 -0
- package/src/skillsWrapper.ts +79 -0
- package/docs/mcp-server.md +0 -240
- package/mcp-server.ts +0 -272
- package/src/cleanupOrphanedTempDirs.ts +0 -31
- package/src/promptUser.ts +0 -15
- package/src/setupSigintHandler.ts +0 -6
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tkeron-patterns
|
|
3
|
+
description: "Tkeron implementation patterns, anti-patterns, and the mandatory maximum pre-render rule. Use this skill when writing or reviewing tkeron code — components, pre/post scripts, browser TS — to apply correct patterns (escapeHtml, parallel fetch, shared meta utils, attribute validation) and avoid common anti-patterns (building UI in JS, .js refs, absolute paths, event listeners in .com.ts, browser APIs at build time, fetch without timeout, silent errors). Also covers basic security and performance."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tkeron — Patterns, Anti-Patterns and the Max-Pre-render Rule
|
|
7
|
+
|
|
8
|
+
## ⚡ Maximum Pre-render (MANDATORY)
|
|
9
|
+
|
|
10
|
+
**FUNDAMENTAL RULE**: The HTML structure of the page is defined in `websrc/*.html` or in Tkeron components (`.com.html` / `.com.ts`). Browser JS **only fills data** into already existing nodes. It is FORBIDDEN to build entire UI structure from scratch in client-side JS.
|
|
11
|
+
|
|
12
|
+
### Render hierarchy — from highest to lowest priority
|
|
13
|
+
|
|
14
|
+
| Level | What goes here | Examples |
|
|
15
|
+
| ------------------------------------------------ | ---------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
16
|
+
| **1. `websrc/*.html` (always)** | All fixed structure of the page | `<thead>`, labels, tabs, containers, headers, buttons, empty KPI cards |
|
|
17
|
+
| **2. `.com.html` / `.com.ts` (build time)** | HTML shared across pages or with build-time variants | `<app-nav>`, KPI cards with structure, badges |
|
|
18
|
+
| **3. `.pre.ts` (build time with data)** | Data available at build time | Static config, feature flags, build-time fetch |
|
|
19
|
+
| **4. `.post.ts` (build time, after components)** | Final DOM rewrites once components are resolved | `rel="noopener"`, `loading="lazy"`, auto heading ids, TOC |
|
|
20
|
+
| **5. Browser JS — DATA ONLY** | The only valid use: fill already existing nodes | `el.textContent = val`, `el.setAttribute(...)`, `.classList.add(...)` |
|
|
21
|
+
| **6. Browser JS — builds DOM** | Last resort, only when absolutely necessary | Table rows whose count changes with user data |
|
|
22
|
+
|
|
23
|
+
### Most common mistake: empty app waiting for JS
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
❌ BAD — the whole UI is built in JS, the HTML is a shell:
|
|
27
|
+
<div id="dashboard" style="display:none"></div>
|
|
28
|
+
<div class="loading">Loading...</div>
|
|
29
|
+
<!-- user sees a blank screen until the fetch completes -->
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
✅ GOOD — full structure in HTML, pending numbers with a placeholder:
|
|
34
|
+
<div class="kpi-grid">
|
|
35
|
+
<div class="kpi-card kpi-critical">
|
|
36
|
+
<span class="kpi-number" id="kpi-critical-val">·</span>
|
|
37
|
+
<span class="kpi-label">Critical</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<!-- JS only: document.getElementById("kpi-critical-val").textContent = s.critical -->
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Quick test: is it pre-rendered enough?
|
|
44
|
+
|
|
45
|
+
> If the API returns empty data (`[]` or `{}`), does the page still show structure (headers, columns, labels)? If yes → correct. If the page is blank → **the HTML lacks enough pre-render**.
|
|
46
|
+
|
|
47
|
+
### When it IS valid to build DOM in JS
|
|
48
|
+
|
|
49
|
+
| Case | Build in JS? | Why |
|
|
50
|
+
| ------------------------------------ | ---------------------------------- | --------------------------------- |
|
|
51
|
+
| Product table rows | ✅ | Row count depends on user data |
|
|
52
|
+
| Table structure (`<thead>`, columns) | ❌ | Always in HTML, never built in JS |
|
|
53
|
+
| KPI cards (changing numbers) | ❌ for structure, ✅ for the value | Card in HTML, number set by JS |
|
|
54
|
+
| Banners that appear/disappear | ✅ | Created and destroyed dynamically |
|
|
55
|
+
| Tabs, sections, labels | ❌ | Always in HTML |
|
|
56
|
+
| Conditional text ("·" → real value) | ✅ | `el.textContent = value` |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## ✅ Patterns — Do
|
|
61
|
+
|
|
62
|
+
### Component with interactivity (build + runtime)
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<!-- counter-btn.com.html -->
|
|
66
|
+
<button class="counter-button" data-count="0">
|
|
67
|
+
Count: <span class="count">0</span>
|
|
68
|
+
</button>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// index.ts (browser)
|
|
73
|
+
document.querySelectorAll(".counter-button").forEach((btn) => {
|
|
74
|
+
let count = 0;
|
|
75
|
+
btn.addEventListener("click", () => {
|
|
76
|
+
count++;
|
|
77
|
+
const span = btn.querySelector(".count");
|
|
78
|
+
if (span) span.textContent = count.toString();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Pre-render with fetch + timeout (ALWAYS use AbortController)
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// index.pre.ts
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
setTimeout(() => controller.abort(), 5000);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch("https://api.example.com/posts", {
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
});
|
|
94
|
+
const posts = await res.json();
|
|
95
|
+
const container = document.getElementById("posts");
|
|
96
|
+
if (container) {
|
|
97
|
+
container.innerHTML = posts
|
|
98
|
+
.map(
|
|
99
|
+
(p: { title: string }) =>
|
|
100
|
+
`<article><h2>${escapeHtml(p.title)}</h2></article>`,
|
|
101
|
+
)
|
|
102
|
+
.join("");
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
console.warn("API fetch failed, using fallback");
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Parallel fetch in pre-render
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// index.pre.ts
|
|
113
|
+
const [posts, authors, tags] = await Promise.all([
|
|
114
|
+
fetch("https://api.example.com/posts").then((r) => r.json()),
|
|
115
|
+
fetch("https://api.example.com/authors").then((r) => r.json()),
|
|
116
|
+
fetch("https://api.example.com/tags").then((r) => r.json()),
|
|
117
|
+
]);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Shared `<head>` meta tags via a `.pre.ts` util
|
|
121
|
+
|
|
122
|
+
The `<head>` (title, description, og:\*, canonical) has identical structure across pages — only the values change. Canonical pattern: a build-time util in `websrc/utils/` imported from each page's `.pre.ts`.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// websrc/utils/page-meta.ts
|
|
126
|
+
export interface PageMeta {
|
|
127
|
+
title: string;
|
|
128
|
+
description: string;
|
|
129
|
+
ogTitle?: string;
|
|
130
|
+
ogDescription?: string;
|
|
131
|
+
ogUrl: string;
|
|
132
|
+
canonical?: string;
|
|
133
|
+
ogType?: "website" | "article";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function applyPageMeta(doc: Document, meta: PageMeta): void {
|
|
137
|
+
const setMeta = (sel: string, attr: string, val: string) => {
|
|
138
|
+
let el = doc.querySelector<HTMLElement>(sel);
|
|
139
|
+
if (!el) {
|
|
140
|
+
el = doc.createElement("meta");
|
|
141
|
+
doc.head!.appendChild(el);
|
|
142
|
+
}
|
|
143
|
+
el.setAttribute(attr, val);
|
|
144
|
+
};
|
|
145
|
+
const title = doc.querySelector("title");
|
|
146
|
+
if (title) title.textContent = meta.title;
|
|
147
|
+
setMeta('meta[name="description"]', "content", meta.description);
|
|
148
|
+
setMeta('meta[property="og:title"]', "content", meta.ogTitle ?? meta.title);
|
|
149
|
+
setMeta(
|
|
150
|
+
'meta[property="og:description"]',
|
|
151
|
+
"content",
|
|
152
|
+
meta.ogDescription ?? meta.description,
|
|
153
|
+
);
|
|
154
|
+
setMeta('meta[property="og:url"]', "content", meta.ogUrl);
|
|
155
|
+
setMeta('link[rel="canonical"]', "href", meta.canonical ?? meta.ogUrl);
|
|
156
|
+
if (meta.ogType) setMeta('meta[property="og:type"]', "content", meta.ogType);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// websrc/about.pre.ts
|
|
162
|
+
import { applyPageMeta } from "./utils/page-meta.ts";
|
|
163
|
+
|
|
164
|
+
applyPageMeta(document, {
|
|
165
|
+
title: "About | My Site",
|
|
166
|
+
description: "Who we are and what we do.",
|
|
167
|
+
ogUrl: "https://example.com/about",
|
|
168
|
+
ogType: "website",
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Benefit: adding `og:image` or JSON-LD to all pages = a single change in `page-meta.ts`.
|
|
173
|
+
|
|
174
|
+
### Attribute validation in `.com.ts`
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// alert-box.com.ts
|
|
178
|
+
const validTypes = ["info", "success", "warning", "error"] as const;
|
|
179
|
+
type AlertType = (typeof validTypes)[number];
|
|
180
|
+
|
|
181
|
+
const type = (com.getAttribute("type") || "info") as AlertType;
|
|
182
|
+
if (!validTypes.includes(type)) {
|
|
183
|
+
throw new Error(`Invalid type: ${type}. Allowed: ${validTypes.join(", ")}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const message = com.getAttribute("message") || "";
|
|
187
|
+
com.innerHTML = `
|
|
188
|
+
<div class="alert alert-${type}">
|
|
189
|
+
<strong>${type.toUpperCase()}</strong>
|
|
190
|
+
<p>${escapeHtml(message)}</p>
|
|
191
|
+
</div>
|
|
192
|
+
`;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Conditional builds by environment
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// index.pre.ts
|
|
199
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
200
|
+
|
|
201
|
+
if (isProd) {
|
|
202
|
+
const script = document.createElement("script");
|
|
203
|
+
script.src = "https://analytics.example.com/script.js";
|
|
204
|
+
document.body?.appendChild(script);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!isProd) {
|
|
208
|
+
const banner = document.createElement("div");
|
|
209
|
+
banner.textContent = "Development Mode";
|
|
210
|
+
document.body?.insertBefore(banner, document.body.firstChild);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### CSS classes, not repeated inline styles
|
|
215
|
+
|
|
216
|
+
```css
|
|
217
|
+
/* styles/components.css */
|
|
218
|
+
:root {
|
|
219
|
+
--primary: #3b82f6;
|
|
220
|
+
--spacing: 1rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.card {
|
|
224
|
+
padding: var(--spacing);
|
|
225
|
+
border: 1px solid #e5e7eb;
|
|
226
|
+
border-radius: 8px;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// card.com.ts
|
|
232
|
+
com.innerHTML = `<div class="card">${content}</div>`;
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## ❌ Anti-Patterns — Do NOT
|
|
238
|
+
|
|
239
|
+
### ❌ Building entire UI structure in browser JS (the main anti-pattern)
|
|
240
|
+
|
|
241
|
+
The most harmful anti-pattern: JS functions that build whole UI sections with `innerHTML` or `createElement`.
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// ❌ BAD — "renderDashboard" builds the entire structure from JS
|
|
245
|
+
function renderDashboard(data: Stats) {
|
|
246
|
+
const el = document.getElementById("dashboard");
|
|
247
|
+
if (!el) return;
|
|
248
|
+
el.innerHTML = `
|
|
249
|
+
<div class="kpi-grid">
|
|
250
|
+
<div class="kpi-card"><h3>Critical</h3><span>${data.critical}</span></div>
|
|
251
|
+
</div>
|
|
252
|
+
<table>
|
|
253
|
+
<thead><tr><th>Product</th><th>Velocity</th></tr></thead>
|
|
254
|
+
<tbody id="product-rows"></tbody>
|
|
255
|
+
</table>
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
// User sees a blank screen while the fetch runs
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// ✅ GOOD — structure already in HTML, JS only fills the values
|
|
263
|
+
function updateDashboard(data: Stats) {
|
|
264
|
+
const critical = document.getElementById("kpi-critical-val");
|
|
265
|
+
if (critical) critical.textContent = String(data.critical);
|
|
266
|
+
|
|
267
|
+
// Only the rows are dynamic — the table is already in the HTML
|
|
268
|
+
const tbody = document.getElementById("product-rows");
|
|
269
|
+
if (tbody) tbody.innerHTML = data.products.map(renderRow).join("");
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Golden rule**: if you are writing a `renderX` function that generates HTML structure from scratch (divs, headers, labels), that structure belongs in `websrc/*.html`, not in JS.
|
|
274
|
+
|
|
275
|
+
### ❌ Referencing `.js` in HTML (most common build error)
|
|
276
|
+
|
|
277
|
+
```html
|
|
278
|
+
<!-- ❌ BAD — causes "Bundle failed" / "Cannot resolve" -->
|
|
279
|
+
<script src="index.js"></script>
|
|
280
|
+
|
|
281
|
+
<!-- ✅ GOOD — tkeron compiles .ts → .js automatically -->
|
|
282
|
+
<script type="module" src="./index.ts"></script>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### ❌ Absolute paths for assets
|
|
286
|
+
|
|
287
|
+
Tkeron serves the output from a subdirectory. Absolute paths (`/assets/...`) point to the server root, not to the build subdirectory.
|
|
288
|
+
|
|
289
|
+
```html
|
|
290
|
+
<!-- ❌ BAD — breaks if the site is not at root -->
|
|
291
|
+
<link rel="icon" href="/assets/favicon.ico" />
|
|
292
|
+
<img src="/assets/logo.webp" />
|
|
293
|
+
|
|
294
|
+
<!-- ✅ GOOD — relative, works on any host/subdirectory -->
|
|
295
|
+
<link rel="icon" href="./assets/favicon.ico" />
|
|
296
|
+
<img src="./assets/logo.webp" />
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Applies to **every `href`, `src` and `url()`** pointing to local assets in `websrc/`.
|
|
300
|
+
|
|
301
|
+
### ❌ Event listeners in `.com.ts`
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// ❌ BAD — lost in the output, never runs in the browser
|
|
305
|
+
com.addEventListener("click", () => console.log("clicked"));
|
|
306
|
+
|
|
307
|
+
// ✅ GOOD — do it in a browser .ts
|
|
308
|
+
// index.ts
|
|
309
|
+
document.querySelector(".my-btn")!.addEventListener("click", () => {
|
|
310
|
+
console.log("clicked");
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### ❌ Browser APIs at build time
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// ❌ BAD — they don't exist in Bun
|
|
318
|
+
// In .pre.ts, .post.ts or .com.ts:
|
|
319
|
+
window.location.href = "/redirect";
|
|
320
|
+
localStorage.setItem("key", "value");
|
|
321
|
+
navigator.userAgent;
|
|
322
|
+
sessionStorage.getItem("token");
|
|
323
|
+
|
|
324
|
+
// ✅ GOOD — use them only in browser .ts
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### ❌ Local state inside `.com.ts`
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// ❌ BAD — local variables are discarded, no state between instances
|
|
331
|
+
let count = 0;
|
|
332
|
+
com.innerHTML = `<button>Count: ${count}</button>`;
|
|
333
|
+
|
|
334
|
+
// ✅ GOOD — state in browser .ts
|
|
335
|
+
let count = 0;
|
|
336
|
+
document.getElementById("btn")!.addEventListener("click", () => {
|
|
337
|
+
count++;
|
|
338
|
+
document.getElementById("count")!.textContent = String(count);
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### ❌ Accessing `document` from `.com.ts`
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// ❌ BAD — .com.ts only has access to `com`, not to `document`
|
|
346
|
+
const header = document.querySelector("header");
|
|
347
|
+
|
|
348
|
+
// ✅ GOOD — use .pre.ts or .post.ts for full document access
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### ❌ Fetch without timeout at build time
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// ❌ BAD — if the API stalls, the build hangs forever
|
|
355
|
+
const data = await fetch("https://slow-api.com/data");
|
|
356
|
+
|
|
357
|
+
// ✅ GOOD — always with AbortController
|
|
358
|
+
const controller = new AbortController();
|
|
359
|
+
setTimeout(() => controller.abort(), 5000);
|
|
360
|
+
try {
|
|
361
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
362
|
+
return await res.json();
|
|
363
|
+
} catch {
|
|
364
|
+
return fallbackData;
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### ❌ npm packages in the browser
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// ❌ BAD — tkeron does NOT bundle npm packages for the browser
|
|
372
|
+
// index.ts (browser)
|
|
373
|
+
import _ from "lodash"; // does not work
|
|
374
|
+
|
|
375
|
+
// ✅ GOOD — alternatives:
|
|
376
|
+
// 1. CDN: <script src="https://cdn.jsdelivr.net/npm/lodash"></script>
|
|
377
|
+
// 2. Write vanilla JS
|
|
378
|
+
// 3. npm packages DO work in .pre.ts, .post.ts and .com.ts (build time)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### ❌ Silent errors
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// ❌ BAD — the component disappears with no explanation
|
|
385
|
+
const data = com.getAttribute("data");
|
|
386
|
+
if (!data) {
|
|
387
|
+
com.innerHTML = "";
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ✅ GOOD — fail fast with a clear message
|
|
392
|
+
const data = com.getAttribute("data");
|
|
393
|
+
if (!data) {
|
|
394
|
+
throw new Error('Attribute "data" is required for data-table component');
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### ❌ Massive repeated inline styles
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// ❌ BAD — if used 100 times, duplicates KB of CSS
|
|
402
|
+
com.innerHTML = `
|
|
403
|
+
<div style="padding: 1rem; margin: 2rem; background: linear-gradient(...);
|
|
404
|
+
border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
|
405
|
+
${content}
|
|
406
|
+
</div>
|
|
407
|
+
`;
|
|
408
|
+
|
|
409
|
+
// ✅ GOOD — use CSS classes
|
|
410
|
+
com.innerHTML = `<div class="card">${content}</div>`;
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Security
|
|
416
|
+
|
|
417
|
+
### Escape dynamic content (XSS prevention)
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
const escapeHtml = (str: string): string =>
|
|
421
|
+
str
|
|
422
|
+
.replace(/&/g, "&")
|
|
423
|
+
.replace(/</g, "<")
|
|
424
|
+
.replace(/>/g, ">")
|
|
425
|
+
.replace(/"/g, """)
|
|
426
|
+
.replace(/'/g, "'");
|
|
427
|
+
|
|
428
|
+
const text = com.getAttribute("text") || "";
|
|
429
|
+
com.innerHTML = `<p>${escapeHtml(text)}</p>`;
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Rule**: any attribute, fetch result, or user-provided string injected into HTML via template literal MUST pass through `escapeHtml`. The only exception is intentionally pre-sanitized HTML (e.g. output of a Markdown renderer you trust).
|
|
433
|
+
|
|
434
|
+
### Validate URLs
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
const isSafeUrl = (url: string): boolean => {
|
|
438
|
+
try {
|
|
439
|
+
const parsed = new URL(url);
|
|
440
|
+
return ["http:", "https:"].includes(parsed.protocol);
|
|
441
|
+
} catch {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const url = com.getAttribute("href") || "#";
|
|
447
|
+
const safeUrl = isSafeUrl(url) ? url : "#";
|
|
448
|
+
com.innerHTML = `<a href="${safeUrl}">Link</a>`;
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Validate numeric ranges
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
const count = parseInt(com.getAttribute("count") || "5");
|
|
455
|
+
if (isNaN(count) || count < 0 || count > 100) {
|
|
456
|
+
throw new Error("Invalid count: must be 0-100");
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Performance
|
|
463
|
+
|
|
464
|
+
### Build time
|
|
465
|
+
|
|
466
|
+
- `Promise.all()` for parallel fetch
|
|
467
|
+
- Timeout on every external call
|
|
468
|
+
- Cache data when possible (write to `/tmp/`)
|
|
469
|
+
- Limit data volume (`?limit=10`)
|
|
470
|
+
|
|
471
|
+
### Output size
|
|
472
|
+
|
|
473
|
+
- CSS classes instead of inline styles
|
|
474
|
+
- Lean components (little HTML per component)
|
|
475
|
+
- Shared external CSS (`components.css`)
|
|
476
|
+
- CSS variables for colors/spacing
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## AI Agent Checklist
|
|
481
|
+
|
|
482
|
+
When creating/editing a tkeron project:
|
|
483
|
+
|
|
484
|
+
1. ✅ HTML scripts: always `.ts` with `type="module"`, NEVER `.js`
|
|
485
|
+
2. ✅ Asset paths: always relative (`./`), never absolute (`/`)
|
|
486
|
+
3. ✅ Components: ALWAYS with a hyphen, never a standard HTML tag
|
|
487
|
+
4. ✅ Browser code → `.ts`. Build code → `.com.ts` / `.pre.ts` / `.post.ts`
|
|
488
|
+
5. ✅ Build-time fetch → always with AbortController + timeout
|
|
489
|
+
6. ✅ Escape dynamic content (`escapeHtml`)
|
|
490
|
+
7. ✅ CSS classes, not repeated inline styles
|
|
491
|
+
8. ✅ **Maximum pre-render**: HTML structure in `.html` / components, JS only fills data
|
|
492
|
+
9. ✅ npm packages only at build time, CDN for the browser
|
|
493
|
+
10. ✅ `tk build` after every edit, then verify `web/`. NEVER `tk dev` synchronously in agent sessions
|
|
494
|
+
11. ✅ `web/` always in `.gitignore` — never edit, never commit
|
|
495
|
+
12. ✅ No silent errors — `throw` with a clear message instead
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tkeron-testing
|
|
3
|
+
description: "Testing tkeron projects programmatically with getBuildResult() from the 'tkeron' package. Covers the full BuildResult API (dom, getContentAsString, type, size, fileHash), Bun test setup, recipes (assert components inlined, meta tags present, TS compiled), and the rules for fast, deterministic tests. Use this skill when writing or debugging tests for a tkeron project."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tkeron — Testing
|
|
7
|
+
|
|
8
|
+
Tkeron exposes `getBuildResult()` for programmatic testing of the build output. It runs a real build into a temporary directory, returns a structured map of every output file, and cleans up afterwards.
|
|
9
|
+
|
|
10
|
+
## Basic Setup (Bun test)
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { describe, it, expect, beforeAll } from "bun:test";
|
|
14
|
+
import { getBuildResult, type BuildResult } from "tkeron";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
|
|
17
|
+
describe("my project", () => {
|
|
18
|
+
const sourcePath = join(import.meta.dir, "websrc");
|
|
19
|
+
let result: BuildResult;
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
result = await getBuildResult(sourcePath);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("generates index.html", () => {
|
|
26
|
+
expect(result["index.html"]).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("has the correct title", () => {
|
|
30
|
+
const dom = result["index.html"]?.dom;
|
|
31
|
+
const title = dom?.querySelector("title");
|
|
32
|
+
expect(title).toBeDefined();
|
|
33
|
+
expect(title!.textContent).toBe("My Site");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### `getBuildResult(sourcePath, options?) => Promise<BuildResult>`
|
|
43
|
+
|
|
44
|
+
| Param | Type | Notes |
|
|
45
|
+
| ------------ | --------------------- | ---------------------------------------------------------- |
|
|
46
|
+
| `sourcePath` | `string` | Absolute path to the `websrc/` directory. |
|
|
47
|
+
| `options` | `{ logger?: Logger }` | Optional. Pass a custom logger; omit for silent (default). |
|
|
48
|
+
|
|
49
|
+
Returns a `BuildResult` — a record keyed by the relative output path (e.g. `"index.html"`, `"blog/post.html"`, `"styles/main.css"`).
|
|
50
|
+
|
|
51
|
+
### `FileInfo` — value of each entry
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
interface FileInfo {
|
|
55
|
+
fileName: string; // "index.html"
|
|
56
|
+
filePath: string; // "" for root, "blog" for blog/index.html
|
|
57
|
+
path: string; // "index.html" / "blog/post.html"
|
|
58
|
+
type: string; // MIME type (e.g. "text/html")
|
|
59
|
+
size: number; // bytes
|
|
60
|
+
fileHash: string; // sha256 of the output
|
|
61
|
+
getContentAsString?: () => string; // ONLY for text files
|
|
62
|
+
dom?: Document; // ONLY for .html (parsed by @tkeron/html-parser)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Text files (`html`, `css`, `js`, `json`, `txt`, `svg`, `xml`, `ts`, `md`, etc.) get `getContentAsString`. Only `.html` files get `dom`.
|
|
67
|
+
|
|
68
|
+
> **`dom` is from `@tkeron/html-parser`, not jsdom**. It supports `querySelector`, `querySelectorAll`, `textContent`, `getAttribute`, `innerHTML`, etc. Do NOT assume jsdom-only APIs (no `getComputedStyle`, no event dispatch, no `MutationObserver`).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Rules
|
|
73
|
+
|
|
74
|
+
1. **Build ONCE in `beforeAll()`**, NEVER inside each `it()` — a build can take seconds.
|
|
75
|
+
2. **Check existence BEFORE properties**: `expect(el).toBeDefined()` → then `el!.textContent`.
|
|
76
|
+
3. **Assert specific, bounded values**, not full output — whitespace/minification changes break brittle tests.
|
|
77
|
+
4. **Use `dom` for HTML assertions**, `getContentAsString()` for CSS/JS string searches.
|
|
78
|
+
5. **Use absolute paths** for `sourcePath` (`join(import.meta.dir, "websrc")`).
|
|
79
|
+
6. **Tests do not need to clean up** — `getBuildResult` removes its temp directory automatically.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Recipes
|
|
84
|
+
|
|
85
|
+
### Assert a component was inlined
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
it("inlines <site-header>", () => {
|
|
89
|
+
const html = result["index.html"]?.getContentAsString?.() ?? "";
|
|
90
|
+
expect(html).not.toContain("<site-header>");
|
|
91
|
+
expect(html).toContain('class="site-header"');
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Assert meta tags from `.pre.ts`
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
it("sets canonical from page-meta util", () => {
|
|
99
|
+
const dom = result["about.html"]?.dom;
|
|
100
|
+
const canonical = dom?.querySelector('link[rel="canonical"]');
|
|
101
|
+
expect(canonical?.getAttribute("href")).toBe("https://example.com/about");
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Assert TS was compiled and reference rewritten
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
it("rewrites .ts to .js in the page", () => {
|
|
109
|
+
const html = result["index.html"]?.getContentAsString?.() ?? "";
|
|
110
|
+
expect(html).toMatch(/src="\.\/index\.js"/);
|
|
111
|
+
expect(result["index.js"]).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Assert `.post.ts` ran on external links
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
it("post.ts adds rel/target to external links", () => {
|
|
119
|
+
const dom = result["index.html"]?.dom;
|
|
120
|
+
const link = dom?.querySelector('a[href^="http"]');
|
|
121
|
+
expect(link?.getAttribute("rel")).toBe("noopener noreferrer");
|
|
122
|
+
expect(link?.getAttribute("target")).toBe("_blank");
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Assert a `.com.ts` rendered each instance with its attributes
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
it("user-badge renders one card per instance", () => {
|
|
130
|
+
const dom = result["index.html"]?.dom;
|
|
131
|
+
const cards = dom?.querySelectorAll(".badge");
|
|
132
|
+
expect(cards?.length).toBe(3);
|
|
133
|
+
expect(cards?.[0].querySelector(".badge-name")?.textContent).toBe("Alice");
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Assert intermediate files do NOT appear in output
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
it("does not emit .com.* or .pre.ts files", () => {
|
|
141
|
+
const paths = Object.keys(result);
|
|
142
|
+
expect(paths.some((p) => p.endsWith(".com.html"))).toBe(false);
|
|
143
|
+
expect(paths.some((p) => p.endsWith(".com.ts"))).toBe(false);
|
|
144
|
+
expect(paths.some((p) => p.endsWith(".pre.ts"))).toBe(false);
|
|
145
|
+
expect(paths.some((p) => p.endsWith(".post.ts"))).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Assert sub-route page exists
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
it("emits blog/index.html as the /blog/ route", () => {
|
|
153
|
+
expect(result["blog/index.html"]).toBeDefined();
|
|
154
|
+
expect(result["blog/index.html"]?.filePath).toBe("blog");
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Assert hash/size constraints (output budget)
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
it("CSS bundle stays under 50 KB", () => {
|
|
162
|
+
const css = result["styles/main.css"];
|
|
163
|
+
expect(css).toBeDefined();
|
|
164
|
+
expect(css!.size).toBeLessThan(50_000);
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Running Tests
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
bun test # all tests
|
|
174
|
+
bun test src/foo.test.ts # one file
|
|
175
|
+
bun test --watch # watch mode
|
|
176
|
+
NODE_ENV=production bun test # if your build depends on env
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`getBuildResult` honors the same `NODE_ENV` as a normal `tk build`, so you can test prod-only behavior (analytics scripts, dev banners) by setting it.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Common Test Failures
|
|
184
|
+
|
|
185
|
+
| Symptom | Likely cause |
|
|
186
|
+
| ------------------------------------- | ---------------------------------------------------------------------------- |
|
|
187
|
+
| `result["index.html"]` is `undefined` | Wrong `sourcePath`, or `index.html` was renamed/moved. |
|
|
188
|
+
| `dom.querySelector(...)` returns null | Selector doesn't match — remember the wrapper custom tag is gone. |
|
|
189
|
+
| `el.textContent` is empty | `.pre.ts`/`.post.ts` didn't run, or template wasn't loaded. |
|
|
190
|
+
| Test passes locally, fails in CI | Missing env var (`NODE_ENV`), or absolute paths assumed. |
|
|
191
|
+
| Slow test suite | Building inside `it()` — move to `beforeAll`. |
|
|
192
|
+
| Type error on `dom?.querySelector` | Import `Document` from your test setup or cast — the dom is parser-flavored. |
|
|
193
|
+
|
|
194
|
+
For build-time errors (the build itself fails), see `tkeron-troubleshooting`.
|