tkeron 5.3.0 → 6.0.1

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 (59) hide show
  1. package/README.md +17 -16
  2. package/bun.lock +13 -196
  3. package/changelog.md +55 -0
  4. package/examples/init_sample/websrc/about.html +19 -0
  5. package/examples/init_sample/websrc/components/content/about-intro.com.md +7 -0
  6. package/examples/init_sample/websrc/components/layout/hero-section.com.html +88 -0
  7. package/examples/init_sample/websrc/components/layout/site-footer.com.html +20 -0
  8. package/examples/init_sample/websrc/components/layout/site-header.com.html +44 -0
  9. package/examples/init_sample/websrc/components/ui/counter-button.com.html +48 -0
  10. package/examples/init_sample/websrc/components/ui/crypto-prices-card.com.html +52 -0
  11. package/examples/init_sample/websrc/components/ui/html-components-card.com.html +10 -0
  12. package/examples/init_sample/websrc/{info-card.com.html → components/ui/info-card.com.html} +10 -6
  13. package/examples/init_sample/websrc/components/ui/markdown-card.com.html +21 -0
  14. package/examples/init_sample/websrc/{pre-render-card.com.html → components/ui/pre-render-card.com.html} +6 -3
  15. package/examples/init_sample/websrc/components/ui/quote-card.com.html +32 -0
  16. package/examples/init_sample/websrc/components/ui/styles-injector.com.ts +5 -0
  17. package/examples/init_sample/websrc/components/ui/ts-components-card.com.html +11 -0
  18. package/examples/init_sample/websrc/components/ui/user-badge.com.ts +30 -0
  19. package/examples/init_sample/websrc/docs.html +33 -0
  20. package/examples/init_sample/websrc/index.html +10 -209
  21. package/examples/init_sample/websrc/index.post.ts +70 -0
  22. package/examples/init_sample/websrc/index.pre.ts +10 -61
  23. package/examples/init_sample/websrc/index.ts +9 -7
  24. package/examples/init_sample/websrc/styles/global-styles.com.ts +5 -0
  25. package/examples/init_sample/websrc/styles/main.css +112 -0
  26. package/examples/init_sample/websrc/utils/api-service.ts +93 -0
  27. package/examples/with_global_styles/websrc/bad.html +23 -0
  28. package/examples/with_global_styles/websrc/global-styles.com.ts +5 -0
  29. package/examples/with_global_styles/websrc/good.html +22 -0
  30. package/examples/with_global_styles/websrc/index.html +25 -0
  31. package/examples/with_global_styles/websrc/styles.css +24 -0
  32. package/examples/with_style_dedup/websrc/tag-chip.com.html +15 -0
  33. package/examples/with_style_dedup/websrc/tag-chip.com.ts +2 -18
  34. package/index.ts +15 -0
  35. package/package.json +9 -13
  36. package/skills/tkeron/SKILL.md +287 -0
  37. package/skills/tkeron-components/SKILL.md +785 -0
  38. package/skills/tkeron-organization/SKILL.md +231 -0
  39. package/skills/tkeron-patterns/SKILL.md +587 -0
  40. package/skills/tkeron-testing/SKILL.md +194 -0
  41. package/skills/tkeron-troubleshooting/SKILL.md +263 -0
  42. package/src/build.ts +4 -6
  43. package/src/develop.ts +1 -2
  44. package/src/init.ts +1 -2
  45. package/src/skills.ts +139 -0
  46. package/src/skillsWrapper.ts +79 -0
  47. package/docs/mcp-server.md +0 -240
  48. package/examples/init_sample/websrc/api-service.ts +0 -98
  49. package/examples/init_sample/websrc/counter-card.com.html +0 -9
  50. package/examples/init_sample/websrc/favicon.ico +0 -0
  51. package/examples/init_sample/websrc/hero-section.com.html +0 -23
  52. package/examples/init_sample/websrc/html-components-card.com.html +0 -6
  53. package/examples/init_sample/websrc/ts-components-card.com.html +0 -6
  54. package/examples/init_sample/websrc/user-badge.com.ts +0 -17
  55. package/mcp-server.ts +0 -272
  56. package/src/cleanupOrphanedTempDirs.ts +0 -31
  57. package/src/promptUser.ts +0 -15
  58. package/src/setupSigintHandler.ts +0 -6
  59. /package/examples/init_sample/websrc/{profile.png → assets/profile.png} +0 -0
@@ -0,0 +1,587 @@
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 via components (NEVER `<link rel="stylesheet">`)
215
+
216
+ **Rule**: CSS in tkeron lives inside `<style>` blocks emitted by components. Pages MUST NOT use `<link rel="stylesheet">`. This unlocks three properties:
217
+
218
+ 1. **Auto-dedup**: tkeron keeps a single `<style data-tk-com="...">` in `<head>` no matter how many times the component is used.
219
+ 2. **Tree-shaking**: if a component is not used on a page, its CSS does not ship to that page.
220
+ 3. **Zero extra requests**: CSS travels inside the HTML download → first paint without waiting for another request.
221
+
222
+ #### Per-component CSS — in `.com.html`
223
+
224
+ ```html
225
+ <!-- card.com.html -->
226
+ <style>
227
+ .card { padding: 1rem; border: 1px solid #e5e7eb; border-radius: 8px; }
228
+ </style>
229
+ <div class="card"></div>
230
+ ```
231
+
232
+ ```typescript
233
+ // card.com.ts (optional — only if logic is needed)
234
+ const body = com.querySelector(".card");
235
+ if (body) body.textContent = com.getAttribute("text") || "";
236
+ ```
237
+
238
+ > If a `.com.html` exists next to a `.com.ts`, the HTML is loaded into `com.innerHTML` **before** the `.com.ts` runs. The `<style>` is preserved and dedup applies. Do NOT re-emit the `<style>` from `.com.ts`.
239
+
240
+ **Build output — dedup in action.** Component used 6 times → `<head>` receives exactly **one** `<style data-tk-com="tag-chip">` block:
241
+
242
+ ```html
243
+ <!-- websrc/index.html — source has 6 <tag-chip> uses -->
244
+ <tag-chip label="TypeScript"></tag-chip>
245
+ <tag-chip label="HTML"></tag-chip>
246
+ <tag-chip label="CSS"></tag-chip>
247
+ <tag-chip label="Bun"></tag-chip>
248
+ <tag-chip label="tkeron"></tag-chip>
249
+ <tag-chip label="zero deps"></tag-chip>
250
+ ```
251
+
252
+ ```html
253
+ <!-- web/index.html — tkeron output -->
254
+ <head>
255
+ <!-- ONE style block, regardless of how many times the component was used -->
256
+ <style data-tk-com="tag-chip">
257
+ .tag-chip { display: inline-block; background: ...; color: white; ... }
258
+ </style>
259
+ </head>
260
+ <body>
261
+ <span class="tag-chip">TypeScript</span>
262
+ <span class="tag-chip">HTML</span>
263
+ <span class="tag-chip">CSS</span>
264
+ <span class="tag-chip">Bun</span>
265
+ <span class="tag-chip">tkeron</span>
266
+ <span class="tag-chip">zero deps</span>
267
+ </body>
268
+ ```
269
+
270
+ #### Global CSS — keep `.css` file, inline via a component
271
+
272
+ For truly shared CSS (resets, tokens, body typography) you can keep a real `.css` file in source (so the editor gives you CSS tooling). Do **not** import it with `<link>`. Inline it through a build-time component — a `.com.ts` is the natural fit because it can read files at build time:
273
+
274
+ ```typescript
275
+ // any-name.com.ts (e.g. global-styles.com.ts, site-css.com.ts — name is free,
276
+ // just respect the custom-element hyphen rule)
277
+ import { join } from "path";
278
+ const css = await Bun.file(join(__dirname, "main.css")).text();
279
+ com.innerHTML = `<style>${css}</style>`;
280
+ ```
281
+
282
+ ```html
283
+ <!-- any page -->
284
+ <head>
285
+ <global-styles></global-styles>
286
+ </head>
287
+ ```
288
+
289
+ The `.css` file can live wherever the user prefers (root of `websrc/`, next to the component, in a `styles/` folder — free choice). What matters is that the page references the **component**, not the `.css` file. Keep the global CSS **small**: it ships on every page. Anything component-specific belongs in that component's `.com.html`.
290
+
291
+ **Build output — `<link>` vs component.** Both source patterns compile, but the output is fundamentally different:
292
+
293
+ ```html
294
+ <!-- ❌ websrc/page.html uses <link rel="stylesheet" href="./styles.css"> -->
295
+ <!-- web/page.html output: -->
296
+ <link rel="stylesheet" href="./page.css" />
297
+ <!-- + a separate page.css file is emitted next to page.html -->
298
+ <!-- = 1 extra HTTP request, blocks first paint -->
299
+ ```
300
+
301
+ ```html
302
+ <!-- ✅ websrc/page.html uses <global-styles></global-styles> -->
303
+ <!-- web/page.html output: -->
304
+ <style>
305
+ /* full content of styles.css inlined — zero extra files, zero extra requests */
306
+ :root { --accent: #38bdf8; }
307
+ body { margin: 0; font-family: system-ui; }
308
+ </style>
309
+ <!-- no .css file emitted alongside page.html -->
310
+ ```
311
+
312
+ ---
313
+
314
+ ## ❌ Anti-Patterns — Do NOT
315
+
316
+ ### ❌ Building entire UI structure in browser JS (the main anti-pattern)
317
+
318
+ The most harmful anti-pattern: JS functions that build whole UI sections with `innerHTML` or `createElement`.
319
+
320
+ ```typescript
321
+ // ❌ BAD — "renderDashboard" builds the entire structure from JS
322
+ function renderDashboard(data: Stats) {
323
+ const el = document.getElementById("dashboard");
324
+ if (!el) return;
325
+ el.innerHTML = `
326
+ <div class="kpi-grid">
327
+ <div class="kpi-card"><h3>Critical</h3><span>${data.critical}</span></div>
328
+ </div>
329
+ <table>
330
+ <thead><tr><th>Product</th><th>Velocity</th></tr></thead>
331
+ <tbody id="product-rows"></tbody>
332
+ </table>
333
+ `;
334
+ }
335
+ // User sees a blank screen while the fetch runs
336
+ ```
337
+
338
+ ```typescript
339
+ // ✅ GOOD — structure already in HTML, JS only fills the values
340
+ function updateDashboard(data: Stats) {
341
+ const critical = document.getElementById("kpi-critical-val");
342
+ if (critical) critical.textContent = String(data.critical);
343
+
344
+ // Only the rows are dynamic — the table is already in the HTML
345
+ const tbody = document.getElementById("product-rows");
346
+ if (tbody) tbody.innerHTML = data.products.map(renderRow).join("");
347
+ }
348
+ ```
349
+
350
+ **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.
351
+
352
+ ### ❌ Referencing `.js` in HTML (most common build error)
353
+
354
+ ```html
355
+ <!-- ❌ BAD — causes "Bundle failed" / "Cannot resolve" -->
356
+ <script src="index.js"></script>
357
+
358
+ <!-- ✅ GOOD — tkeron compiles .ts → .js automatically -->
359
+ <script type="module" src="./index.ts"></script>
360
+ ```
361
+
362
+ ### ❌ Absolute paths for assets
363
+
364
+ Tkeron serves the output from a subdirectory. Absolute paths (`/assets/...`) point to the server root, not to the build subdirectory.
365
+
366
+ ```html
367
+ <!-- ❌ BAD — breaks if the site is not at root -->
368
+ <link rel="icon" href="/assets/favicon.ico" />
369
+ <img src="/assets/logo.webp" />
370
+
371
+ <!-- ✅ GOOD — relative, works on any host/subdirectory -->
372
+ <link rel="icon" href="./assets/favicon.ico" />
373
+ <img src="./assets/logo.webp" />
374
+ ```
375
+
376
+ Applies to **every `href`, `src` and `url()`** pointing to local assets in `websrc/`.
377
+
378
+ ### ❌ `<link rel="stylesheet">` in HTML
379
+
380
+ Generates an extra HTTP request, blocks first paint, and bypasses tkeron's per-component dedup and tree-shaking.
381
+
382
+ ```html
383
+ <!-- ❌ BAD — extra request, no dedup, no tree-shaking -->
384
+ <link rel="stylesheet" href="./styles.css" />
385
+
386
+ <!-- ✅ GOOD — inlined at build time via a component -->
387
+ <global-styles></global-styles>
388
+ ```
389
+
390
+ See "CSS via components" above for the `global-styles.com.ts` pattern.
391
+
392
+ ### ❌ Event listeners in `.com.ts`
393
+
394
+ ```typescript
395
+ // ❌ BAD — lost in the output, never runs in the browser
396
+ com.addEventListener("click", () => console.log("clicked"));
397
+
398
+ // ✅ GOOD — do it in a browser .ts
399
+ // index.ts
400
+ document.querySelector(".my-btn")!.addEventListener("click", () => {
401
+ console.log("clicked");
402
+ });
403
+ ```
404
+
405
+ ### ❌ Browser APIs at build time
406
+
407
+ ```typescript
408
+ // ❌ BAD — they don't exist in Bun
409
+ // In .pre.ts, .post.ts or .com.ts:
410
+ window.location.href = "/redirect";
411
+ localStorage.setItem("key", "value");
412
+ navigator.userAgent;
413
+ sessionStorage.getItem("token");
414
+
415
+ // ✅ GOOD — use them only in browser .ts
416
+ ```
417
+
418
+ ### ❌ Local state inside `.com.ts`
419
+
420
+ ```typescript
421
+ // ❌ BAD — local variables are discarded, no state between instances
422
+ let count = 0;
423
+ com.innerHTML = `<button>Count: ${count}</button>`;
424
+
425
+ // ✅ GOOD — state in browser .ts
426
+ let count = 0;
427
+ document.getElementById("btn")!.addEventListener("click", () => {
428
+ count++;
429
+ document.getElementById("count")!.textContent = String(count);
430
+ });
431
+ ```
432
+
433
+ ### ❌ Accessing `document` from `.com.ts`
434
+
435
+ ```typescript
436
+ // ❌ BAD — .com.ts only has access to `com`, not to `document`
437
+ const header = document.querySelector("header");
438
+
439
+ // ✅ GOOD — use .pre.ts or .post.ts for full document access
440
+ ```
441
+
442
+ ### ❌ Fetch without timeout at build time
443
+
444
+ ```typescript
445
+ // ❌ BAD — if the API stalls, the build hangs forever
446
+ const data = await fetch("https://slow-api.com/data");
447
+
448
+ // ✅ GOOD — always with AbortController
449
+ const controller = new AbortController();
450
+ setTimeout(() => controller.abort(), 5000);
451
+ try {
452
+ const res = await fetch(url, { signal: controller.signal });
453
+ return await res.json();
454
+ } catch {
455
+ return fallbackData;
456
+ }
457
+ ```
458
+
459
+ ### ❌ npm packages in the browser
460
+
461
+ ```typescript
462
+ // ❌ BAD — tkeron does NOT bundle npm packages for the browser
463
+ // index.ts (browser)
464
+ import _ from "lodash"; // does not work
465
+
466
+ // ✅ GOOD — alternatives:
467
+ // 1. CDN: <script src="https://cdn.jsdelivr.net/npm/lodash"></script>
468
+ // 2. Write vanilla JS
469
+ // 3. npm packages DO work in .pre.ts, .post.ts and .com.ts (build time)
470
+ ```
471
+
472
+ ### ❌ Silent errors
473
+
474
+ ```typescript
475
+ // ❌ BAD — the component disappears with no explanation
476
+ const data = com.getAttribute("data");
477
+ if (!data) {
478
+ com.innerHTML = "";
479
+ return;
480
+ }
481
+
482
+ // ✅ GOOD — fail fast with a clear message
483
+ const data = com.getAttribute("data");
484
+ if (!data) {
485
+ throw new Error('Attribute "data" is required for data-table component');
486
+ }
487
+ ```
488
+
489
+ ### ❌ Massive repeated inline styles
490
+
491
+ ```typescript
492
+ // ❌ BAD — if used 100 times, duplicates KB of CSS
493
+ com.innerHTML = `
494
+ <div style="padding: 1rem; margin: 2rem; background: linear-gradient(...);
495
+ border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
496
+ ${content}
497
+ </div>
498
+ `;
499
+
500
+ // ✅ GOOD — use CSS classes
501
+ com.innerHTML = `<div class="card">${content}</div>`;
502
+ ```
503
+
504
+ ---
505
+
506
+ ## Security
507
+
508
+ ### Escape dynamic content (XSS prevention)
509
+
510
+ ```typescript
511
+ const escapeHtml = (str: string): string =>
512
+ str
513
+ .replace(/&/g, "&amp;")
514
+ .replace(/</g, "&lt;")
515
+ .replace(/>/g, "&gt;")
516
+ .replace(/"/g, "&quot;")
517
+ .replace(/'/g, "&#039;");
518
+
519
+ const text = com.getAttribute("text") || "";
520
+ com.innerHTML = `<p>${escapeHtml(text)}</p>`;
521
+ ```
522
+
523
+ **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).
524
+
525
+ ### Validate URLs
526
+
527
+ ```typescript
528
+ const isSafeUrl = (url: string): boolean => {
529
+ try {
530
+ const parsed = new URL(url);
531
+ return ["http:", "https:"].includes(parsed.protocol);
532
+ } catch {
533
+ return false;
534
+ }
535
+ };
536
+
537
+ const url = com.getAttribute("href") || "#";
538
+ const safeUrl = isSafeUrl(url) ? url : "#";
539
+ com.innerHTML = `<a href="${safeUrl}">Link</a>`;
540
+ ```
541
+
542
+ ### Validate numeric ranges
543
+
544
+ ```typescript
545
+ const count = parseInt(com.getAttribute("count") || "5");
546
+ if (isNaN(count) || count < 0 || count > 100) {
547
+ throw new Error("Invalid count: must be 0-100");
548
+ }
549
+ ```
550
+
551
+ ---
552
+
553
+ ## Performance
554
+
555
+ ### Build time
556
+
557
+ - `Promise.all()` for parallel fetch
558
+ - Timeout on every external call
559
+ - Cache data when possible (write to `/tmp/`)
560
+ - Limit data volume (`?limit=10`)
561
+
562
+ ### Output size
563
+
564
+ - CSS classes instead of repeated inline `style="..."`
565
+ - Lean components (little HTML per component)
566
+ - CSS lives in `<style>` inside `.com.html` → auto-dedup + tree-shaking
567
+ - Global CSS via `<global-styles>` component (inlined), NEVER `<link rel="stylesheet">`
568
+ - CSS variables for colors/spacing
569
+
570
+ ---
571
+
572
+ ## AI Agent Checklist
573
+
574
+ When creating/editing a tkeron project:
575
+
576
+ 1. ✅ HTML scripts: always `.ts` with `type="module"`, NEVER `.js`
577
+ 2. ✅ Asset paths: always relative (`./`), never absolute (`/`)
578
+ 3. ✅ Components: ALWAYS with a hyphen, never a standard HTML tag
579
+ 4. ✅ Browser code → `.ts`. Build code → `.com.ts` / `.pre.ts` / `.post.ts`
580
+ 5. ✅ Build-time fetch → always with AbortController + timeout
581
+ 6. ✅ Escape dynamic content (`escapeHtml`)
582
+ 7. ✅ CSS via components (`<style>` in `.com.html`), never `<link rel="stylesheet">`
583
+ 8. ✅ **Maximum pre-render**: HTML structure in `.html` / components, JS only fills data
584
+ 9. ✅ npm packages only at build time, CDN for the browser
585
+ 10. ✅ `tk build` after every edit, then verify `web/`. NEVER `tk dev` synchronously in agent sessions
586
+ 11. ✅ `web/` always in `.gitignore` — never edit, never commit
587
+ 12. ✅ No silent errors — `throw` with a clear message instead