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 CHANGED
@@ -5,7 +5,7 @@
5
5
  "": {
6
6
  "name": "tkeron",
7
7
  "dependencies": {
8
- "@modelcontextprotocol/sdk": "^1.26.0",
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.26.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-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="],
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,12 @@
1
+ <div
2
+ style="
3
+ background: #f0f9ff;
4
+ border: 1px solid #0284c7;
5
+ border-radius: 8px;
6
+ padding: 16px;
7
+ margin: 16px 0;
8
+ "
9
+ >
10
+ <h3 class="title" style="margin-top: 0; color: #0284c7"></h3>
11
+ <ul class="items"></ul>
12
+ </div>
@@ -0,0 +1,3 @@
1
+ const title = com.getAttribute("data-title") || "Info";
2
+ const titleEl = com.querySelector(".title");
3
+ if (titleEl) titleEl.textContent = title;
@@ -0,0 +1,12 @@
1
+ <div
2
+ style="
3
+ border: 2px solid #333;
4
+ border-radius: 8px;
5
+ padding: 16px;
6
+ margin: 16px 0;
7
+ max-width: 300px;
8
+ "
9
+ >
10
+ <h3 class="name" style="margin-top: 0; color: #2563eb"></h3>
11
+ <p class="role"></p>
12
+ </div>
@@ -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.1",
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
- "build_examples": "bun run sample1 && bun run sample2 && bun run sample3 && bun run sample4 && bun run sample5 && bun run sample6 && bun run sample7"
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.26.0",
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
- let hasChanges = false;
35
-
36
- for (const htmlFile of htmlFiles) {
37
- const htmlContent = await Bun.file(htmlFile).text();
38
- const document = parseHTML(ensureHtmlDocument(htmlContent));
39
-
40
- const htmlElement =
41
- document.querySelector("html") || document.documentElement;
42
- if (htmlElement) {
43
- const changed = await processComponents(
44
- htmlElement,
45
- dirname(htmlFile),
46
- tempDir,
47
- [],
48
- 0,
49
- log,
50
- );
51
- hasChanges = hasChanges || changed;
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
- const output =
55
- DOCTYPE +
56
- (htmlElement?.outerHTML || document.documentElement?.outerHTML || "");
57
- await Bun.write(htmlFile, output);
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 hasChanges;
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 localPath = join(currentDir, `${tagName}.com.html`);
86
- const globMatches = getFilePaths(
87
- rootDir,
88
- `**/${tagName}.com.html`,
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 localPath = join(currentDir, `${tagName}.com.html`);
108
-
109
- let componentPath: string | null = null;
110
-
111
- if (await Bun.file(localPath).exists()) {
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 Bun.file(componentPath).text();
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
  }
@@ -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
- let hasChanges = false;
35
-
36
- for (const htmlFile of htmlFiles) {
37
- const htmlContent = await Bun.file(htmlFile).text();
38
- const document = parseHTML(ensureHtmlDocument(htmlContent));
39
-
40
- const htmlElement =
41
- document.querySelector("html") || document.documentElement;
42
- if (htmlElement) {
43
- const changed = await processComponents(
44
- htmlElement,
45
- dirname(htmlFile),
46
- tempDir,
47
- [],
48
- 0,
49
- log,
50
- );
51
- hasChanges = hasChanges || changed;
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
- const output =
55
- DOCTYPE +
56
- (htmlElement?.outerHTML || document.documentElement?.outerHTML || "");
57
- await Bun.write(htmlFile, output);
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 hasChanges;
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 localPath = join(currentDir, `${tagName}.com.md`);
86
- const globMatches = getFilePaths(rootDir, `**/${tagName}.com.md`, true);
87
- 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.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 localPath = join(currentDir, `${tagName}.com.md`);
104
-
105
- let componentPath: string | null = null;
106
-
107
- if (await Bun.file(localPath).exists()) {
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 Bun.file(componentPath).text();
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
  }
@@ -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
- let hasChanges = false;
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
- for (const htmlFile of htmlFiles) {
37
- const htmlContent = await Bun.file(htmlFile).text();
38
- const document = parseHTML(ensureHtmlDocument(htmlContent));
39
-
40
- const htmlElement =
41
- document.querySelector("html") || document.documentElement;
42
- if (htmlElement) {
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
- const output =
56
- DOCTYPE +
57
- (htmlElement?.outerHTML || document.documentElement?.outerHTML || "");
58
- await Bun.write(htmlFile, output);
59
- }
126
+ return results.some(Boolean);
127
+ };
60
128
 
61
- return hasChanges;
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 localPath = join(currentDir, `${tagName}.com.ts`);
88
- const globMatches = getFilePaths(rootDir, `**/${tagName}.com.ts`, true);
89
- if ((await Bun.file(localPath).exists()) || globMatches.length > 0) {
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 localPath = join(currentDir, `${tagName}.com.ts`);
106
-
107
- let componentPath: string | null = null;
108
-
109
- if (await Bun.file(localPath).exists()) {
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 Bun.file(componentPath).text();
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 fs = await import("fs/promises");
191
- await fs.unlink(tempPath);
284
+ const { unlink } = await import("fs/promises");
285
+ await unlink(tempPath);
192
286
  } catch {}
193
287
  }
194
288
  } else {
195
- const transpiler = new Bun.Transpiler({ loader: "ts" });
196
- const jsCode = transpiler.transformSync(originalComponentCode);
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
- const packageJson = await Bun.file(packageJsonPath).json();
8
- const TKERON_VERSION = packageJson.version;
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