tkeron 5.0.1 → 5.1.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/bun.lock +2 -2
- package/changelog.md +24 -0
- package/docs/components-markdown.md +2 -0
- package/docs/components-typescript.md +59 -0
- package/examples/with_com_html_in_ts/src/index.html +20 -0
- package/examples/with_com_html_in_ts/src/info-panel.com.html +12 -0
- package/examples/with_com_html_in_ts/src/info-panel.com.ts +3 -0
- package/examples/with_com_html_in_ts/src/user-card.com.html +12 -0
- package/examples/with_com_html_in_ts/src/user-card.com.ts +8 -0
- package/mcp-server.ts +2 -0
- package/package.json +4 -3
- package/src/processCom.ts +90 -52
- package/src/processComMd.ts +90 -48
- package/src/processComTs.ts +150 -52
- package/src/processPre.ts +10 -2
package/bun.lock
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "tkeron",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
8
|
+
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
9
9
|
"@tkeron/commands": "0.4.6",
|
|
10
10
|
"@tkeron/html-parser": "1.5.5",
|
|
11
11
|
"@tkeron/tools": "^0.4.0",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"packages": {
|
|
21
21
|
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
|
22
22
|
|
|
23
|
-
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.
|
|
23
|
+
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-qOdO524oPMkUsOJTrsH9vz/HN3B5pKyW+9zIW51A9kDMVe7ON70drz1ouoyoyOcfzc+oxhkQ6jWmbyKnlWmYqA=="],
|
|
24
24
|
|
|
25
25
|
"@tkeron/commands": ["@tkeron/commands@0.4.6", "", { "peerDependencies": { "typescript": "^5.9.3" } }, "sha512-Bojs3GmIpvd/4Mcw7FvbohRcDyHEzs3ZJHxwIeXE0diQzN+ER3R30xIFovgzwE8QfWvC9uFDaN2VTk4O+0ffyA=="],
|
|
26
26
|
|
package/changelog.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
# v5.1.0
|
|
2
|
+
|
|
3
|
+
## Feature: .com.html template injection into .com.ts components
|
|
4
|
+
|
|
5
|
+
- When both `name.com.html` and `name.com.ts` exist for the same component, the `.com.html` content is loaded as `com.innerHTML` before executing the `.com.ts` code
|
|
6
|
+
- The `.com.html` acts as a static template that `.com.ts` can read, modify, or override
|
|
7
|
+
- Template resolution uses the same priority system: adjacent to `.com.ts` first, then fallback to root
|
|
8
|
+
- Fully backwards compatible: `.com.ts`-only components work exactly as before
|
|
9
|
+
- Added example `with_com_html_in_ts` demonstrating the feature
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# v5.0.2
|
|
14
|
+
|
|
15
|
+
## Performance: component processing optimization
|
|
16
|
+
|
|
17
|
+
- Perf: single glob scan per build pass to pre-load all component paths into a `Map<tagName, filePath[]>` — eliminates O(pages × components) filesystem scans in `processCom`, `processComTs`, `processComMd`
|
|
18
|
+
- Perf: `Map<path, content>` cache avoids redundant disk reads when the same component is used across multiple pages
|
|
19
|
+
- Perf: `Map<code, jsCode>` transpilation cache in `processComTs` — `Bun.Transpiler.transformSync` result reused for unchanged `.com.ts` files
|
|
20
|
+
- Perf: parallel page processing via `Promise.all` instead of sequential `for...of` — independent pages now process concurrently
|
|
21
|
+
- Perf: lazy `package.json` read in `processPre` — removed top-level `await` that ran on module import regardless of whether pre-rendering was needed
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
1
25
|
# v5.0.1
|
|
2
26
|
|
|
3
27
|
## Documentation fixes and dependency updates
|
|
@@ -76,6 +76,8 @@ When multiple component types exist for the same name, Tkeron uses this priority
|
|
|
76
76
|
2. **`.com.html`** (HTML components)
|
|
77
77
|
3. **`.com.md`** (Markdown components — lowest priority)
|
|
78
78
|
|
|
79
|
+
**Special case:** When both `.com.ts` and `.com.html` exist for the same name, the `.com.html` is loaded as the template (`com.innerHTML`) before the `.com.ts` executes. See [TypeScript Components — HTML Template + TypeScript Logic](./components-typescript.md#html-template--typescript-logic).
|
|
80
|
+
|
|
79
81
|
If both `hero-text.com.html` and `hero-text.com.md` exist, the `.com.html` version is used and `.com.md` is ignored.
|
|
80
82
|
|
|
81
83
|
### 4. Build Process
|
|
@@ -540,6 +540,65 @@ If a component doesn't need attributes or logic, use [HTML components](./compone
|
|
|
540
540
|
</footer>
|
|
541
541
|
```
|
|
542
542
|
|
|
543
|
+
## HTML Template + TypeScript Logic
|
|
544
|
+
|
|
545
|
+
When both `name.com.html` and `name.com.ts` exist for the same component, the `.com.html` content is loaded as `com.innerHTML` **before** the `.com.ts` code executes. This lets you separate structure from logic.
|
|
546
|
+
|
|
547
|
+
### How It Works
|
|
548
|
+
|
|
549
|
+
```
|
|
550
|
+
1. Tkeron finds <user-card name="Alice">
|
|
551
|
+
2. Loads user-card.com.html content into com.innerHTML
|
|
552
|
+
3. Executes user-card.com.ts (com already has the template)
|
|
553
|
+
4. Replaces <user-card> with the final com.innerHTML
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### Example
|
|
557
|
+
|
|
558
|
+
**Template** (`user-card.com.html`):
|
|
559
|
+
|
|
560
|
+
```html
|
|
561
|
+
<div class="card">
|
|
562
|
+
<h3 class="name"></h3>
|
|
563
|
+
<p class="role"></p>
|
|
564
|
+
</div>
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**Logic** (`user-card.com.ts`):
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
const name = com.getAttribute("data-name") || "Unknown";
|
|
571
|
+
const role = com.getAttribute("data-role") || "N/A";
|
|
572
|
+
|
|
573
|
+
const nameEl = com.querySelector(".name");
|
|
574
|
+
const roleEl = com.querySelector(".role");
|
|
575
|
+
|
|
576
|
+
if (nameEl) nameEl.textContent = name;
|
|
577
|
+
if (roleEl) roleEl.textContent = `Role: ${role}`;
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Usage:**
|
|
581
|
+
|
|
582
|
+
```html
|
|
583
|
+
<user-card data-name="Alice" data-role="Developer"></user-card>
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
**Output:**
|
|
587
|
+
|
|
588
|
+
```html
|
|
589
|
+
<div class="card">
|
|
590
|
+
<h3 class="name">Alice</h3>
|
|
591
|
+
<p class="role">Role: Developer</p>
|
|
592
|
+
</div>
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Key Points
|
|
596
|
+
|
|
597
|
+
- The `.com.ts` can read, modify, or completely override the template
|
|
598
|
+
- If only `.com.ts` exists (no `.com.html`), it works exactly as before
|
|
599
|
+
- Template resolution follows the same directory priority as components (adjacent first, then root)
|
|
600
|
+
- Great for separating HTML structure from dynamic logic
|
|
601
|
+
|
|
543
602
|
## Debugging
|
|
544
603
|
|
|
545
604
|
### Log at Build Time
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>HTML Template in TS Component Example</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<h1>.com.html Template + .com.ts Logic</h1>
|
|
10
|
+
|
|
11
|
+
<user-card data-name="Alice" data-role="Developer"></user-card>
|
|
12
|
+
<user-card data-name="Bob" data-role="Designer"></user-card>
|
|
13
|
+
|
|
14
|
+
<info-panel data-title="Features">
|
|
15
|
+
<li>HTML templates for structure</li>
|
|
16
|
+
<li>TypeScript for logic</li>
|
|
17
|
+
<li>Clean separation of concerns</li>
|
|
18
|
+
</info-panel>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const name = com.getAttribute("data-name") || "Unknown";
|
|
2
|
+
const role = com.getAttribute("data-role") || "N/A";
|
|
3
|
+
|
|
4
|
+
const nameEl = com.querySelector(".name");
|
|
5
|
+
const roleEl = com.querySelector(".role");
|
|
6
|
+
|
|
7
|
+
if (nameEl) nameEl.textContent = name;
|
|
8
|
+
if (roleEl) roleEl.textContent = `Role: ${role}`;
|
package/mcp-server.ts
CHANGED
|
@@ -106,6 +106,8 @@ const EXAMPLE_DESCRIPTIONS: { [key: string]: string } = {
|
|
|
106
106
|
"TypeScript components (.com.ts) with local override priority",
|
|
107
107
|
with_com_mixed_priority:
|
|
108
108
|
"Mixed .com.html and .com.ts showing processing order",
|
|
109
|
+
with_com_html_in_ts:
|
|
110
|
+
"HTML template (.com.html) injected into .com.ts as com.innerHTML before execution",
|
|
109
111
|
with_component_iteration:
|
|
110
112
|
"Component iteration: .com.ts with logic, .com.html for templates",
|
|
111
113
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tkeron",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "CLI build tool for vanilla web development with TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -26,12 +26,13 @@
|
|
|
26
26
|
"sample5": "bun . build examples/with_com_ts/src",
|
|
27
27
|
"sample6": "bun . build examples/with_com_ts_priority/src",
|
|
28
28
|
"sample7": "bun . build examples/with_com_mixed_priority/src",
|
|
29
|
-
"
|
|
29
|
+
"sample8": "bun . build examples/with_com_html_in_ts/src",
|
|
30
|
+
"build_examples": "bun run sample1 && bun run sample2 && bun run sample3 && bun run sample4 && bun run sample5 && bun run sample6 && bun run sample7 && bun run sample8"
|
|
30
31
|
},
|
|
31
32
|
"author": "tkeron",
|
|
32
33
|
"license": "MIT",
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
35
36
|
"@tkeron/commands": "0.4.6",
|
|
36
37
|
"@tkeron/html-parser": "1.5.5",
|
|
37
38
|
"@tkeron/tools": "^0.4.0"
|
package/src/processCom.ts
CHANGED
|
@@ -12,10 +12,50 @@ const ensureHtmlDocument = (html: string): string => {
|
|
|
12
12
|
return `${DOCTYPE}<html><head></head><body>${html}</body></html>`;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
interface ComponentCache {
|
|
16
|
+
componentMap: Map<string, string[]>;
|
|
17
|
+
contentCache: Map<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const buildComponentMap = (rootDir: string): Map<string, string[]> => {
|
|
21
|
+
const allFiles = getFilePaths(rootDir, "**/*.com.html", true);
|
|
22
|
+
const map = new Map<string, string[]>();
|
|
23
|
+
for (const file of allFiles) {
|
|
24
|
+
const basename = file.split("/").pop()!;
|
|
25
|
+
const tagName = basename.replace(/\.com\.html$/, "");
|
|
26
|
+
if (!map.has(tagName)) map.set(tagName, []);
|
|
27
|
+
map.get(tagName)!.push(file);
|
|
28
|
+
}
|
|
29
|
+
return map;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const getCachedContent = async (
|
|
33
|
+
path: string,
|
|
34
|
+
cache: Map<string, string>,
|
|
35
|
+
): Promise<string> => {
|
|
36
|
+
const cached = cache.get(path);
|
|
37
|
+
if (cached !== undefined) return cached;
|
|
38
|
+
const content = await Bun.file(path).text();
|
|
39
|
+
cache.set(path, content);
|
|
40
|
+
return content;
|
|
41
|
+
};
|
|
42
|
+
|
|
15
43
|
export interface ProcessComOptions {
|
|
16
44
|
logger?: Logger;
|
|
17
45
|
}
|
|
18
46
|
|
|
47
|
+
const resolveComponent = (
|
|
48
|
+
tagName: string,
|
|
49
|
+
currentDir: string,
|
|
50
|
+
componentMap: Map<string, string[]>,
|
|
51
|
+
): string | null => {
|
|
52
|
+
const matches = componentMap.get(tagName);
|
|
53
|
+
if (!matches || matches.length === 0) return null;
|
|
54
|
+
const localPath = join(currentDir, `${tagName}.com.html`);
|
|
55
|
+
if (matches.includes(localPath)) return localPath;
|
|
56
|
+
return matches[0]!;
|
|
57
|
+
};
|
|
58
|
+
|
|
19
59
|
export const processCom = async (
|
|
20
60
|
tempDir: string,
|
|
21
61
|
options: ProcessComOptions = {},
|
|
@@ -27,37 +67,45 @@ export const processCom = async (
|
|
|
27
67
|
return false;
|
|
28
68
|
}
|
|
29
69
|
|
|
70
|
+
const componentMap = buildComponentMap(tempDir);
|
|
71
|
+
const cache: ComponentCache = {
|
|
72
|
+
componentMap,
|
|
73
|
+
contentCache: new Map(),
|
|
74
|
+
};
|
|
75
|
+
|
|
30
76
|
const htmlFiles = getFilePaths(tempDir, "**/*.html", true).filter(
|
|
31
77
|
(p) => !p.endsWith(".com.html"),
|
|
32
78
|
);
|
|
33
79
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
80
|
+
const results = await Promise.all(
|
|
81
|
+
htmlFiles.map(async (htmlFile) => {
|
|
82
|
+
const htmlContent = await Bun.file(htmlFile).text();
|
|
83
|
+
const document = parseHTML(ensureHtmlDocument(htmlContent));
|
|
84
|
+
|
|
85
|
+
const htmlElement =
|
|
86
|
+
document.querySelector("html") || document.documentElement;
|
|
87
|
+
let changed = false;
|
|
88
|
+
if (htmlElement) {
|
|
89
|
+
changed = await processComponents(
|
|
90
|
+
htmlElement,
|
|
91
|
+
dirname(htmlFile),
|
|
92
|
+
tempDir,
|
|
93
|
+
[],
|
|
94
|
+
0,
|
|
95
|
+
log,
|
|
96
|
+
cache,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
53
99
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
100
|
+
const output =
|
|
101
|
+
DOCTYPE +
|
|
102
|
+
(htmlElement?.outerHTML || document.documentElement?.outerHTML || "");
|
|
103
|
+
await Bun.write(htmlFile, output);
|
|
104
|
+
return changed;
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
59
107
|
|
|
60
|
-
return
|
|
108
|
+
return results.some(Boolean);
|
|
61
109
|
};
|
|
62
110
|
|
|
63
111
|
const processComponents = async (
|
|
@@ -67,6 +115,7 @@ const processComponents = async (
|
|
|
67
115
|
componentStack: string[],
|
|
68
116
|
depth: number = 0,
|
|
69
117
|
log: Logger = silentLogger,
|
|
118
|
+
cache: ComponentCache,
|
|
70
119
|
): Promise<boolean> => {
|
|
71
120
|
let hasChanges = false;
|
|
72
121
|
const MAX_DEPTH = 50;
|
|
@@ -82,13 +131,10 @@ const processComponents = async (
|
|
|
82
131
|
|
|
83
132
|
if (tagName) {
|
|
84
133
|
if (!tagName.includes("-")) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
true,
|
|
90
|
-
);
|
|
91
|
-
if ((await Bun.file(localPath).exists()) || globMatches.length > 0) {
|
|
134
|
+
const matches = cache.componentMap.get(tagName);
|
|
135
|
+
if (matches && matches.length > 0) {
|
|
136
|
+
const localPath = join(currentDir, `${tagName}.com.html`);
|
|
137
|
+
const filePath = matches.includes(localPath) ? localPath : matches[0];
|
|
92
138
|
log.error(
|
|
93
139
|
`\n❌ Error: Component name '${tagName}' must contain at least one hyphen.`,
|
|
94
140
|
);
|
|
@@ -96,30 +142,17 @@ const processComponents = async (
|
|
|
96
142
|
log.error(
|
|
97
143
|
` Rename '${tagName}.com.html' to 'tk-${tagName}.com.html' or similar.`,
|
|
98
144
|
);
|
|
99
|
-
log.error(
|
|
100
|
-
` File: ${(await Bun.file(localPath).exists()) ? localPath : globMatches[0]}\n`,
|
|
101
|
-
);
|
|
145
|
+
log.error(` File: ${filePath}\n`);
|
|
102
146
|
throw new Error(
|
|
103
147
|
`Component name '${tagName}' must contain at least one hyphen`,
|
|
104
148
|
);
|
|
105
149
|
}
|
|
106
150
|
} else {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
componentPath = localPath;
|
|
113
|
-
} else {
|
|
114
|
-
const globMatches = getFilePaths(
|
|
115
|
-
rootDir,
|
|
116
|
-
`**/${tagName}.com.html`,
|
|
117
|
-
true,
|
|
118
|
-
);
|
|
119
|
-
if (globMatches.length > 0) {
|
|
120
|
-
componentPath = globMatches[0];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
151
|
+
const componentPath = resolveComponent(
|
|
152
|
+
tagName,
|
|
153
|
+
currentDir,
|
|
154
|
+
cache.componentMap,
|
|
155
|
+
);
|
|
123
156
|
|
|
124
157
|
if (componentPath) {
|
|
125
158
|
hasChanges = true;
|
|
@@ -137,7 +170,10 @@ const processComponents = async (
|
|
|
137
170
|
throw new Error(`Circular dependency: ${chain}`);
|
|
138
171
|
}
|
|
139
172
|
|
|
140
|
-
const componentHtml = await
|
|
173
|
+
const componentHtml = await getCachedContent(
|
|
174
|
+
componentPath,
|
|
175
|
+
cache.contentCache,
|
|
176
|
+
);
|
|
141
177
|
|
|
142
178
|
const tempDoc = parseHTML(
|
|
143
179
|
`<html><body><div id="__tkeron_component_root__">${componentHtml}</div></body></html>`,
|
|
@@ -182,6 +218,7 @@ const processComponents = async (
|
|
|
182
218
|
nextStack,
|
|
183
219
|
depth + 1,
|
|
184
220
|
log,
|
|
221
|
+
cache,
|
|
185
222
|
);
|
|
186
223
|
hasChanges = hasChanges || nestedChanged;
|
|
187
224
|
}
|
|
@@ -197,6 +234,7 @@ const processComponents = async (
|
|
|
197
234
|
componentStack,
|
|
198
235
|
depth,
|
|
199
236
|
log,
|
|
237
|
+
cache,
|
|
200
238
|
);
|
|
201
239
|
hasChanges = hasChanges || childChanged;
|
|
202
240
|
}
|
package/src/processComMd.ts
CHANGED
|
@@ -12,10 +12,50 @@ const ensureHtmlDocument = (html: string): string => {
|
|
|
12
12
|
return `${DOCTYPE}<html><head></head><body>${html}</body></html>`;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
interface ComponentCache {
|
|
16
|
+
componentMap: Map<string, string[]>;
|
|
17
|
+
contentCache: Map<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const buildComponentMap = (rootDir: string): Map<string, string[]> => {
|
|
21
|
+
const allFiles = getFilePaths(rootDir, "**/*.com.md", true);
|
|
22
|
+
const map = new Map<string, string[]>();
|
|
23
|
+
for (const file of allFiles) {
|
|
24
|
+
const basename = file.split("/").pop()!;
|
|
25
|
+
const tagName = basename.replace(/\.com\.md$/, "");
|
|
26
|
+
if (!map.has(tagName)) map.set(tagName, []);
|
|
27
|
+
map.get(tagName)!.push(file);
|
|
28
|
+
}
|
|
29
|
+
return map;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const getCachedContent = async (
|
|
33
|
+
path: string,
|
|
34
|
+
cache: Map<string, string>,
|
|
35
|
+
): Promise<string> => {
|
|
36
|
+
const cached = cache.get(path);
|
|
37
|
+
if (cached !== undefined) return cached;
|
|
38
|
+
const content = await Bun.file(path).text();
|
|
39
|
+
cache.set(path, content);
|
|
40
|
+
return content;
|
|
41
|
+
};
|
|
42
|
+
|
|
15
43
|
export interface ProcessComMdOptions {
|
|
16
44
|
logger?: Logger;
|
|
17
45
|
}
|
|
18
46
|
|
|
47
|
+
const resolveComponent = (
|
|
48
|
+
tagName: string,
|
|
49
|
+
currentDir: string,
|
|
50
|
+
componentMap: Map<string, string[]>,
|
|
51
|
+
): string | null => {
|
|
52
|
+
const matches = componentMap.get(tagName);
|
|
53
|
+
if (!matches || matches.length === 0) return null;
|
|
54
|
+
const localPath = join(currentDir, `${tagName}.com.md`);
|
|
55
|
+
if (matches.includes(localPath)) return localPath;
|
|
56
|
+
return matches[0]!;
|
|
57
|
+
};
|
|
58
|
+
|
|
19
59
|
export const processComMd = async (
|
|
20
60
|
tempDir: string,
|
|
21
61
|
options: ProcessComMdOptions = {},
|
|
@@ -27,37 +67,45 @@ export const processComMd = async (
|
|
|
27
67
|
return false;
|
|
28
68
|
}
|
|
29
69
|
|
|
70
|
+
const componentMap = buildComponentMap(tempDir);
|
|
71
|
+
const cache: ComponentCache = {
|
|
72
|
+
componentMap,
|
|
73
|
+
contentCache: new Map(),
|
|
74
|
+
};
|
|
75
|
+
|
|
30
76
|
const htmlFiles = getFilePaths(tempDir, "**/*.html", true).filter(
|
|
31
77
|
(p) => !p.endsWith(".com.html"),
|
|
32
78
|
);
|
|
33
79
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
80
|
+
const results = await Promise.all(
|
|
81
|
+
htmlFiles.map(async (htmlFile) => {
|
|
82
|
+
const htmlContent = await Bun.file(htmlFile).text();
|
|
83
|
+
const document = parseHTML(ensureHtmlDocument(htmlContent));
|
|
84
|
+
|
|
85
|
+
const htmlElement =
|
|
86
|
+
document.querySelector("html") || document.documentElement;
|
|
87
|
+
let changed = false;
|
|
88
|
+
if (htmlElement) {
|
|
89
|
+
changed = await processComponents(
|
|
90
|
+
htmlElement,
|
|
91
|
+
dirname(htmlFile),
|
|
92
|
+
tempDir,
|
|
93
|
+
[],
|
|
94
|
+
0,
|
|
95
|
+
log,
|
|
96
|
+
cache,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
53
99
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
100
|
+
const output =
|
|
101
|
+
DOCTYPE +
|
|
102
|
+
(htmlElement?.outerHTML || document.documentElement?.outerHTML || "");
|
|
103
|
+
await Bun.write(htmlFile, output);
|
|
104
|
+
return changed;
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
59
107
|
|
|
60
|
-
return
|
|
108
|
+
return results.some(Boolean);
|
|
61
109
|
};
|
|
62
110
|
|
|
63
111
|
const processComponents = async (
|
|
@@ -67,6 +115,7 @@ const processComponents = async (
|
|
|
67
115
|
componentStack: string[],
|
|
68
116
|
depth: number = 0,
|
|
69
117
|
log: Logger = silentLogger,
|
|
118
|
+
cache: ComponentCache,
|
|
70
119
|
): Promise<boolean> => {
|
|
71
120
|
let hasChanges = false;
|
|
72
121
|
const MAX_DEPTH = 50;
|
|
@@ -82,9 +131,10 @@ const processComponents = async (
|
|
|
82
131
|
|
|
83
132
|
if (tagName) {
|
|
84
133
|
if (!tagName.includes("-")) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
134
|
+
const matches = cache.componentMap.get(tagName);
|
|
135
|
+
if (matches && matches.length > 0) {
|
|
136
|
+
const localPath = join(currentDir, `${tagName}.com.md`);
|
|
137
|
+
const filePath = matches.includes(localPath) ? localPath : matches[0];
|
|
88
138
|
log.error(
|
|
89
139
|
`\n❌ Error: Component name '${tagName}' must contain at least one hyphen.`,
|
|
90
140
|
);
|
|
@@ -92,30 +142,17 @@ const processComponents = async (
|
|
|
92
142
|
log.error(
|
|
93
143
|
` Rename '${tagName}.com.md' to 'tk-${tagName}.com.md' or similar.`,
|
|
94
144
|
);
|
|
95
|
-
log.error(
|
|
96
|
-
` File: ${(await Bun.file(localPath).exists()) ? localPath : globMatches[0]}\n`,
|
|
97
|
-
);
|
|
145
|
+
log.error(` File: ${filePath}\n`);
|
|
98
146
|
throw new Error(
|
|
99
147
|
`Component name '${tagName}' must contain at least one hyphen`,
|
|
100
148
|
);
|
|
101
149
|
}
|
|
102
150
|
} else {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
componentPath = localPath;
|
|
109
|
-
} else {
|
|
110
|
-
const globMatches = getFilePaths(
|
|
111
|
-
rootDir,
|
|
112
|
-
`**/${tagName}.com.md`,
|
|
113
|
-
true,
|
|
114
|
-
);
|
|
115
|
-
if (globMatches.length > 0) {
|
|
116
|
-
componentPath = globMatches[0];
|
|
117
|
-
}
|
|
118
|
-
}
|
|
151
|
+
const componentPath = resolveComponent(
|
|
152
|
+
tagName,
|
|
153
|
+
currentDir,
|
|
154
|
+
cache.componentMap,
|
|
155
|
+
);
|
|
119
156
|
|
|
120
157
|
if (componentPath) {
|
|
121
158
|
hasChanges = true;
|
|
@@ -131,7 +168,10 @@ const processComponents = async (
|
|
|
131
168
|
throw new Error(`Circular dependency: ${chain}`);
|
|
132
169
|
}
|
|
133
170
|
|
|
134
|
-
const mdContent = await
|
|
171
|
+
const mdContent = await getCachedContent(
|
|
172
|
+
componentPath,
|
|
173
|
+
cache.contentCache,
|
|
174
|
+
);
|
|
135
175
|
const componentHtml = Bun.markdown.html(mdContent);
|
|
136
176
|
|
|
137
177
|
const tempDoc = parseHTML(
|
|
@@ -177,6 +217,7 @@ const processComponents = async (
|
|
|
177
217
|
nextStack,
|
|
178
218
|
depth + 1,
|
|
179
219
|
log,
|
|
220
|
+
cache,
|
|
180
221
|
);
|
|
181
222
|
hasChanges = hasChanges || nestedChanged;
|
|
182
223
|
}
|
|
@@ -192,6 +233,7 @@ const processComponents = async (
|
|
|
192
233
|
componentStack,
|
|
193
234
|
depth,
|
|
194
235
|
log,
|
|
236
|
+
cache,
|
|
195
237
|
);
|
|
196
238
|
hasChanges = hasChanges || childChanged;
|
|
197
239
|
}
|
package/src/processComTs.ts
CHANGED
|
@@ -12,6 +12,60 @@ const ensureHtmlDocument = (html: string): string => {
|
|
|
12
12
|
return `${DOCTYPE}<html><head></head><body>${html}</body></html>`;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
interface ComponentCache {
|
|
16
|
+
componentMap: Map<string, string[]>;
|
|
17
|
+
htmlTemplateMap: Map<string, string[]>;
|
|
18
|
+
contentCache: Map<string, string>;
|
|
19
|
+
transpileCache: Map<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const buildComponentMap = (rootDir: string): Map<string, string[]> => {
|
|
23
|
+
const allFiles = getFilePaths(rootDir, "**/*.com.ts", true);
|
|
24
|
+
const map = new Map<string, string[]>();
|
|
25
|
+
for (const file of allFiles) {
|
|
26
|
+
const basename = file.split("/").pop()!;
|
|
27
|
+
const tagName = basename.replace(/\.com\.ts$/, "");
|
|
28
|
+
if (!map.has(tagName)) map.set(tagName, []);
|
|
29
|
+
map.get(tagName)!.push(file);
|
|
30
|
+
}
|
|
31
|
+
return map;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const buildHtmlTemplateMap = (rootDir: string): Map<string, string[]> => {
|
|
35
|
+
const allFiles = getFilePaths(rootDir, "**/*.com.html", true);
|
|
36
|
+
const map = new Map<string, string[]>();
|
|
37
|
+
for (const file of allFiles) {
|
|
38
|
+
const basename = file.split("/").pop()!;
|
|
39
|
+
const tagName = basename.replace(/\.com\.html$/, "");
|
|
40
|
+
if (!map.has(tagName)) map.set(tagName, []);
|
|
41
|
+
map.get(tagName)!.push(file);
|
|
42
|
+
}
|
|
43
|
+
return map;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getCachedContent = async (
|
|
47
|
+
path: string,
|
|
48
|
+
cache: Map<string, string>,
|
|
49
|
+
): Promise<string> => {
|
|
50
|
+
const cached = cache.get(path);
|
|
51
|
+
if (cached !== undefined) return cached;
|
|
52
|
+
const content = await Bun.file(path).text();
|
|
53
|
+
cache.set(path, content);
|
|
54
|
+
return content;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getCachedTranspile = (
|
|
58
|
+
tsCode: string,
|
|
59
|
+
cache: Map<string, string>,
|
|
60
|
+
): string => {
|
|
61
|
+
const cached = cache.get(tsCode);
|
|
62
|
+
if (cached !== undefined) return cached;
|
|
63
|
+
const transpiler = new Bun.Transpiler({ loader: "ts" });
|
|
64
|
+
const jsCode = transpiler.transformSync(tsCode);
|
|
65
|
+
cache.set(tsCode, jsCode);
|
|
66
|
+
return jsCode;
|
|
67
|
+
};
|
|
68
|
+
|
|
15
69
|
export interface ProcessComTsOptions {
|
|
16
70
|
logger?: Logger;
|
|
17
71
|
}
|
|
@@ -27,38 +81,73 @@ export const processComTs = async (
|
|
|
27
81
|
return false;
|
|
28
82
|
}
|
|
29
83
|
|
|
84
|
+
const componentMap = buildComponentMap(tempDir);
|
|
85
|
+
const htmlTemplateMap = buildHtmlTemplateMap(tempDir);
|
|
86
|
+
const cache: ComponentCache = {
|
|
87
|
+
componentMap,
|
|
88
|
+
htmlTemplateMap,
|
|
89
|
+
contentCache: new Map(),
|
|
90
|
+
transpileCache: new Map(),
|
|
91
|
+
};
|
|
92
|
+
|
|
30
93
|
const htmlFiles = getFilePaths(tempDir, "**/*.html", true).filter(
|
|
31
94
|
(p) => !p.endsWith(".com.html"),
|
|
32
95
|
);
|
|
33
96
|
|
|
34
|
-
|
|
97
|
+
const results = await Promise.all(
|
|
98
|
+
htmlFiles.map(async (htmlFile) => {
|
|
99
|
+
const htmlContent = await Bun.file(htmlFile).text();
|
|
100
|
+
const document = parseHTML(ensureHtmlDocument(htmlContent));
|
|
101
|
+
|
|
102
|
+
const htmlElement =
|
|
103
|
+
document.querySelector("html") || document.documentElement;
|
|
104
|
+
let changed = false;
|
|
105
|
+
if (htmlElement) {
|
|
106
|
+
changed = await processComponentsTs(
|
|
107
|
+
htmlElement,
|
|
108
|
+
dirname(htmlFile),
|
|
109
|
+
tempDir,
|
|
110
|
+
[],
|
|
111
|
+
0,
|
|
112
|
+
log,
|
|
113
|
+
options,
|
|
114
|
+
cache,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
35
117
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const changed = await processComponentsTs(
|
|
44
|
-
htmlElement,
|
|
45
|
-
dirname(htmlFile),
|
|
46
|
-
tempDir,
|
|
47
|
-
[],
|
|
48
|
-
0,
|
|
49
|
-
log,
|
|
50
|
-
options,
|
|
51
|
-
);
|
|
52
|
-
hasChanges = hasChanges || changed;
|
|
53
|
-
}
|
|
118
|
+
const output =
|
|
119
|
+
DOCTYPE +
|
|
120
|
+
(htmlElement?.outerHTML || document.documentElement?.outerHTML || "");
|
|
121
|
+
await Bun.write(htmlFile, output);
|
|
122
|
+
return changed;
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
54
125
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
(htmlElement?.outerHTML || document.documentElement?.outerHTML || "");
|
|
58
|
-
await Bun.write(htmlFile, output);
|
|
59
|
-
}
|
|
126
|
+
return results.some(Boolean);
|
|
127
|
+
};
|
|
60
128
|
|
|
61
|
-
|
|
129
|
+
const resolveComponent = (
|
|
130
|
+
tagName: string,
|
|
131
|
+
currentDir: string,
|
|
132
|
+
componentMap: Map<string, string[]>,
|
|
133
|
+
): string | null => {
|
|
134
|
+
const matches = componentMap.get(tagName);
|
|
135
|
+
if (!matches || matches.length === 0) return null;
|
|
136
|
+
const localPath = join(currentDir, `${tagName}.com.ts`);
|
|
137
|
+
if (matches.includes(localPath)) return localPath;
|
|
138
|
+
return matches[0]!;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const resolveHtmlTemplate = (
|
|
142
|
+
tagName: string,
|
|
143
|
+
tsDir: string,
|
|
144
|
+
htmlTemplateMap: Map<string, string[]>,
|
|
145
|
+
): string | null => {
|
|
146
|
+
const matches = htmlTemplateMap.get(tagName);
|
|
147
|
+
if (!matches || matches.length === 0) return null;
|
|
148
|
+
const adjacentPath = join(tsDir, `${tagName}.com.html`);
|
|
149
|
+
if (matches.includes(adjacentPath)) return adjacentPath;
|
|
150
|
+
return matches[0]!;
|
|
62
151
|
};
|
|
63
152
|
|
|
64
153
|
const processComponentsTs = async (
|
|
@@ -69,6 +158,7 @@ const processComponentsTs = async (
|
|
|
69
158
|
depth: number = 0,
|
|
70
159
|
log: Logger = silentLogger,
|
|
71
160
|
options: ProcessComTsOptions = {},
|
|
161
|
+
cache: ComponentCache,
|
|
72
162
|
): Promise<boolean> => {
|
|
73
163
|
let hasChanges = false;
|
|
74
164
|
const MAX_DEPTH = 50;
|
|
@@ -84,9 +174,10 @@ const processComponentsTs = async (
|
|
|
84
174
|
|
|
85
175
|
if (tagName) {
|
|
86
176
|
if (!tagName.includes("-")) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
177
|
+
const matches = cache.componentMap.get(tagName);
|
|
178
|
+
if (matches && matches.length > 0) {
|
|
179
|
+
const localPath = join(currentDir, `${tagName}.com.ts`);
|
|
180
|
+
const filePath = matches.includes(localPath) ? localPath : matches[0];
|
|
90
181
|
log.error(
|
|
91
182
|
`\n❌ Error: Component name '${tagName}' must contain at least one hyphen.`,
|
|
92
183
|
);
|
|
@@ -94,30 +185,17 @@ const processComponentsTs = async (
|
|
|
94
185
|
log.error(
|
|
95
186
|
` Rename '${tagName}.com.ts' to 'tk-${tagName}.com.ts' or similar.`,
|
|
96
187
|
);
|
|
97
|
-
log.error(
|
|
98
|
-
` File: ${(await Bun.file(localPath).exists()) ? localPath : globMatches[0]}\n`,
|
|
99
|
-
);
|
|
188
|
+
log.error(` File: ${filePath}\n`);
|
|
100
189
|
throw new Error(
|
|
101
190
|
`Component name '${tagName}' must contain at least one hyphen`,
|
|
102
191
|
);
|
|
103
192
|
}
|
|
104
193
|
} else {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
componentPath = localPath;
|
|
111
|
-
} else {
|
|
112
|
-
const globMatches = getFilePaths(
|
|
113
|
-
rootDir,
|
|
114
|
-
`**/${tagName}.com.ts`,
|
|
115
|
-
true,
|
|
116
|
-
);
|
|
117
|
-
if (globMatches.length > 0) {
|
|
118
|
-
componentPath = globMatches[0];
|
|
119
|
-
}
|
|
120
|
-
}
|
|
194
|
+
const componentPath = resolveComponent(
|
|
195
|
+
tagName,
|
|
196
|
+
currentDir,
|
|
197
|
+
cache.componentMap,
|
|
198
|
+
);
|
|
121
199
|
|
|
122
200
|
if (componentPath) {
|
|
123
201
|
hasChanges = true;
|
|
@@ -133,7 +211,10 @@ const processComponentsTs = async (
|
|
|
133
211
|
throw new Error(`Circular dependency: ${chain}`);
|
|
134
212
|
}
|
|
135
213
|
|
|
136
|
-
const originalComponentCode = await
|
|
214
|
+
const originalComponentCode = await getCachedContent(
|
|
215
|
+
componentPath,
|
|
216
|
+
cache.contentCache,
|
|
217
|
+
);
|
|
137
218
|
|
|
138
219
|
const elementHTML = (child as any).outerHTML;
|
|
139
220
|
|
|
@@ -155,6 +236,19 @@ const processComponentsTs = async (
|
|
|
155
236
|
);
|
|
156
237
|
}
|
|
157
238
|
|
|
239
|
+
const htmlTemplatePath = resolveHtmlTemplate(
|
|
240
|
+
tagName,
|
|
241
|
+
dirname(componentPath),
|
|
242
|
+
cache.htmlTemplateMap,
|
|
243
|
+
);
|
|
244
|
+
if (htmlTemplatePath) {
|
|
245
|
+
const templateContent = await getCachedContent(
|
|
246
|
+
htmlTemplatePath,
|
|
247
|
+
cache.contentCache,
|
|
248
|
+
);
|
|
249
|
+
com.innerHTML = templateContent;
|
|
250
|
+
}
|
|
251
|
+
|
|
158
252
|
const hasImports = /^\s*import\s+/m.test(originalComponentCode);
|
|
159
253
|
|
|
160
254
|
try {
|
|
@@ -187,13 +281,15 @@ ${codeWithoutImports}
|
|
|
187
281
|
await module.component(com);
|
|
188
282
|
} finally {
|
|
189
283
|
try {
|
|
190
|
-
const
|
|
191
|
-
await
|
|
284
|
+
const { unlink } = await import("fs/promises");
|
|
285
|
+
await unlink(tempPath);
|
|
192
286
|
} catch {}
|
|
193
287
|
}
|
|
194
288
|
} else {
|
|
195
|
-
const
|
|
196
|
-
|
|
289
|
+
const jsCode = getCachedTranspile(
|
|
290
|
+
originalComponentCode,
|
|
291
|
+
cache.transpileCache,
|
|
292
|
+
);
|
|
197
293
|
const AsyncFunction = Object.getPrototypeOf(
|
|
198
294
|
async function () {},
|
|
199
295
|
).constructor;
|
|
@@ -232,6 +328,7 @@ ${codeWithoutImports}
|
|
|
232
328
|
depth + 1,
|
|
233
329
|
log,
|
|
234
330
|
options,
|
|
331
|
+
cache,
|
|
235
332
|
);
|
|
236
333
|
|
|
237
334
|
const nodesToInsert = Array.from((div as any).childNodes || []).map(
|
|
@@ -266,6 +363,7 @@ ${codeWithoutImports}
|
|
|
266
363
|
depth,
|
|
267
364
|
log,
|
|
268
365
|
options,
|
|
366
|
+
cache,
|
|
269
367
|
);
|
|
270
368
|
hasChanges = hasChanges || childChanged;
|
|
271
369
|
}
|
package/src/processPre.ts
CHANGED
|
@@ -4,8 +4,14 @@ import type { Logger } from "@tkeron/tools";
|
|
|
4
4
|
import { silentLogger } from "@tkeron/tools";
|
|
5
5
|
|
|
6
6
|
const packageJsonPath = join(import.meta.dir, "..", "package.json");
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
|
|
8
|
+
let _tkeronVersion: string | null = null;
|
|
9
|
+
const getTkeronVersion = async (): Promise<string> => {
|
|
10
|
+
if (_tkeronVersion) return _tkeronVersion;
|
|
11
|
+
const packageJson = await Bun.file(packageJsonPath).json();
|
|
12
|
+
_tkeronVersion = packageJson.version;
|
|
13
|
+
return _tkeronVersion!;
|
|
14
|
+
};
|
|
9
15
|
|
|
10
16
|
const HTML_PARSER_PATH = import.meta.resolve("@tkeron/html-parser");
|
|
11
17
|
|
|
@@ -40,6 +46,8 @@ export const processPre = async (
|
|
|
40
46
|
|
|
41
47
|
const hasChanges = preFiles.length > 0;
|
|
42
48
|
|
|
49
|
+
const TKERON_VERSION = hasChanges ? await getTkeronVersion() : "";
|
|
50
|
+
|
|
43
51
|
for (const preFile of preFiles) {
|
|
44
52
|
const htmlFile = preFile.replace(/\.pre\.ts$/, ".html");
|
|
45
53
|
|