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.
@@ -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, "&amp;")
423
+ .replace(/</g, "&lt;")
424
+ .replace(/>/g, "&gt;")
425
+ .replace(/"/g, "&quot;")
426
+ .replace(/'/g, "&#039;");
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`.