tkeron 4.0.0-beta.9 → 4.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 → README.md} +36 -4
- package/bun.lock +23 -21
- package/changelog.md +274 -8
- package/docs/best-practices.md +127 -101
- package/docs/cli-reference.md +58 -38
- package/docs/components-html.md +30 -24
- package/docs/components-typescript.md +110 -79
- package/docs/getting-started.md +24 -18
- package/docs/mcp-server.md +19 -36
- package/docs/overview.md +16 -10
- package/docs/pre-rendering.md +163 -157
- package/docs/testing.md +331 -0
- package/examples/basic_build/src/index.ts +5 -5
- package/examples/with_assets/src/index.ts +5 -5
- package/examples/with_com_html_priority/src/admin/admin-panel.com.html +1 -1
- package/examples/with_com_html_priority/src/admin/dashboard.html +16 -16
- package/examples/with_com_html_priority/src/admin/nested-stat.com.html +1 -1
- package/examples/with_com_html_priority/src/admin/site-header.com.html +1 -1
- package/examples/with_com_html_priority/src/blog/comment-item.com.html +1 -1
- package/examples/with_com_html_priority/src/blog/comment-section.com.html +1 -1
- package/examples/with_com_html_priority/src/blog/post.html +22 -19
- package/examples/with_com_html_priority/src/index.html +15 -15
- package/examples/with_com_html_priority/src/info-box.com.html +1 -1
- package/examples/with_com_html_priority/src/site-header.com.html +1 -1
- package/examples/with_com_mixed_priority/src/dashboard/main.html +17 -17
- package/examples/with_com_mixed_priority/src/dashboard/stats-widget.com.ts +1 -1
- package/examples/with_com_mixed_priority/src/footer-info.com.html +3 -1
- package/examples/with_com_mixed_priority/src/index.html +16 -16
- package/examples/with_com_mixed_priority/src/page-header.com.html +8 -1
- package/examples/with_com_mixed_priority/src/settings/config.html +16 -16
- package/examples/with_com_mixed_priority/src/settings/priority-test.com.html +3 -1
- package/examples/with_com_mixed_priority/src/users/profiles/activity-feed.com.ts +7 -3
- package/examples/with_com_mixed_priority/src/users/profiles/page-header.com.html +8 -1
- package/examples/with_com_mixed_priority/src/users/profiles/profile-actions.com.html +45 -4
- package/examples/with_com_mixed_priority/src/users/profiles/user-profile.com.ts +1 -1
- package/examples/with_com_mixed_priority/src/users/profiles/view.html +17 -17
- package/examples/with_com_ts/src/index.html +15 -11
- package/examples/with_com_ts_priority/src/analytics/chart-widget.com.ts +7 -5
- package/examples/with_com_ts_priority/src/analytics/report.html +16 -16
- package/examples/with_com_ts_priority/src/analytics/user-stats.com.ts +1 -1
- package/examples/with_com_ts_priority/src/data-table.com.ts +7 -4
- package/examples/with_com_ts_priority/src/index.html +15 -15
- package/examples/with_com_ts_priority/src/sales/regional/data-table.com.ts +8 -5
- package/examples/with_com_ts_priority/src/sales/regional/overview.html +16 -16
- package/examples/with_com_ts_priority/src/sales/regional/sales-summary.com.ts +7 -4
- package/examples/with_com_ts_priority/src/user-stats.com.ts +1 -1
- package/examples/with_component_iteration/src/card-list.com.ts +40 -12
- package/examples/with_component_iteration/src/engagement-meter.com.ts +11 -6
- package/examples/with_component_iteration/src/index.html +108 -51
- package/examples/with_component_iteration/src/index.ts +1 -1
- package/examples/with_component_iteration/src/note-box.com.html +12 -2
- package/examples/with_component_iteration/src/status-badge.com.ts +14 -7
- package/examples/with_component_iteration/src/user-card.com.ts +10 -9
- package/examples/with_pre/src/contact.pre.ts +3 -3
- package/examples/with_pre/src/index.ts +5 -5
- package/examples/with_pre/src/section/index.pre.ts +2 -4
- package/index.ts +31 -40
- package/mcp-server.ts +168 -445
- package/package.json +19 -8
- package/src/banner.ts +5 -4
- package/src/build.ts +29 -29
- package/src/buildDir.ts +36 -18
- package/src/buildEntrypoints.ts +17 -6
- package/src/buildWrapper.ts +21 -0
- package/src/cleanupOrphanedTempDirs.ts +31 -0
- package/src/createTestLogger.ts +24 -0
- package/src/develop.ts +47 -46
- package/src/getBuildResult.ts +146 -0
- package/src/index.ts +7 -0
- package/src/init.ts +39 -42
- package/src/initWrapper.ts +39 -26
- package/src/logger.ts +4 -11
- package/src/loggerObj.ts +13 -0
- package/src/processCom.ts +136 -108
- package/src/processComTs.ts +173 -151
- package/src/processPre.ts +24 -27
- package/src/promptUser.ts +15 -0
- package/src/setupSigintHandler.ts +6 -0
- package/src/silentLogger.ts +8 -0
- package/tests/test-helpers.ts +30 -0
- package/funciones.md +0 -63
package/docs/best-practices.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
## What Tkeron Does
|
|
4
4
|
|
|
5
5
|
**Build time:**
|
|
6
|
+
|
|
6
7
|
- Runs `.pre.ts` - full DOM access, TypeScript, Bun APIs
|
|
7
8
|
- Executes `.com.ts` - reads attributes, generates HTML
|
|
8
9
|
- Inlines `.com.html` - static replacement
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
- Copies assets
|
|
11
12
|
|
|
12
13
|
**Runtime (browser):**
|
|
14
|
+
|
|
13
15
|
- Your compiled JavaScript runs
|
|
14
16
|
- Zero framework overhead
|
|
15
17
|
- Use any browser APIs
|
|
@@ -36,27 +38,28 @@ Dynamic generation with attributes:
|
|
|
36
38
|
|
|
37
39
|
```typescript
|
|
38
40
|
// user-badge.com.ts
|
|
39
|
-
const count = com.getAttribute(
|
|
41
|
+
const count = com.getAttribute("count") || "3";
|
|
40
42
|
const items = [];
|
|
41
43
|
for (let i = 1; i <= parseInt(count); i++) {
|
|
42
44
|
items.push(`<li>Item ${i}</li>`);
|
|
43
45
|
}
|
|
44
|
-
com.innerHTML = `<ul>${items.join(
|
|
46
|
+
com.innerHTML = `<ul>${items.join("")}</ul>`;
|
|
45
47
|
```
|
|
46
48
|
|
|
47
49
|
Use for: Dynamic lists, conditional rendering, computed HTML.
|
|
48
50
|
|
|
49
51
|
**Always validate and escape:**
|
|
52
|
+
|
|
50
53
|
```typescript
|
|
51
54
|
function escapeHtml(str: string): string {
|
|
52
55
|
return str
|
|
53
|
-
.replace(/&/g,
|
|
54
|
-
.replace(/</g,
|
|
55
|
-
.replace(/>/g,
|
|
56
|
-
.replace(/"/g,
|
|
56
|
+
.replace(/&/g, "&")
|
|
57
|
+
.replace(/</g, "<")
|
|
58
|
+
.replace(/>/g, ">")
|
|
59
|
+
.replace(/"/g, """);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
const text = com.getAttribute(
|
|
62
|
+
const text = com.getAttribute("text") || "";
|
|
60
63
|
com.innerHTML = `<p>${escapeHtml(text)}</p>`;
|
|
61
64
|
```
|
|
62
65
|
|
|
@@ -66,14 +69,15 @@ Build-time DOM manipulation:
|
|
|
66
69
|
|
|
67
70
|
```typescript
|
|
68
71
|
// index.pre.ts
|
|
69
|
-
const res = await fetch(
|
|
72
|
+
const res = await fetch("https://api.quotable.io/random");
|
|
70
73
|
const data = await res.json();
|
|
71
|
-
document.getElementById(
|
|
74
|
+
document.getElementById("quote").textContent = data.content;
|
|
72
75
|
```
|
|
73
76
|
|
|
74
77
|
Use for: API data fetching, SEO meta tags, build metadata.
|
|
75
78
|
|
|
76
79
|
**Handle errors:**
|
|
80
|
+
|
|
77
81
|
```typescript
|
|
78
82
|
const controller = new AbortController();
|
|
79
83
|
setTimeout(() => controller.abort(), 5000);
|
|
@@ -114,20 +118,22 @@ Components run at build time. For runtime interactivity, use your `.ts` files:
|
|
|
114
118
|
|
|
115
119
|
```typescript
|
|
116
120
|
// index.ts - runs in browser
|
|
117
|
-
document.getElementById(
|
|
118
|
-
console.log(
|
|
121
|
+
document.getElementById("btn").addEventListener("click", () => {
|
|
122
|
+
console.log("clicked");
|
|
119
123
|
});
|
|
120
124
|
```
|
|
121
125
|
|
|
122
126
|
## Build vs Runtime
|
|
123
127
|
|
|
124
128
|
**Build time (.pre.ts, .com.ts):**
|
|
129
|
+
|
|
125
130
|
- Runs in Bun
|
|
126
131
|
- Has `fetch()`, `Bun.file()`, Node APIs
|
|
127
132
|
- Can import npm packages
|
|
128
133
|
- No `window`, `localStorage`, etc.
|
|
129
134
|
|
|
130
135
|
**Runtime (browser):**
|
|
136
|
+
|
|
131
137
|
- Your compiled `.js` files
|
|
132
138
|
- Standard browser environment
|
|
133
139
|
- All browser APIs available
|
|
@@ -137,18 +143,21 @@ document.getElementById('btn').addEventListener('click', () => {
|
|
|
137
143
|
Tkeron does NOT bundle npm packages for browser.
|
|
138
144
|
|
|
139
145
|
**Build time:**
|
|
146
|
+
|
|
140
147
|
```typescript
|
|
141
148
|
// .pre.ts - works
|
|
142
|
-
import _ from
|
|
149
|
+
import _ from "lodash";
|
|
143
150
|
```
|
|
144
151
|
|
|
145
152
|
**Browser:**
|
|
153
|
+
|
|
146
154
|
```typescript
|
|
147
155
|
// index.ts - won't work without bundler
|
|
148
|
-
import _ from
|
|
156
|
+
import _ from "lodash";
|
|
149
157
|
```
|
|
150
158
|
|
|
151
159
|
**Solutions:**
|
|
160
|
+
|
|
152
161
|
- Use CDN: `<script src="https://cdn.jsdelivr.net/npm/lodash"></script>`
|
|
153
162
|
- Use a bundler alongside (Vite, esbuild)
|
|
154
163
|
- Write vanilla JavaScript
|
|
@@ -164,11 +173,13 @@ tsc --noEmit && tk build
|
|
|
164
173
|
## Performance
|
|
165
174
|
|
|
166
175
|
**Build time:**
|
|
176
|
+
|
|
167
177
|
- Cache external data when possible
|
|
168
178
|
- Use timeouts for API calls
|
|
169
179
|
- Parallel requests with `Promise.all()`
|
|
170
180
|
|
|
171
181
|
**Output size:**
|
|
182
|
+
|
|
172
183
|
- Keep components lean
|
|
173
184
|
- Use CSS classes, not inline styles
|
|
174
185
|
- Share styles via external CSS files
|
|
@@ -184,6 +195,7 @@ tk build # Build for production
|
|
|
184
195
|
## Common Patterns
|
|
185
196
|
|
|
186
197
|
**Client-side counter:**
|
|
198
|
+
|
|
187
199
|
```html
|
|
188
200
|
<!-- counter.com.html -->
|
|
189
201
|
<button id="btn">Count: <span id="count">0</span></button>
|
|
@@ -192,25 +204,27 @@ tk build # Build for production
|
|
|
192
204
|
```typescript
|
|
193
205
|
// index.ts
|
|
194
206
|
let count = 0;
|
|
195
|
-
document.getElementById(
|
|
207
|
+
document.getElementById("btn").addEventListener("click", () => {
|
|
196
208
|
count++;
|
|
197
|
-
document.getElementById(
|
|
209
|
+
document.getElementById("count").textContent = count.toString();
|
|
198
210
|
});
|
|
199
211
|
```
|
|
200
212
|
|
|
201
213
|
**Dynamic list component:**
|
|
214
|
+
|
|
202
215
|
```typescript
|
|
203
216
|
// list.com.ts
|
|
204
|
-
const items = com.getAttribute(
|
|
205
|
-
com.innerHTML = `<ul>${items.map(i => `<li>${i}</li>`).join(
|
|
217
|
+
const items = com.getAttribute("items")?.split(",") || [];
|
|
218
|
+
com.innerHTML = `<ul>${items.map((i) => `<li>${i}</li>`).join("")}</ul>`;
|
|
206
219
|
```
|
|
207
220
|
|
|
208
221
|
**Pre-render API data:**
|
|
222
|
+
|
|
209
223
|
```typescript
|
|
210
224
|
// index.pre.ts
|
|
211
|
-
const res = await fetch(
|
|
225
|
+
const res = await fetch("https://api.github.com/users/tkeron");
|
|
212
226
|
const data = await res.json();
|
|
213
|
-
document.getElementById(
|
|
227
|
+
document.getElementById("stars").textContent = data.public_repos;
|
|
214
228
|
```
|
|
215
229
|
|
|
216
230
|
## Summary
|
|
@@ -223,30 +237,31 @@ document.getElementById('stars').textContent = data.public_repos;
|
|
|
223
237
|
- No type checking (use `tsc --noEmit`)
|
|
224
238
|
- Powered by Bun
|
|
225
239
|
document.querySelectorAll('.my-button').forEach(btn => {
|
|
226
|
-
|
|
240
|
+
btn.addEventListener('click', handleClick);
|
|
227
241
|
});
|
|
228
|
-
});
|
|
229
|
-
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
````
|
|
230
245
|
|
|
231
246
|
```typescript
|
|
232
247
|
// ❌ Don't try to maintain state
|
|
233
248
|
let count = 0; // This is meaningless
|
|
234
249
|
com.innerHTML = `<button>Count: ${count}</button>`;
|
|
235
|
-
|
|
250
|
+
````
|
|
236
251
|
|
|
237
252
|
**Solution:** Use client-side JavaScript for stateful UI.
|
|
238
253
|
|
|
239
254
|
```typescript
|
|
240
255
|
// ❌ Don't access DOM outside 'com'
|
|
241
|
-
const header = document.querySelector(
|
|
242
|
-
com.innerHTML = header?.innerHTML ||
|
|
256
|
+
const header = document.querySelector("header");
|
|
257
|
+
com.innerHTML = header?.innerHTML || ""; // Won't work
|
|
243
258
|
```
|
|
244
259
|
|
|
245
260
|
**Solution:** Use pre-rendering for document-wide access.
|
|
246
261
|
|
|
247
262
|
```typescript
|
|
248
263
|
// ❌ Don't use relative imports that won't resolve
|
|
249
|
-
import { helper } from
|
|
264
|
+
import { helper } from "../../../utils"; // May break
|
|
250
265
|
```
|
|
251
266
|
|
|
252
267
|
**Solution:** Use absolute paths or ensure the temp build directory preserves import paths.
|
|
@@ -261,10 +276,10 @@ import { helper } from '../../../utils'; // May break
|
|
|
261
276
|
// ✅ Fetch data at build time
|
|
262
277
|
async function fetchPosts() {
|
|
263
278
|
try {
|
|
264
|
-
const res = await fetch(
|
|
279
|
+
const res = await fetch("https://api.example.com/posts");
|
|
265
280
|
return await res.json();
|
|
266
281
|
} catch (error) {
|
|
267
|
-
console.error(
|
|
282
|
+
console.error("Failed to fetch:", error);
|
|
268
283
|
return [];
|
|
269
284
|
}
|
|
270
285
|
}
|
|
@@ -274,7 +289,7 @@ const posts = await fetchPosts();
|
|
|
274
289
|
|
|
275
290
|
```typescript
|
|
276
291
|
// ✅ Inject build metadata
|
|
277
|
-
const buildTime = document.getElementById(
|
|
292
|
+
const buildTime = document.getElementById("build-time");
|
|
278
293
|
if (buildTime) {
|
|
279
294
|
buildTime.textContent = new Date().toISOString();
|
|
280
295
|
}
|
|
@@ -282,23 +297,23 @@ if (buildTime) {
|
|
|
282
297
|
|
|
283
298
|
```typescript
|
|
284
299
|
// ✅ Conditional content based on environment
|
|
285
|
-
const isProd = process.env.NODE_ENV ===
|
|
300
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
286
301
|
|
|
287
302
|
if (isProd) {
|
|
288
303
|
// Add analytics
|
|
289
|
-
const script = document.createElement(
|
|
290
|
-
script.src =
|
|
304
|
+
const script = document.createElement("script");
|
|
305
|
+
script.src = "https://analytics.example.com/script.js";
|
|
291
306
|
document.body?.appendChild(script);
|
|
292
307
|
}
|
|
293
308
|
```
|
|
294
309
|
|
|
295
310
|
```typescript
|
|
296
311
|
// ✅ Generate SEO meta tags
|
|
297
|
-
const head = document.querySelector(
|
|
312
|
+
const head = document.querySelector("head");
|
|
298
313
|
if (head) {
|
|
299
|
-
const meta = document.createElement(
|
|
300
|
-
meta.setAttribute(
|
|
301
|
-
meta.setAttribute(
|
|
314
|
+
const meta = document.createElement("meta");
|
|
315
|
+
meta.setAttribute("property", "og:title");
|
|
316
|
+
meta.setAttribute("content", "My Page Title");
|
|
302
317
|
head.appendChild(meta);
|
|
303
318
|
}
|
|
304
319
|
```
|
|
@@ -307,8 +322,8 @@ if (head) {
|
|
|
307
322
|
|
|
308
323
|
```typescript
|
|
309
324
|
// ❌ Don't use browser APIs
|
|
310
|
-
window.location.href =
|
|
311
|
-
localStorage.setItem(
|
|
325
|
+
window.location.href = "/redirect"; // No 'window'
|
|
326
|
+
localStorage.setItem("key", "value"); // No localStorage
|
|
312
327
|
navigator.userAgent; // No navigator
|
|
313
328
|
```
|
|
314
329
|
|
|
@@ -316,7 +331,7 @@ navigator.userAgent; // No navigator
|
|
|
316
331
|
|
|
317
332
|
```typescript
|
|
318
333
|
// ❌ Don't try to read runtime user data
|
|
319
|
-
const username = document.querySelector(
|
|
334
|
+
const username = document.querySelector("#username-input")?.value;
|
|
320
335
|
// This is build-time, no user input exists yet
|
|
321
336
|
```
|
|
322
337
|
|
|
@@ -324,7 +339,7 @@ const username = document.querySelector('#username-input')?.value;
|
|
|
324
339
|
|
|
325
340
|
```typescript
|
|
326
341
|
// ❌ Don't make blocking requests without timeout
|
|
327
|
-
const data = await fetch(
|
|
342
|
+
const data = await fetch("https://slow-api.com/data");
|
|
328
343
|
// If API is down, build hangs forever
|
|
329
344
|
```
|
|
330
345
|
|
|
@@ -335,8 +350,8 @@ const controller = new AbortController();
|
|
|
335
350
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
336
351
|
|
|
337
352
|
try {
|
|
338
|
-
const data = await fetch(
|
|
339
|
-
signal: controller.signal
|
|
353
|
+
const data = await fetch("https://api.com/data", {
|
|
354
|
+
signal: controller.signal,
|
|
340
355
|
});
|
|
341
356
|
return await data.json();
|
|
342
357
|
} catch (error) {
|
|
@@ -390,6 +405,7 @@ websrc/
|
|
|
390
405
|
```
|
|
391
406
|
|
|
392
407
|
**Benefits:**
|
|
408
|
+
|
|
393
409
|
- Components are organized by purpose
|
|
394
410
|
- Related files are grouped together
|
|
395
411
|
- Easy to find and maintain
|
|
@@ -408,6 +424,7 @@ websrc/
|
|
|
408
424
|
```
|
|
409
425
|
|
|
410
426
|
**Problems:**
|
|
427
|
+
|
|
411
428
|
- No organization
|
|
412
429
|
- Unclear purpose
|
|
413
430
|
- Hard to maintain
|
|
@@ -429,6 +446,7 @@ social-share-button.com.ts → <social-share-button>
|
|
|
429
446
|
```
|
|
430
447
|
|
|
431
448
|
**Rules:**
|
|
449
|
+
|
|
432
450
|
- Lowercase with hyphens (kebab-case)
|
|
433
451
|
- Descriptive and specific
|
|
434
452
|
- Follows custom element naming (must have hyphen)
|
|
@@ -475,43 +493,46 @@ test123.html → Meaningless
|
|
|
475
493
|
|
|
476
494
|
```typescript
|
|
477
495
|
// ✅ Cache external data
|
|
478
|
-
import { readFileSync, writeFileSync, existsSync } from
|
|
496
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
479
497
|
|
|
480
|
-
const cacheFile =
|
|
498
|
+
const cacheFile = "/tmp/api-cache.json";
|
|
481
499
|
const cacheMaxAge = 3600000; // 1 hour
|
|
482
500
|
|
|
483
501
|
async function getCachedData() {
|
|
484
502
|
if (existsSync(cacheFile)) {
|
|
485
|
-
const cache = JSON.parse(readFileSync(cacheFile,
|
|
503
|
+
const cache = JSON.parse(readFileSync(cacheFile, "utf-8"));
|
|
486
504
|
if (Date.now() - cache.timestamp < cacheMaxAge) {
|
|
487
505
|
return cache.data;
|
|
488
506
|
}
|
|
489
507
|
}
|
|
490
|
-
|
|
491
|
-
const data = await fetch(
|
|
508
|
+
|
|
509
|
+
const data = await fetch("https://api.example.com/data");
|
|
492
510
|
const json = await data.json();
|
|
493
|
-
|
|
494
|
-
writeFileSync(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
511
|
+
|
|
512
|
+
writeFileSync(
|
|
513
|
+
cacheFile,
|
|
514
|
+
JSON.stringify({
|
|
515
|
+
timestamp: Date.now(),
|
|
516
|
+
data: json,
|
|
517
|
+
}),
|
|
518
|
+
);
|
|
519
|
+
|
|
499
520
|
return json;
|
|
500
521
|
}
|
|
501
522
|
```
|
|
502
523
|
|
|
503
524
|
```typescript
|
|
504
525
|
// ✅ Limit API calls
|
|
505
|
-
const posts = await fetch(
|
|
526
|
+
const posts = await fetch("https://api.example.com/posts?limit=10");
|
|
506
527
|
// Don't fetch all 10,000 posts at build time
|
|
507
528
|
```
|
|
508
529
|
|
|
509
530
|
```typescript
|
|
510
531
|
// ✅ Parallel processing when possible
|
|
511
532
|
const [posts, authors, tags] = await Promise.all([
|
|
512
|
-
fetch(
|
|
513
|
-
fetch(
|
|
514
|
-
fetch(
|
|
533
|
+
fetch("https://api.example.com/posts").then((r) => r.json()),
|
|
534
|
+
fetch("https://api.example.com/authors").then((r) => r.json()),
|
|
535
|
+
fetch("https://api.example.com/tags").then((r) => r.json()),
|
|
515
536
|
]);
|
|
516
537
|
```
|
|
517
538
|
|
|
@@ -519,15 +540,15 @@ const [posts, authors, tags] = await Promise.all([
|
|
|
519
540
|
|
|
520
541
|
```typescript
|
|
521
542
|
// ❌ Sequential API calls
|
|
522
|
-
const posts = await fetch(
|
|
523
|
-
const authors = await fetch(
|
|
524
|
-
const tags = await fetch(
|
|
543
|
+
const posts = await fetch("/api/posts").then((r) => r.json());
|
|
544
|
+
const authors = await fetch("/api/authors").then((r) => r.json());
|
|
545
|
+
const tags = await fetch("/api/tags").then((r) => r.json());
|
|
525
546
|
// Use Promise.all instead
|
|
526
547
|
```
|
|
527
548
|
|
|
528
549
|
```typescript
|
|
529
550
|
// ❌ Large data fetching
|
|
530
|
-
const allUsers = await fetch(
|
|
551
|
+
const allUsers = await fetch("/api/users?limit=100000");
|
|
531
552
|
// This makes builds slow and outputs huge HTML
|
|
532
553
|
```
|
|
533
554
|
|
|
@@ -547,7 +568,7 @@ com.innerHTML = `
|
|
|
547
568
|
```html
|
|
548
569
|
<!-- ✅ Share styles via CSS -->
|
|
549
570
|
<!-- Don't inline repeated styles in every component -->
|
|
550
|
-
<link rel="stylesheet" href="/styles/components.css"
|
|
571
|
+
<link rel="stylesheet" href="/styles/components.css" />
|
|
551
572
|
```
|
|
552
573
|
|
|
553
574
|
**❌ DON'T:**
|
|
@@ -591,14 +612,14 @@ com.innerHTML = `<div class="card">${content}</div>`;
|
|
|
591
612
|
// ✅ Escape HTML in user content
|
|
592
613
|
function escapeHtml(unsafe: string): string {
|
|
593
614
|
return unsafe
|
|
594
|
-
.replace(/&/g,
|
|
595
|
-
.replace(/</g,
|
|
596
|
-
.replace(/>/g,
|
|
597
|
-
.replace(/"/g,
|
|
598
|
-
.replace(/'/g,
|
|
615
|
+
.replace(/&/g, "&")
|
|
616
|
+
.replace(/</g, "<")
|
|
617
|
+
.replace(/>/g, ">")
|
|
618
|
+
.replace(/"/g, """)
|
|
619
|
+
.replace(/'/g, "'");
|
|
599
620
|
}
|
|
600
621
|
|
|
601
|
-
const userComment = com.getAttribute(
|
|
622
|
+
const userComment = com.getAttribute("comment") || "";
|
|
602
623
|
com.innerHTML = `<p>${escapeHtml(userComment)}</p>`;
|
|
603
624
|
```
|
|
604
625
|
|
|
@@ -607,14 +628,14 @@ com.innerHTML = `<p>${escapeHtml(userComment)}</p>`;
|
|
|
607
628
|
function isSafeUrl(url: string): boolean {
|
|
608
629
|
try {
|
|
609
630
|
const parsed = new URL(url);
|
|
610
|
-
return [
|
|
631
|
+
return ["http:", "https:"].includes(parsed.protocol);
|
|
611
632
|
} catch {
|
|
612
633
|
return false;
|
|
613
634
|
}
|
|
614
635
|
}
|
|
615
636
|
|
|
616
|
-
const url = com.getAttribute(
|
|
617
|
-
const safeUrl = isSafeUrl(url) ? url :
|
|
637
|
+
const url = com.getAttribute("href") || "#";
|
|
638
|
+
const safeUrl = isSafeUrl(url) ? url : "#";
|
|
618
639
|
com.innerHTML = `<a href="${safeUrl}">Link</a>`;
|
|
619
640
|
```
|
|
620
641
|
|
|
@@ -622,13 +643,13 @@ com.innerHTML = `<a href="${safeUrl}">Link</a>`;
|
|
|
622
643
|
|
|
623
644
|
```typescript
|
|
624
645
|
// ❌ Raw user input
|
|
625
|
-
const userInput = com.getAttribute(
|
|
646
|
+
const userInput = com.getAttribute("text") || "";
|
|
626
647
|
com.innerHTML = userInput; // XSS vulnerability!
|
|
627
648
|
```
|
|
628
649
|
|
|
629
650
|
```typescript
|
|
630
651
|
// ❌ Unvalidated URLs
|
|
631
|
-
const url = com.getAttribute(
|
|
652
|
+
const url = com.getAttribute("href") || "";
|
|
632
653
|
com.innerHTML = `<a href="${url}">Link</a>`;
|
|
633
654
|
// Can be exploited: <my-link href="javascript:alert('xss')">
|
|
634
655
|
```
|
|
@@ -639,19 +660,19 @@ com.innerHTML = `<a href="${url}">Link</a>`;
|
|
|
639
660
|
|
|
640
661
|
```typescript
|
|
641
662
|
// ✅ Validate and sanitize
|
|
642
|
-
const count = parseInt(com.getAttribute(
|
|
663
|
+
const count = parseInt(com.getAttribute("count") || "5");
|
|
643
664
|
if (isNaN(count) || count < 0 || count > 100) {
|
|
644
|
-
throw new Error(
|
|
665
|
+
throw new Error("Invalid count: must be 0-100");
|
|
645
666
|
}
|
|
646
667
|
```
|
|
647
668
|
|
|
648
669
|
```typescript
|
|
649
670
|
// ✅ Whitelist allowed values
|
|
650
|
-
const validTypes = [
|
|
651
|
-
const type = com.getAttribute(
|
|
671
|
+
const validTypes = ["info", "warning", "error", "success"];
|
|
672
|
+
const type = com.getAttribute("type") || "info";
|
|
652
673
|
|
|
653
674
|
if (!validTypes.includes(type)) {
|
|
654
|
-
throw new Error(`Invalid type. Allowed: ${validTypes.join(
|
|
675
|
+
throw new Error(`Invalid type. Allowed: ${validTypes.join(", ")}`);
|
|
655
676
|
}
|
|
656
677
|
```
|
|
657
678
|
|
|
@@ -665,7 +686,7 @@ if (!validTypes.includes(type)) {
|
|
|
665
686
|
|
|
666
687
|
```typescript
|
|
667
688
|
// ✅ Fail fast with clear messages
|
|
668
|
-
const required = com.getAttribute(
|
|
689
|
+
const required = com.getAttribute("data");
|
|
669
690
|
if (!required) {
|
|
670
691
|
throw new Error('Attribute "data" is required for data-table component');
|
|
671
692
|
}
|
|
@@ -674,11 +695,11 @@ if (!required) {
|
|
|
674
695
|
```typescript
|
|
675
696
|
// ✅ Provide context
|
|
676
697
|
try {
|
|
677
|
-
const data = JSON.parse(com.getAttribute(
|
|
698
|
+
const data = JSON.parse(com.getAttribute("json") || "{}");
|
|
678
699
|
} catch (error) {
|
|
679
700
|
throw new Error(
|
|
680
701
|
`Failed to parse JSON in component. ` +
|
|
681
|
-
|
|
702
|
+
`Attribute value: ${com.getAttribute("json")}`,
|
|
682
703
|
);
|
|
683
704
|
}
|
|
684
705
|
```
|
|
@@ -687,16 +708,16 @@ try {
|
|
|
687
708
|
|
|
688
709
|
```typescript
|
|
689
710
|
// ❌ Silent failures
|
|
690
|
-
const data = com.getAttribute(
|
|
711
|
+
const data = com.getAttribute("data");
|
|
691
712
|
if (!data) {
|
|
692
|
-
com.innerHTML =
|
|
713
|
+
com.innerHTML = ""; // Component just disappears, unclear why
|
|
693
714
|
return;
|
|
694
715
|
}
|
|
695
716
|
```
|
|
696
717
|
|
|
697
718
|
```typescript
|
|
698
719
|
// ❌ Generic errors
|
|
699
|
-
throw new Error(
|
|
720
|
+
throw new Error("Something went wrong"); // Not helpful
|
|
700
721
|
```
|
|
701
722
|
|
|
702
723
|
### Pre-rendering Errors
|
|
@@ -707,11 +728,11 @@ throw new Error('Something went wrong'); // Not helpful
|
|
|
707
728
|
// ✅ Graceful degradation
|
|
708
729
|
async function fetchData() {
|
|
709
730
|
try {
|
|
710
|
-
const response = await fetch(
|
|
731
|
+
const response = await fetch("https://api.example.com/data");
|
|
711
732
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
712
733
|
return await response.json();
|
|
713
734
|
} catch (error) {
|
|
714
|
-
console.warn(
|
|
735
|
+
console.warn("API fetch failed, using fallback:", error);
|
|
715
736
|
return { items: [] }; // Fallback data
|
|
716
737
|
}
|
|
717
738
|
}
|
|
@@ -726,7 +747,7 @@ try {
|
|
|
726
747
|
const response = await fetch(url, { signal: controller.signal });
|
|
727
748
|
return await response.json();
|
|
728
749
|
} catch (error) {
|
|
729
|
-
console.error(
|
|
750
|
+
console.error("Request timeout or failed");
|
|
730
751
|
return fallbackData;
|
|
731
752
|
}
|
|
732
753
|
```
|
|
@@ -782,6 +803,7 @@ test -f web/index.js || exit 1
|
|
|
782
803
|
Components generate HTML at build time. For runtime interactivity, use your regular `.ts` files:
|
|
783
804
|
|
|
784
805
|
**Static button component:**
|
|
806
|
+
|
|
785
807
|
```html
|
|
786
808
|
<!-- button-counter.com.html -->
|
|
787
809
|
<button class="counter-button" data-count="0">
|
|
@@ -790,13 +812,14 @@ Components generate HTML at build time. For runtime interactivity, use your regu
|
|
|
790
812
|
```
|
|
791
813
|
|
|
792
814
|
**Add interactivity in browser:**
|
|
815
|
+
|
|
793
816
|
```typescript
|
|
794
817
|
// index.ts - runs in browser
|
|
795
|
-
document.querySelectorAll(
|
|
818
|
+
document.querySelectorAll(".counter-button").forEach((btn) => {
|
|
796
819
|
let count = 0;
|
|
797
|
-
btn.addEventListener(
|
|
820
|
+
btn.addEventListener("click", () => {
|
|
798
821
|
count++;
|
|
799
|
-
const span = btn.querySelector(
|
|
822
|
+
const span = btn.querySelector(".count");
|
|
800
823
|
if (span) span.textContent = count.toString();
|
|
801
824
|
});
|
|
802
825
|
});
|
|
@@ -820,7 +843,7 @@ page → layout → section → card → header → icon → svg (7 levels!)
|
|
|
820
843
|
|
|
821
844
|
```typescript
|
|
822
845
|
// user-badge.com.ts
|
|
823
|
-
const value = com.getAttribute(
|
|
846
|
+
const value = com.getAttribute("value") || "0";
|
|
824
847
|
const num = parseInt(value, 10);
|
|
825
848
|
|
|
826
849
|
if (isNaN(num)) {
|
|
@@ -837,14 +860,14 @@ Always escape dynamic content to prevent XSS:
|
|
|
837
860
|
```typescript
|
|
838
861
|
function escapeHtml(unsafe: string): string {
|
|
839
862
|
return unsafe
|
|
840
|
-
.replace(/&/g,
|
|
841
|
-
.replace(/</g,
|
|
842
|
-
.replace(/>/g,
|
|
843
|
-
.replace(/"/g,
|
|
844
|
-
.replace(/'/g,
|
|
863
|
+
.replace(/&/g, "&")
|
|
864
|
+
.replace(/</g, "<")
|
|
865
|
+
.replace(/>/g, ">")
|
|
866
|
+
.replace(/"/g, """)
|
|
867
|
+
.replace(/'/g, "'");
|
|
845
868
|
}
|
|
846
869
|
|
|
847
|
-
const text = com.getAttribute(
|
|
870
|
+
const text = com.getAttribute("text") || "";
|
|
848
871
|
com.innerHTML = `<p>${escapeHtml(text)}</p>`;
|
|
849
872
|
```
|
|
850
873
|
|
|
@@ -890,6 +913,7 @@ rsync -avz web/ server:/var/www/html/
|
|
|
890
913
|
### Build-Time vs Runtime
|
|
891
914
|
|
|
892
915
|
**Build time (.pre.ts, .com.ts):**
|
|
916
|
+
|
|
893
917
|
- Runs in Bun runtime (not browser)
|
|
894
918
|
- Full TypeScript support
|
|
895
919
|
- Can use `fetch()`, `Bun.file()`, Node APIs
|
|
@@ -897,6 +921,7 @@ rsync -avz web/ server:/var/www/html/
|
|
|
897
921
|
- Manipulate DOM with full access to `document`
|
|
898
922
|
|
|
899
923
|
**Runtime (browser):**
|
|
924
|
+
|
|
900
925
|
- Your compiled `.js` files run
|
|
901
926
|
- Standard browser environment
|
|
902
927
|
- Use any browser APIs, libraries, patterns
|
|
@@ -908,16 +933,17 @@ Tkeron **does NOT bundle** npm packages into browser code.
|
|
|
908
933
|
|
|
909
934
|
```typescript
|
|
910
935
|
// .pre.ts or .com.ts - Build time
|
|
911
|
-
import _ from
|
|
936
|
+
import _ from "lodash"; // ✅ Works - runs in Bun during build
|
|
912
937
|
const result = _.chunk([1, 2, 3], 2);
|
|
913
938
|
```
|
|
914
939
|
|
|
915
940
|
```typescript
|
|
916
|
-
// index.ts - Browser runtime
|
|
917
|
-
import _ from
|
|
941
|
+
// index.ts - Browser runtime
|
|
942
|
+
import _ from "lodash"; // ❌ Won't work - not bundled
|
|
918
943
|
```
|
|
919
944
|
|
|
920
945
|
**Solutions:**
|
|
946
|
+
|
|
921
947
|
- Use CDN: `<script src="https://cdn.jsdelivr.net/npm/lodash"></script>`
|
|
922
948
|
- Use a bundler alongside Tkeron (Vite, esbuild, etc.)
|
|
923
949
|
- Write vanilla JavaScript (often simpler than you think)
|
|
@@ -998,7 +1024,7 @@ websrc/features/
|
|
|
998
1024
|
|
|
999
1025
|
```typescript
|
|
1000
1026
|
// index.pre.ts
|
|
1001
|
-
const isProd = process.env.NODE_ENV ===
|
|
1027
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
1002
1028
|
|
|
1003
1029
|
if (!isProd) {
|
|
1004
1030
|
// Development-only code
|