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.
- package/README.md +17 -16
- package/bun.lock +13 -196
- package/changelog.md +55 -0
- package/examples/init_sample/websrc/about.html +19 -0
- package/examples/init_sample/websrc/components/content/about-intro.com.md +7 -0
- package/examples/init_sample/websrc/components/layout/hero-section.com.html +88 -0
- package/examples/init_sample/websrc/components/layout/site-footer.com.html +20 -0
- package/examples/init_sample/websrc/components/layout/site-header.com.html +44 -0
- package/examples/init_sample/websrc/components/ui/counter-button.com.html +48 -0
- package/examples/init_sample/websrc/components/ui/crypto-prices-card.com.html +52 -0
- package/examples/init_sample/websrc/components/ui/html-components-card.com.html +10 -0
- package/examples/init_sample/websrc/{info-card.com.html → components/ui/info-card.com.html} +10 -6
- package/examples/init_sample/websrc/components/ui/markdown-card.com.html +21 -0
- package/examples/init_sample/websrc/{pre-render-card.com.html → components/ui/pre-render-card.com.html} +6 -3
- package/examples/init_sample/websrc/components/ui/quote-card.com.html +32 -0
- package/examples/init_sample/websrc/components/ui/styles-injector.com.ts +5 -0
- package/examples/init_sample/websrc/components/ui/ts-components-card.com.html +11 -0
- package/examples/init_sample/websrc/components/ui/user-badge.com.ts +30 -0
- package/examples/init_sample/websrc/docs.html +33 -0
- package/examples/init_sample/websrc/index.html +10 -209
- package/examples/init_sample/websrc/index.post.ts +70 -0
- package/examples/init_sample/websrc/index.pre.ts +10 -61
- package/examples/init_sample/websrc/index.ts +9 -7
- package/examples/init_sample/websrc/styles/global-styles.com.ts +5 -0
- package/examples/init_sample/websrc/styles/main.css +112 -0
- package/examples/init_sample/websrc/utils/api-service.ts +93 -0
- package/examples/with_global_styles/websrc/bad.html +23 -0
- package/examples/with_global_styles/websrc/global-styles.com.ts +5 -0
- package/examples/with_global_styles/websrc/good.html +22 -0
- package/examples/with_global_styles/websrc/index.html +25 -0
- package/examples/with_global_styles/websrc/styles.css +24 -0
- package/examples/with_style_dedup/websrc/tag-chip.com.html +15 -0
- package/examples/with_style_dedup/websrc/tag-chip.com.ts +2 -18
- package/index.ts +15 -0
- package/package.json +9 -13
- package/skills/tkeron/SKILL.md +287 -0
- package/skills/tkeron-components/SKILL.md +785 -0
- package/skills/tkeron-organization/SKILL.md +231 -0
- package/skills/tkeron-patterns/SKILL.md +587 -0
- package/skills/tkeron-testing/SKILL.md +194 -0
- package/skills/tkeron-troubleshooting/SKILL.md +263 -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/examples/init_sample/websrc/api-service.ts +0 -98
- package/examples/init_sample/websrc/counter-card.com.html +0 -9
- package/examples/init_sample/websrc/favicon.ico +0 -0
- package/examples/init_sample/websrc/hero-section.com.html +0 -23
- package/examples/init_sample/websrc/html-components-card.com.html +0 -6
- package/examples/init_sample/websrc/ts-components-card.com.html +0 -6
- package/examples/init_sample/websrc/user-badge.com.ts +0 -17
- package/mcp-server.ts +0 -272
- package/src/cleanupOrphanedTempDirs.ts +0 -31
- package/src/promptUser.ts +0 -15
- package/src/setupSigintHandler.ts +0 -6
- /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, "&")
|
|
514
|
+
.replace(/</g, "<")
|
|
515
|
+
.replace(/>/g, ">")
|
|
516
|
+
.replace(/"/g, """)
|
|
517
|
+
.replace(/'/g, "'");
|
|
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
|