tkeron 5.2.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 +52 -3
- 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/examples/with_style_dedup/websrc/index.html +29 -0
- package/examples/with_style_dedup/websrc/tag-chip.com.ts +19 -0
- package/index.ts +15 -0
- package/package.json +10 -23
- 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 +7 -6
- package/src/deduplicateComStyles.ts +41 -0
- package/src/develop.ts +1 -2
- package/src/init.ts +1 -2
- package/src/processCom.ts +2 -2
- package/src/processComTs.ts +2 -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,785 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tkeron-components
|
|
3
|
+
description: "Deep dive into tkeron components and build-time scripts: .com.html, .com.ts, .com.md, .pre.ts, .post.ts — the com/document globals, attribute reading, resolution priority, nesting, the wrapper-disappears rule, and full-cycle examples. Use this skill when creating, editing, or debugging any tkeron component or pre/post-rendering script."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tkeron — Components and Pre/Post-rendering
|
|
7
|
+
|
|
8
|
+
## The 4 Component Types
|
|
9
|
+
|
|
10
|
+
### 1. `.com.html` — Static HTML Component
|
|
11
|
+
|
|
12
|
+
Plain markup. Inlined in place. No logic, no attributes, no variables.
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<!-- site-header.com.html -->
|
|
16
|
+
<header class="site-header">
|
|
17
|
+
<nav>
|
|
18
|
+
<a href="/">Home</a>
|
|
19
|
+
<a href="/about">About</a>
|
|
20
|
+
</nav>
|
|
21
|
+
</header>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<!-- In any .html -->
|
|
26
|
+
<site-header></site-header>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Output**: `<site-header>` is replaced with the content of the `.com.html`.
|
|
30
|
+
|
|
31
|
+
**Can have multiple root elements:**
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<!-- two-columns.com.html -->
|
|
35
|
+
<div class="col">Column 1</div>
|
|
36
|
+
<div class="col">Column 2</div>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Limitations**:
|
|
40
|
+
|
|
41
|
+
- ❌ Cannot read attributes (`{title}` does not work)
|
|
42
|
+
- ❌ No conditional logic
|
|
43
|
+
- ❌ No slots or Shadow DOM
|
|
44
|
+
- ❌ Cannot include itself (circular)
|
|
45
|
+
|
|
46
|
+
**When to use**: headers, footers, navs, static cards, SVG icons, fixed layouts.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### 2. `.com.ts` — Dynamic TypeScript Component
|
|
51
|
+
|
|
52
|
+
Runs in **Bun** during the build. Has the `com` variable (the custom element in the DOM). Only `com.innerHTML` is preserved in the output.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// user-badge.com.ts
|
|
56
|
+
const name = com.getAttribute("name") || "Guest";
|
|
57
|
+
const role = com.getAttribute("role") || "User";
|
|
58
|
+
|
|
59
|
+
com.innerHTML = `
|
|
60
|
+
<div class="badge">
|
|
61
|
+
<span>${name}</span>
|
|
62
|
+
<span>${role}</span>
|
|
63
|
+
</div>
|
|
64
|
+
`;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<user-badge name="Alice" role="Admin"></user-badge>
|
|
69
|
+
<user-badge name="Bob"></user-badge>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**The `com` variable** (HTMLElement):
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
com.getAttribute("attr") // Read an attribute
|
|
76
|
+
com.innerHTML = "..." // ONLY preserved effect → defines the output
|
|
77
|
+
com.tagName // Element name
|
|
78
|
+
com.querySelector(...) // Search inside (when innerHTML is already set)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Can import modules:**
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// product-card.com.ts
|
|
85
|
+
import { formatPrice } from "./utils/format.js";
|
|
86
|
+
|
|
87
|
+
const price = parseFloat(com.getAttribute("price") || "0");
|
|
88
|
+
com.innerHTML = `<p>${formatPrice(price)}</p>`;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Can fetch:**
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// weather-card.com.ts
|
|
95
|
+
const city = com.getAttribute("city") || "Madrid";
|
|
96
|
+
const res = await fetch(`https://api.weather.com?city=${city}`);
|
|
97
|
+
const data = await res.json();
|
|
98
|
+
com.innerHTML = `<div>${data.temp}°C in ${city}</div>`;
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Can use npm packages** (installed with `bun add`):
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// code-block.com.ts
|
|
105
|
+
import { codeToHtml } from "shiki";
|
|
106
|
+
const code = com.getAttribute("code") || "";
|
|
107
|
+
const html = await codeToHtml(code, {
|
|
108
|
+
lang: "typescript",
|
|
109
|
+
theme: "github-dark",
|
|
110
|
+
});
|
|
111
|
+
com.innerHTML = `<div class="code-block">${html}</div>`;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Full TypeScript**: interfaces, types, async/await, for/if, everything works.
|
|
115
|
+
|
|
116
|
+
**Limitations**:
|
|
117
|
+
|
|
118
|
+
- ❌ Event listeners are lost (only static HTML in output)
|
|
119
|
+
- ❌ No access to global `document` (only `com`)
|
|
120
|
+
- ❌ No `window`, `localStorage`, `navigator`
|
|
121
|
+
- ❌ No state between instances
|
|
122
|
+
- ❌ Local variables are discarded after the file runs
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### 3. `.com.md` — Markdown Component
|
|
127
|
+
|
|
128
|
+
Markdown (full GFM) → HTML. Inlined just like `.com.html`.
|
|
129
|
+
|
|
130
|
+
```markdown
|
|
131
|
+
<!-- hero-text.com.md -->
|
|
132
|
+
|
|
133
|
+
# Welcome
|
|
134
|
+
|
|
135
|
+
This is an **amazing site**.
|
|
136
|
+
|
|
137
|
+
- Fast
|
|
138
|
+
- Simple
|
|
139
|
+
- Powerful
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```html
|
|
143
|
+
<hero-text></hero-text>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Supports full GFM**: headings, bold, italic, strikethrough, lists, tables, blockquotes, code blocks, links, images.
|
|
147
|
+
|
|
148
|
+
**Can contain custom elements** inside the Markdown (processed on the next iteration):
|
|
149
|
+
|
|
150
|
+
```markdown
|
|
151
|
+
<!-- page-content.com.md -->
|
|
152
|
+
|
|
153
|
+
# Content
|
|
154
|
+
|
|
155
|
+
Introductory text.
|
|
156
|
+
|
|
157
|
+
<feature-list></feature-list>
|
|
158
|
+
|
|
159
|
+
Final text.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Limitations**:
|
|
163
|
+
|
|
164
|
+
- ❌ Cannot read attributes (static, like `.com.html`)
|
|
165
|
+
- ❌ No logic
|
|
166
|
+
|
|
167
|
+
**When to use**: text sections, FAQs, about, docs, rich content.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### 4. `.com.ts` + `.com.html` — Template + Logic
|
|
172
|
+
|
|
173
|
+
When BOTH `name.com.html` and `name.com.ts` exist for the same component name, the `.com.html` is loaded as `com.innerHTML` **before** the `.com.ts` runs. This separates structure from logic.
|
|
174
|
+
|
|
175
|
+
**Template** (`user-card.com.html`):
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<div class="card">
|
|
179
|
+
<h3 class="name"></h3>
|
|
180
|
+
<p class="role"></p>
|
|
181
|
+
</div>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Logic** (`user-card.com.ts`):
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const name = com.getAttribute("data-name") || "Unknown";
|
|
188
|
+
const role = com.getAttribute("data-role") || "N/A";
|
|
189
|
+
|
|
190
|
+
const nameEl = com.querySelector(".name");
|
|
191
|
+
const roleEl = com.querySelector(".role");
|
|
192
|
+
if (nameEl) nameEl.textContent = name;
|
|
193
|
+
if (roleEl) roleEl.textContent = `Role: ${role}`;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Usage:**
|
|
197
|
+
|
|
198
|
+
```html
|
|
199
|
+
<user-card data-name="Alice" data-role="Developer"></user-card>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The `.com.ts` can read, mutate, or completely replace the template.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Build-time Scripts
|
|
207
|
+
|
|
208
|
+
In addition to components, tkeron has two "page-level" build-time scripts that operate on the full DOM of a paired `.html`:
|
|
209
|
+
|
|
210
|
+
### `.pre.ts` — Pre-rendering (runs BEFORE components)
|
|
211
|
+
|
|
212
|
+
Runs in **Bun** at the **very start** of the build. Has `document` (the full DOM of the paired `.html`). Manipulates the whole document **before** any component is resolved.
|
|
213
|
+
|
|
214
|
+
**Automatic pairing**: `index.pre.ts` → `index.html`. `about.pre.ts` → `about.html`.
|
|
215
|
+
|
|
216
|
+
> **If the `.html` does not exist**, tkeron creates a base one automatically.
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// index.pre.ts
|
|
220
|
+
import { fetchPosts } from "./api-service.js";
|
|
221
|
+
|
|
222
|
+
const title = document.querySelector("title");
|
|
223
|
+
if (title) title.textContent = "My Site";
|
|
224
|
+
|
|
225
|
+
const head = document.querySelector("head");
|
|
226
|
+
if (head) {
|
|
227
|
+
const meta = document.createElement("meta");
|
|
228
|
+
meta.setAttribute("name", "description");
|
|
229
|
+
meta.setAttribute("content", "Site description");
|
|
230
|
+
head.appendChild(meta);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const posts = await fetchPosts();
|
|
234
|
+
const container = document.getElementById("posts");
|
|
235
|
+
if (container) {
|
|
236
|
+
container.innerHTML = posts
|
|
237
|
+
.map((p) => `<article><h2>${p.title}</h2></article>`)
|
|
238
|
+
.join("");
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Inject components dynamically** (they will be processed by the component loop afterwards):
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// dashboard.pre.ts
|
|
246
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
247
|
+
const body = document.querySelector("body");
|
|
248
|
+
if (body && isDev) {
|
|
249
|
+
const panel = document.createElement("debug-panel");
|
|
250
|
+
body.appendChild(panel);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Use cases**: SEO meta tags, dynamic `<title>`, fetch data at build time, conditionally insert custom elements that the component loop will then resolve.
|
|
255
|
+
|
|
256
|
+
### `.post.ts` — Post-processing (runs AFTER components)
|
|
257
|
+
|
|
258
|
+
Runs in **Bun** at the **very end** of the build, **after** all components (`.com.html`, `.com.ts`, `.com.md`) have been inlined. Has `document` (the full DOM of the paired `.html`, with components already resolved). Last chance to mutate the page before it is written to `web/`.
|
|
259
|
+
|
|
260
|
+
**Automatic pairing**: `index.post.ts` → `index.html`. `about.post.ts` → `about.html`.
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// index.post.ts
|
|
264
|
+
// At this point all <site-header>, <user-card>, etc. have been replaced
|
|
265
|
+
// with their final HTML. The DOM is ready for post-processing.
|
|
266
|
+
|
|
267
|
+
const links = document.querySelectorAll("a[href^='http']");
|
|
268
|
+
for (const link of links) {
|
|
269
|
+
link.setAttribute("rel", "noopener noreferrer");
|
|
270
|
+
link.setAttribute("target", "_blank");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const images = document.querySelectorAll("img:not([loading])");
|
|
274
|
+
for (const img of images) {
|
|
275
|
+
img.setAttribute("loading", "lazy");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const headings = document.querySelectorAll("h2, h3");
|
|
279
|
+
for (const h of headings) {
|
|
280
|
+
if (!h.id && h.textContent) {
|
|
281
|
+
h.setAttribute("id", h.textContent.toLowerCase().replace(/\s+/g, "-"));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Use cases**:
|
|
287
|
+
|
|
288
|
+
- Add `rel="noopener"` / `target="_blank"` to external links
|
|
289
|
+
- Add `loading="lazy"` to images that don't have it
|
|
290
|
+
- Auto-generate `id`s on headings for anchor links
|
|
291
|
+
- Build a Table of Contents from the resolved DOM
|
|
292
|
+
- Sanitize / rewrite attributes
|
|
293
|
+
- Final HTML transformations that depend on the **fully assembled** page
|
|
294
|
+
|
|
295
|
+
**Variables and limits**: identical to `.pre.ts` (`document`, full Bun runtime, no `window`/`localStorage`).
|
|
296
|
+
|
|
297
|
+
### `.pre.ts` vs `.post.ts` — When to use which
|
|
298
|
+
|
|
299
|
+
| Concern | `.pre.ts` | `.post.ts` |
|
|
300
|
+
| --------------------------------------------- | ------------------------------- | --------------------- |
|
|
301
|
+
| Inject custom elements that must be processed | ✅ (loop runs after) | ❌ (loop already ran) |
|
|
302
|
+
| Set `<title>` / `<meta>` | ✅ | ✅ |
|
|
303
|
+
| Fetch data and seed page content | ✅ | ✅ |
|
|
304
|
+
| Walk the final, fully-resolved DOM | ❌ | ✅ |
|
|
305
|
+
| Rewrite links/images in components | ❌ (components not yet inlined) | ✅ |
|
|
306
|
+
| Generate TOC from real headings | ❌ | ✅ |
|
|
307
|
+
|
|
308
|
+
**Rule of thumb**: if you need to insert custom elements → `.pre.ts`. If you need to read the final HTML of components → `.post.ts`.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Naming — Mandatory Rules
|
|
313
|
+
|
|
314
|
+
Every component **must have a hyphen** in its name (HTML custom-elements standard):
|
|
315
|
+
|
|
316
|
+
| File | Element | Valid? |
|
|
317
|
+
| ----------------------- | ------------------ | -------------------- |
|
|
318
|
+
| `user-card.com.html` | `<user-card>` | ✅ |
|
|
319
|
+
| `nav-menu.com.ts` | `<nav-menu>` | ✅ |
|
|
320
|
+
| `blog-post-card.com.ts` | `<blog-post-card>` | ✅ |
|
|
321
|
+
| `card.com.html` | `<card>` | ❌ no hyphen |
|
|
322
|
+
| `header.com.html` | `<header>` | ❌ standard HTML tag |
|
|
323
|
+
| `UserCard.com.html` | `<user-card>` | ✅ case-insensitive |
|
|
324
|
+
|
|
325
|
+
**Format**: kebab-case, lowercase with hyphens.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Component Resolution
|
|
330
|
+
|
|
331
|
+
Tkeron looks up components in this order:
|
|
332
|
+
|
|
333
|
+
1. **Same directory** as the file that uses it (high priority — local override)
|
|
334
|
+
2. **Any directory** under `websrc/` (glob search)
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
websrc/
|
|
338
|
+
├── index.html # <blog-card> → components/blog-card.com.html
|
|
339
|
+
├── components/
|
|
340
|
+
│ └── blog-card.com.html # Global component
|
|
341
|
+
└── blog/
|
|
342
|
+
├── post.html # <blog-card> → blog/blog-card.com.html (local wins)
|
|
343
|
+
└── blog-card.com.html # Local override for blog/
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Priority between types
|
|
347
|
+
|
|
348
|
+
When multiple file types exist for the same name:
|
|
349
|
+
|
|
350
|
+
1. **`.com.ts`** — highest priority
|
|
351
|
+
2. **`.com.html`** — second
|
|
352
|
+
3. **`.com.md`** — lowest
|
|
353
|
+
|
|
354
|
+
(Exception: if `.com.html` and `.com.ts` share a name, the HTML is loaded as `com.innerHTML` and the `.com.ts` runs on top of it — see "Template + Logic" above.)
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Iteration and Nesting
|
|
359
|
+
|
|
360
|
+
Tkeron processes components in an **iterative loop (max 10 cycles)** until no more changes are detected. This enables:
|
|
361
|
+
|
|
362
|
+
### Components that use other components
|
|
363
|
+
|
|
364
|
+
```html
|
|
365
|
+
<!-- user-card.com.html -->
|
|
366
|
+
<div class="user-card">
|
|
367
|
+
<user-avatar></user-avatar>
|
|
368
|
+
<div class="info">John Doe</div>
|
|
369
|
+
</div>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```html
|
|
373
|
+
<!-- user-avatar.com.html -->
|
|
374
|
+
<div class="avatar">
|
|
375
|
+
<img src="./avatars/default.jpg" alt="User" />
|
|
376
|
+
</div>
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Build: `<user-card>` is replaced → contains `<user-avatar>` → replaced on the next iteration.
|
|
380
|
+
|
|
381
|
+
### `.com.ts` that emits custom elements
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// user-profile.com.ts
|
|
385
|
+
const username = com.getAttribute("username") || "anon";
|
|
386
|
+
com.innerHTML = `
|
|
387
|
+
<div class="profile">
|
|
388
|
+
<user-avatar username="${username}"></user-avatar>
|
|
389
|
+
<span>${username}</span>
|
|
390
|
+
</div>
|
|
391
|
+
`;
|
|
392
|
+
// <user-avatar> will be processed on the next iteration
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### `.pre.ts` that injects components
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
// index.pre.ts
|
|
399
|
+
const body = document.querySelector("body");
|
|
400
|
+
if (body) {
|
|
401
|
+
const nav = document.createElement("site-nav");
|
|
402
|
+
body.insertBefore(nav, body.firstChild);
|
|
403
|
+
}
|
|
404
|
+
// <site-nav> will be processed by the component loop
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Limits
|
|
408
|
+
|
|
409
|
+
- **Max 10 iterations** of the component loop
|
|
410
|
+
- **Max 50 levels** of nesting depth
|
|
411
|
+
- **No circular dependencies** (A uses B uses A → error)
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Interactivity: Build Time + Runtime
|
|
416
|
+
|
|
417
|
+
Components emit static HTML. For interactivity, use a browser `.ts` file:
|
|
418
|
+
|
|
419
|
+
**Static component:**
|
|
420
|
+
|
|
421
|
+
```html
|
|
422
|
+
<!-- counter-btn.com.html -->
|
|
423
|
+
<button id="counter-btn">Clicks: <span id="count">0</span></button>
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Browser interactivity:**
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// index.ts (runtime, browser)
|
|
430
|
+
const btn = document.getElementById("counter-btn")!;
|
|
431
|
+
const count = document.getElementById("count")!;
|
|
432
|
+
let n = 0;
|
|
433
|
+
btn.addEventListener("click", () => {
|
|
434
|
+
n++;
|
|
435
|
+
count.textContent = String(n);
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Build in Action — Real Input and Output
|
|
442
|
+
|
|
443
|
+
### Example 1: `.com.html` — direct replacement
|
|
444
|
+
|
|
445
|
+
**Source `websrc/`:**
|
|
446
|
+
|
|
447
|
+
```
|
|
448
|
+
websrc/
|
|
449
|
+
├── index.html
|
|
450
|
+
└── site-header.com.html
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
```html
|
|
454
|
+
<!-- websrc/site-header.com.html -->
|
|
455
|
+
<header class="site-header">
|
|
456
|
+
<nav>
|
|
457
|
+
<a href="/">Home</a>
|
|
458
|
+
<a href="/about">About</a>
|
|
459
|
+
</nav>
|
|
460
|
+
</header>
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
```html
|
|
464
|
+
<!-- websrc/index.html (BEFORE build) -->
|
|
465
|
+
<!DOCTYPE html>
|
|
466
|
+
<html lang="en">
|
|
467
|
+
<head>
|
|
468
|
+
<title>My Site</title>
|
|
469
|
+
</head>
|
|
470
|
+
<body>
|
|
471
|
+
<site-header></site-header>
|
|
472
|
+
<main><p>Content</p></main>
|
|
473
|
+
<script type="module" src="index.ts"></script>
|
|
474
|
+
</body>
|
|
475
|
+
</html>
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**Output `web/` after `tk build`:**
|
|
479
|
+
|
|
480
|
+
```html
|
|
481
|
+
<!-- web/index.html (AFTER build) -->
|
|
482
|
+
<!DOCTYPE html>
|
|
483
|
+
<html lang="en">
|
|
484
|
+
<head>
|
|
485
|
+
<title>My Site</title>
|
|
486
|
+
</head>
|
|
487
|
+
<body>
|
|
488
|
+
<header class="site-header">
|
|
489
|
+
<nav>
|
|
490
|
+
<a href="/">Home</a>
|
|
491
|
+
<a href="/about">About</a>
|
|
492
|
+
</nav>
|
|
493
|
+
</header>
|
|
494
|
+
<main><p>Content</p></main>
|
|
495
|
+
<script type="module" src="index.js"></script>
|
|
496
|
+
</body>
|
|
497
|
+
</html>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Note: `<site-header>` was replaced with the component HTML. `index.ts` was compiled and the reference rewritten to `index.js`. `site-header.com.html` does not exist in `web/`.
|
|
501
|
+
|
|
502
|
+
### Example 2: `.com.ts` — dynamic component with attributes
|
|
503
|
+
|
|
504
|
+
**Source `websrc/`:**
|
|
505
|
+
|
|
506
|
+
```
|
|
507
|
+
websrc/
|
|
508
|
+
├── index.html
|
|
509
|
+
└── user-badge.com.ts
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
// websrc/user-badge.com.ts
|
|
514
|
+
const name = com.getAttribute("name") || "Guest";
|
|
515
|
+
const role = com.getAttribute("role") || "User";
|
|
516
|
+
|
|
517
|
+
com.innerHTML = `
|
|
518
|
+
<div class="badge">
|
|
519
|
+
<span class="badge-name">${name}</span>
|
|
520
|
+
<span class="badge-role">${role}</span>
|
|
521
|
+
</div>
|
|
522
|
+
`;
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
```html
|
|
526
|
+
<!-- websrc/index.html (BEFORE build) -->
|
|
527
|
+
<body>
|
|
528
|
+
<user-badge name="Alice" role="Admin"></user-badge>
|
|
529
|
+
<user-badge name="Bob"></user-badge>
|
|
530
|
+
<user-badge></user-badge>
|
|
531
|
+
</body>
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Output `web/index.html`:**
|
|
535
|
+
|
|
536
|
+
```html
|
|
537
|
+
<body>
|
|
538
|
+
<div class="badge">
|
|
539
|
+
<span class="badge-name">Alice</span>
|
|
540
|
+
<span class="badge-role">Admin</span>
|
|
541
|
+
</div>
|
|
542
|
+
<div class="badge">
|
|
543
|
+
<span class="badge-name">Bob</span>
|
|
544
|
+
<span class="badge-role">User</span>
|
|
545
|
+
</div>
|
|
546
|
+
<div class="badge">
|
|
547
|
+
<span class="badge-name">Guest</span>
|
|
548
|
+
<span class="badge-role">User</span>
|
|
549
|
+
</div>
|
|
550
|
+
</body>
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Each `<user-badge>` instance is replaced with the `com.innerHTML` generated for that specific instance (with its own attributes).
|
|
554
|
+
|
|
555
|
+
### Example 3: `.pre.ts` — document transformation
|
|
556
|
+
|
|
557
|
+
**Source `websrc/`:**
|
|
558
|
+
|
|
559
|
+
```
|
|
560
|
+
websrc/
|
|
561
|
+
├── index.html
|
|
562
|
+
└── index.pre.ts
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
```html
|
|
566
|
+
<!-- websrc/index.html (BEFORE build) -->
|
|
567
|
+
<!DOCTYPE html>
|
|
568
|
+
<html lang="en">
|
|
569
|
+
<head>
|
|
570
|
+
<title>Document</title>
|
|
571
|
+
</head>
|
|
572
|
+
<body>
|
|
573
|
+
<h1>Welcome</h1>
|
|
574
|
+
<p id="build-time"></p>
|
|
575
|
+
<ul id="posts"></ul>
|
|
576
|
+
</body>
|
|
577
|
+
</html>
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
// websrc/index.pre.ts
|
|
582
|
+
const title = document.querySelector("title");
|
|
583
|
+
if (title) title.textContent = "My Blog";
|
|
584
|
+
|
|
585
|
+
const head = document.querySelector("head");
|
|
586
|
+
if (head) {
|
|
587
|
+
const meta = document.createElement("meta");
|
|
588
|
+
meta.setAttribute("name", "description");
|
|
589
|
+
meta.setAttribute("content", "My personal site");
|
|
590
|
+
head.appendChild(meta);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const buildTime = document.getElementById("build-time");
|
|
594
|
+
if (buildTime) buildTime.textContent = "Build: 2026-03-03";
|
|
595
|
+
|
|
596
|
+
const posts = document.getElementById("posts");
|
|
597
|
+
if (posts) {
|
|
598
|
+
posts.innerHTML = `
|
|
599
|
+
<li><a href="/post-1">Post 1</a></li>
|
|
600
|
+
<li><a href="/post-2">Post 2</a></li>
|
|
601
|
+
`;
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
**Output `web/index.html`:**
|
|
606
|
+
|
|
607
|
+
```html
|
|
608
|
+
<!DOCTYPE html>
|
|
609
|
+
<html lang="en">
|
|
610
|
+
<head>
|
|
611
|
+
<title>My Blog</title>
|
|
612
|
+
<meta name="description" content="My personal site" />
|
|
613
|
+
</head>
|
|
614
|
+
<body>
|
|
615
|
+
<h1>Welcome</h1>
|
|
616
|
+
<p id="build-time">Build: 2026-03-03</p>
|
|
617
|
+
<ul id="posts">
|
|
618
|
+
<li><a href="/post-1">Post 1</a></li>
|
|
619
|
+
<li><a href="/post-2">Post 2</a></li>
|
|
620
|
+
</ul>
|
|
621
|
+
</body>
|
|
622
|
+
</html>
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
The `.pre.ts` file does not appear in `web/`. The HTML was mutated in memory during the build.
|
|
626
|
+
|
|
627
|
+
### Example 4: `.post.ts` — final DOM walk
|
|
628
|
+
|
|
629
|
+
**Source `websrc/`:**
|
|
630
|
+
|
|
631
|
+
```
|
|
632
|
+
websrc/
|
|
633
|
+
├── index.html
|
|
634
|
+
├── external-link.com.html
|
|
635
|
+
└── index.post.ts
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
```html
|
|
639
|
+
<!-- websrc/external-link.com.html -->
|
|
640
|
+
<a href="https://example.com">Example</a>
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
```html
|
|
644
|
+
<!-- websrc/index.html (BEFORE build) -->
|
|
645
|
+
<body>
|
|
646
|
+
<h2>Resources</h2>
|
|
647
|
+
<external-link></external-link>
|
|
648
|
+
<img src="./photo.jpg" alt="photo" />
|
|
649
|
+
</body>
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
// websrc/index.post.ts
|
|
654
|
+
// Runs AFTER <external-link> has already been replaced
|
|
655
|
+
// by its <a href="https://example.com">...</a>
|
|
656
|
+
|
|
657
|
+
const links = document.querySelectorAll("a[href^='http']");
|
|
658
|
+
for (const link of links) {
|
|
659
|
+
link.setAttribute("rel", "noopener noreferrer");
|
|
660
|
+
link.setAttribute("target", "_blank");
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const images = document.querySelectorAll("img:not([loading])");
|
|
664
|
+
for (const img of images) {
|
|
665
|
+
img.setAttribute("loading", "lazy");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const headings = document.querySelectorAll("h2");
|
|
669
|
+
for (const h of headings) {
|
|
670
|
+
if (!h.id && h.textContent) {
|
|
671
|
+
h.setAttribute("id", h.textContent.toLowerCase().replace(/\s+/g, "-"));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
**Output `web/index.html`:**
|
|
677
|
+
|
|
678
|
+
```html
|
|
679
|
+
<body>
|
|
680
|
+
<h2 id="resources">Resources</h2>
|
|
681
|
+
<a href="https://example.com" rel="noopener noreferrer" target="_blank">
|
|
682
|
+
Example
|
|
683
|
+
</a>
|
|
684
|
+
<img src="./photo.jpg" alt="photo" loading="lazy" />
|
|
685
|
+
</body>
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
The `<external-link>` was inlined first, then `.post.ts` walked the resolved DOM and added `rel`, `target`, `loading` and `id` attributes.
|
|
689
|
+
|
|
690
|
+
### Example 5: Full cycle (`.pre.ts` + components + `.post.ts`)
|
|
691
|
+
|
|
692
|
+
**Source:**
|
|
693
|
+
|
|
694
|
+
```
|
|
695
|
+
websrc/
|
|
696
|
+
├── index.html
|
|
697
|
+
├── index.pre.ts
|
|
698
|
+
├── index.post.ts
|
|
699
|
+
├── site-footer.com.html
|
|
700
|
+
└── alert-box.com.ts
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
```html
|
|
704
|
+
<!-- websrc/site-footer.com.html -->
|
|
705
|
+
<footer><p>© 2026 My Site</p></footer>
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
// websrc/alert-box.com.ts
|
|
710
|
+
const type = com.getAttribute("type") || "info";
|
|
711
|
+
const msg = com.getAttribute("msg") || "";
|
|
712
|
+
com.innerHTML = `<div class="alert alert-${type}">${msg}</div>`;
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
```html
|
|
716
|
+
<!-- websrc/index.html -->
|
|
717
|
+
<!DOCTYPE html>
|
|
718
|
+
<html>
|
|
719
|
+
<head>
|
|
720
|
+
<title>App</title>
|
|
721
|
+
</head>
|
|
722
|
+
<body>
|
|
723
|
+
<alert-box type="success" msg="Welcome"></alert-box>
|
|
724
|
+
<main><p>Content</p></main>
|
|
725
|
+
<site-footer></site-footer>
|
|
726
|
+
</body>
|
|
727
|
+
</html>
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
```typescript
|
|
731
|
+
// websrc/index.pre.ts
|
|
732
|
+
const title = document.querySelector("title");
|
|
733
|
+
if (title) title.textContent = "App — Home";
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
// websrc/index.post.ts
|
|
738
|
+
const main = document.querySelector("main");
|
|
739
|
+
if (main) main.setAttribute("data-section", "home");
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**Output `web/index.html`:**
|
|
743
|
+
|
|
744
|
+
```html
|
|
745
|
+
<!DOCTYPE html>
|
|
746
|
+
<html>
|
|
747
|
+
<head>
|
|
748
|
+
<title>App — Home</title>
|
|
749
|
+
</head>
|
|
750
|
+
<body>
|
|
751
|
+
<div class="alert alert-success">Welcome</div>
|
|
752
|
+
<main data-section="home"><p>Content</p></main>
|
|
753
|
+
<footer><p>© 2026 My Site</p></footer>
|
|
754
|
+
</body>
|
|
755
|
+
</html>
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
**`web/` contains only:**
|
|
759
|
+
|
|
760
|
+
```
|
|
761
|
+
web/
|
|
762
|
+
└── index.html
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
**Files that disappear from the output**: `index.pre.ts`, `index.post.ts`, `alert-box.com.ts`, `site-footer.com.html`.
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
## When to Use Each Type
|
|
770
|
+
|
|
771
|
+
| I need to... | Use... |
|
|
772
|
+
| ----------------------------------------------------- | ----------------------- |
|
|
773
|
+
| Reusable markup with no logic | `.com.html` |
|
|
774
|
+
| Dynamic HTML driven by attributes | `.com.ts` |
|
|
775
|
+
| Formatted text in Markdown | `.com.md` |
|
|
776
|
+
| Mutate the document BEFORE components run | `.pre.ts` |
|
|
777
|
+
| Mutate the document AFTER components are resolved | `.post.ts` |
|
|
778
|
+
| SEO meta tags, page title | `.pre.ts` or `.post.ts` |
|
|
779
|
+
| Fetch API at build time and seed content | `.pre.ts` or `.com.ts` |
|
|
780
|
+
| Inject custom elements conditionally | `.pre.ts` |
|
|
781
|
+
| Add `rel="noopener"` / `loading="lazy"` to final HTML | `.post.ts` |
|
|
782
|
+
| Auto-generate heading ids / table of contents | `.post.ts` |
|
|
783
|
+
| Separate template from logic | `.com.html` + `.com.ts` |
|
|
784
|
+
| Interactivity, events, user input | `.ts` (browser) |
|
|
785
|
+
| State, localStorage, animations | `.ts` (browser) |
|