tkeron 4.0.0-beta.16 → 4.0.0-beta.18
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/changelog.md +70 -0
- package/docs/mcp-server.md +5 -25
- package/docs/testing.md +331 -0
- package/mcp-server.ts +147 -446
- package/package.json +1 -1
- package/src/build.ts +3 -11
- package/src/develop.ts +6 -23
- package/src/init.ts +1 -1
- package/src/initWrapper.ts +2 -2
- package/src/processCom.ts +3 -4
- package/src/processComTs.ts +3 -4
- package/src/processPre.ts +1 -1
- package/tests/test-helpers.ts +2 -2
package/changelog.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
+
# v4.0.0-beta.18
|
|
2
|
+
|
|
3
|
+
## Code Quality & Architecture
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- **Fixed**: Removed `process.exit()` from library code (`build.ts`, `develop.ts`, `init.ts`) — errors now propagate via `throw`, letting wrappers handle exit
|
|
8
|
+
- **Fixed**: Moved `reloadClients` Set inside `develop()` to prevent shared state across multiple server instances
|
|
9
|
+
- **Fixed**: Removed `await` on synchronous `path.resolve()` calls in `build.ts` and `develop.ts`
|
|
10
|
+
- **Fixed**: `log.error()` called without arguments in `processPre.ts` — now passes empty string
|
|
11
|
+
|
|
12
|
+
### Code Style
|
|
13
|
+
|
|
14
|
+
- **Changed**: Converted all `function` declarations to arrow functions (`initWrapper`, `processComponents`, `processComponentsTs`)
|
|
15
|
+
- **Removed**: Comments in source code (`processCom.ts`, `processComTs.ts`)
|
|
16
|
+
- **Removed**: Dead re-export of `setupSigintHandler` from `develop.ts`
|
|
17
|
+
|
|
18
|
+
### Tests
|
|
19
|
+
|
|
20
|
+
- **Fixed**: TS2532 errors in `getBuildResult.test.ts` (proper handling of possibly undefined values)
|
|
21
|
+
- **Updated**: Tests adapted to new error propagation (no more `process.exit` mocking in library tests)
|
|
22
|
+
- **Removed**: Redundant/dead test cases for removed behavior
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# v4.0.0-beta.17
|
|
27
|
+
|
|
28
|
+
## MCP Server Updates
|
|
29
|
+
|
|
30
|
+
### API Migration
|
|
31
|
+
|
|
32
|
+
- **Changed**: Migrated MCP server to new SDK API (`McpServer`, `registerResource`, `registerTool`)
|
|
33
|
+
- **Removed**: Legacy API (`Server`, `setRequestHandler`)
|
|
34
|
+
- **Benefit**: Better compatibility with MCP SDK 1.26.0, cleaner code structure
|
|
35
|
+
|
|
36
|
+
### Tools Simplification
|
|
37
|
+
|
|
38
|
+
- **Removed**: `tkeron_init`, `tkeron_build`, `tkeron_develop` tools
|
|
39
|
+
- **Rationale**: MCP server now focuses on documentation and examples only
|
|
40
|
+
- **Alternative**: AI agents should use terminal commands (`tk init`, `tk build`, `tk dev`) instead
|
|
41
|
+
- **Kept**: `list_examples` and `get_example` tools (read-only)
|
|
42
|
+
- **Annotations**: Added `readOnlyHint: true` to remaining tools
|
|
43
|
+
|
|
44
|
+
### New Testing Documentation
|
|
45
|
+
|
|
46
|
+
- **New**: `tkeron://testing` resource with comprehensive testing guide
|
|
47
|
+
- **Location**: `docs/testing.md`
|
|
48
|
+
- **Coverage**:
|
|
49
|
+
- How to use `getBuildResult()` for testing tkeron projects
|
|
50
|
+
- Proper test structure with `beforeAll()` for concurrent safety
|
|
51
|
+
- Testing patterns: file existence, DOM elements, bounded content, transformations
|
|
52
|
+
- Complete examples with best practices
|
|
53
|
+
- Rules summary (8 key rules)
|
|
54
|
+
- **Purpose**: Help users write reliable tests for their tkeron projects
|
|
55
|
+
|
|
56
|
+
### Documentation Updates
|
|
57
|
+
|
|
58
|
+
- **Updated**: `docs/mcp-server.md` to reflect new tools approach
|
|
59
|
+
- **Clarified**: AI agents can access examples but must use terminal for project operations
|
|
60
|
+
- **Updated**: MCP server tests to include new `testing` resource
|
|
61
|
+
|
|
62
|
+
### Tests
|
|
63
|
+
|
|
64
|
+
- **New**: `examples/basic_build/basic_build.test.ts` - complete test suite demonstrating testing patterns
|
|
65
|
+
- **Purpose**: Reference implementation for testing tkeron projects
|
|
66
|
+
|
|
67
|
+
## Dependency Updates
|
|
68
|
+
|
|
69
|
+
- **Updated**: `@modelcontextprotocol/sdk` from `^1.25.3` to `^1.26.0`
|
|
70
|
+
|
|
1
71
|
# v4.0.0-beta.16
|
|
2
72
|
|
|
3
73
|
## Improvements
|
package/docs/mcp-server.md
CHANGED
|
@@ -99,6 +99,7 @@ The MCP server exposes the following documentation resources:
|
|
|
99
99
|
| `tkeron://pre-rendering` | Transform HTML at build time with .pre.ts files |
|
|
100
100
|
| `tkeron://cli-reference` | Complete command-line interface documentation |
|
|
101
101
|
| `tkeron://best-practices` | Patterns, anti-patterns, and limitations |
|
|
102
|
+
| `tkeron://testing` | How to test tkeron projects with getBuildResult() |
|
|
102
103
|
|
|
103
104
|
## What AI Agents Can Do
|
|
104
105
|
|
|
@@ -111,25 +112,19 @@ With access to the MCP server, AI agents can:
|
|
|
111
112
|
- ✅ Provide accurate CLI commands and options
|
|
112
113
|
- ✅ Debug common issues with components and pre-rendering
|
|
113
114
|
- ✅ Recommend best practices and avoid anti-patterns
|
|
114
|
-
- ✅ **Initialize new Tkeron projects directly**
|
|
115
|
-
- ✅ **Build and develop Tkeron projects**
|
|
116
115
|
- ✅ **List and explore example projects**
|
|
117
116
|
- ✅ **Retrieve example code and structures**
|
|
117
|
+
- ✅ **Initialize, build, and develop projects via terminal** (`tk init`, `tk build`, `tk dev`)
|
|
118
118
|
|
|
119
119
|
## Available Tools
|
|
120
120
|
|
|
121
121
|
The MCP server provides the following tools for AI agents:
|
|
122
122
|
|
|
123
|
-
### Documentation Tool
|
|
124
|
-
|
|
125
|
-
- **`get_tkeron_docs`**: Retrieve specific documentation topics
|
|
126
|
-
- Parameters: `topic` (overview, getting-started, components-html, etc.)
|
|
127
|
-
- Returns the full markdown content of the requested documentation
|
|
128
|
-
|
|
129
123
|
### Example Management Tools
|
|
130
124
|
|
|
131
125
|
- **`list_examples`**: List all available Tkeron example projects
|
|
132
|
-
-
|
|
126
|
+
- No parameters required
|
|
127
|
+
- Returns example names, descriptions, and paths
|
|
133
128
|
- Helps users discover what's possible with Tkeron
|
|
134
129
|
|
|
135
130
|
- **`get_example`**: Get the complete source code of an example
|
|
@@ -137,22 +132,7 @@ The MCP server provides the following tools for AI agents:
|
|
|
137
132
|
- Returns file structure and all source code
|
|
138
133
|
- Useful for learning patterns and bootstrapping projects
|
|
139
134
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
- **`tkeron_init`**: Initialize a new Tkeron project
|
|
143
|
-
- Parameters: `projectPath` (absolute path), `force` (optional boolean)
|
|
144
|
-
- Creates the standard Tkeron structure (websrc/ with sample files)
|
|
145
|
-
- AI agents can create projects without user intervention
|
|
146
|
-
|
|
147
|
-
- **`tkeron_build`**: Build a Tkeron project
|
|
148
|
-
- Parameters: `sourceDir`, `targetDir` (optional)
|
|
149
|
-
- Processes all `.pre.ts`, `.com.html`, and `.com.ts` files
|
|
150
|
-
- Generates production-ready output
|
|
151
|
-
|
|
152
|
-
- **`tkeron_develop`**: Start the development server
|
|
153
|
-
- Parameters: `sourceDir`, `outputDir`, `port`, `host` (all optional)
|
|
154
|
-
- Launches hot-reload development server
|
|
155
|
-
- AI agents can test projects immediately after creation
|
|
135
|
+
> **Note:** The MCP server only provides documentation and examples. It does NOT modify files. The AI agent should use terminal commands (`tk init`, `tk build`, `tk dev`) to manage projects.
|
|
156
136
|
|
|
157
137
|
## Benefits
|
|
158
138
|
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Testing Tkeron Projects
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Tkeron provides a `getBuildResult()` function that lets you test your built projects programmatically. This is useful for verifying that your components, pre-rendering scripts, and build output work correctly.
|
|
6
|
+
|
|
7
|
+
**Important:** These tests validate YOUR project's build output — not tkeron internals. You're testing that your HTML, components, and scripts produce the expected result after the build.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
Install tkeron as a dependency in your project:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun add -d tkeron
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { getBuildResult, type BuildResult } from "tkeron";
|
|
21
|
+
|
|
22
|
+
const result: BuildResult = await getBuildResult("./websrc");
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### BuildResult
|
|
26
|
+
|
|
27
|
+
`getBuildResult()` returns a `Record<string, FileInfo>` indexed by relative path:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
type BuildResult = Record<string, FileInfo>;
|
|
31
|
+
|
|
32
|
+
interface FileInfo {
|
|
33
|
+
fileName: string; // "index.html"
|
|
34
|
+
filePath: string; // "styles" or "" for root files
|
|
35
|
+
path: string; // "styles/main.css" (the key)
|
|
36
|
+
type: string; // MIME type: "text/html", "text/css", etc.
|
|
37
|
+
size: number; // size in bytes
|
|
38
|
+
fileHash: string; // SHA-256 hash
|
|
39
|
+
getContentAsString?: () => string; // text files only
|
|
40
|
+
dom?: Document; // HTML files only - parsed DOM
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `dom` is only present on `.html` files. It provides full DOM API: `querySelector()`, `getElementById()`, `querySelectorAll()`, etc.
|
|
45
|
+
- `getContentAsString()` is only present on text files (html, css, js, json, txt, svg, xml, ts, mjs, cjs, md, map).
|
|
46
|
+
- Binary files (images, fonts) only have metadata — no `dom` or `getContentAsString`.
|
|
47
|
+
|
|
48
|
+
## Test Structure
|
|
49
|
+
|
|
50
|
+
### Single Build with beforeAll
|
|
51
|
+
|
|
52
|
+
**Always** build once and share the result across tests. This is:
|
|
53
|
+
|
|
54
|
+
- Concurrent-safe (`bun test --concurrent`)
|
|
55
|
+
- Efficient (1 build instead of N builds)
|
|
56
|
+
- Reliable (no race conditions)
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { describe, it, expect, beforeAll } from "bun:test";
|
|
60
|
+
import { getBuildResult, type BuildResult } from "tkeron";
|
|
61
|
+
import { join } from "path";
|
|
62
|
+
|
|
63
|
+
describe("my project", () => {
|
|
64
|
+
const sourcePath = join(import.meta.dir, "src");
|
|
65
|
+
let result: BuildResult;
|
|
66
|
+
|
|
67
|
+
beforeAll(async () => {
|
|
68
|
+
result = await getBuildResult(sourcePath);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should generate index.html", () => {
|
|
72
|
+
expect(result?.["index.html"]).toBeDefined();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Never** call `getBuildResult()` inside each `it()` — this causes race conditions in concurrent mode.
|
|
78
|
+
|
|
79
|
+
## Testing Patterns
|
|
80
|
+
|
|
81
|
+
### 1. Verify File Existence
|
|
82
|
+
|
|
83
|
+
Check that expected output files exist:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
it("should generate expected files", () => {
|
|
87
|
+
expect(result?.["index.html"]).toBeDefined();
|
|
88
|
+
expect(result?.["index.js"]).toBeDefined();
|
|
89
|
+
expect(result?.["styles/main.css"]).toBeDefined();
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 2. Verify Element Existence (Always Check First)
|
|
94
|
+
|
|
95
|
+
**Rule:** Always verify an element exists BEFORE checking its properties.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// ✅ CORRECT: check existence first, then properties
|
|
99
|
+
it("should have login button", () => {
|
|
100
|
+
const dom = result?.["index.html"]?.dom;
|
|
101
|
+
|
|
102
|
+
const button = dom?.querySelector("#login-button");
|
|
103
|
+
expect(button).toBeDefined();
|
|
104
|
+
expect(button!.textContent).toBe("Log In");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ❌ WRONG: accessing properties without checking existence
|
|
108
|
+
it("should have login button", () => {
|
|
109
|
+
const dom = result?.["index.html"]?.dom;
|
|
110
|
+
expect(dom!.querySelector("#login-button")!.textContent).toBe("Log In");
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 3. Verify Bounded Content
|
|
115
|
+
|
|
116
|
+
Test specific, short, predictable values — not full output strings.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// ✅ GOOD: specific, bounded values
|
|
120
|
+
it("should have correct title", () => {
|
|
121
|
+
const dom = result?.["index.html"]?.dom;
|
|
122
|
+
|
|
123
|
+
const title = dom?.querySelector("title");
|
|
124
|
+
expect(title).toBeDefined();
|
|
125
|
+
expect(title!.textContent).toBe("My App");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ✅ GOOD: keyword presence in compiled JS
|
|
129
|
+
it("should contain event handling logic", () => {
|
|
130
|
+
const js = result?.["index.js"]?.getContentAsString!();
|
|
131
|
+
expect(js).toContain("addEventListener");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ❌ BAD: comparing full minified JS output (fragile, breaks on any optimization change)
|
|
135
|
+
it("should match exact JS", () => {
|
|
136
|
+
const js = result?.["index.js"]?.getContentAsString!();
|
|
137
|
+
expect(js).toBe('var t=0,e=document.querySelector("button")...');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ❌ BAD: comparing full HTML output
|
|
141
|
+
it("should match exact HTML", () => {
|
|
142
|
+
const html = result?.["index.html"]?.getContentAsString!();
|
|
143
|
+
expect(html).toBe("<!doctype html><html>...");
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Why bounded:** Build output changes with optimizations, minification, and bundling. Test the **meaning**, not the **exact format**.
|
|
148
|
+
|
|
149
|
+
### 4. Verify DOM Elements (Specific Queries)
|
|
150
|
+
|
|
151
|
+
Test specific elements that matter to your project:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
it("should have navigation with correct links", () => {
|
|
155
|
+
const dom = result?.["index.html"]?.dom;
|
|
156
|
+
|
|
157
|
+
const homeLink = dom?.querySelector('nav a[href="/"]');
|
|
158
|
+
expect(homeLink).toBeDefined();
|
|
159
|
+
expect(homeLink!.textContent).toBe("Home");
|
|
160
|
+
|
|
161
|
+
const aboutLink = dom?.querySelector('nav a[href="/about"]');
|
|
162
|
+
expect(aboutLink).toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should have user profile section", () => {
|
|
166
|
+
const dom = result?.["index.html"]?.dom;
|
|
167
|
+
|
|
168
|
+
const profile = dom?.querySelector("#user-profile");
|
|
169
|
+
expect(profile).toBeDefined();
|
|
170
|
+
|
|
171
|
+
const avatar = profile?.querySelector("img.avatar");
|
|
172
|
+
expect(avatar).toBeDefined();
|
|
173
|
+
expect(avatar!.getAttribute("alt")).toBe("User avatar");
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 5. Verify Tkeron Transformations
|
|
178
|
+
|
|
179
|
+
Test that tkeron's build features work correctly in your project:
|
|
180
|
+
|
|
181
|
+
**Script injection (TypeScript → JavaScript):**
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
it("should have compiled script in head", () => {
|
|
185
|
+
const dom = result?.["index.html"]?.dom;
|
|
186
|
+
|
|
187
|
+
const script = dom?.querySelector('script[type="module"]');
|
|
188
|
+
expect(script).toBeDefined();
|
|
189
|
+
expect(script!.getAttribute("src")).toBe("./index.js");
|
|
190
|
+
expect(script!.parentElement!.tagName).toBe("HEAD");
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Component substitution (.com.html):**
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
it("should have processed nav component", () => {
|
|
198
|
+
const dom = result?.["index.html"]?.dom;
|
|
199
|
+
|
|
200
|
+
// The <nav-menu> tag should be replaced by its .com.html content
|
|
201
|
+
const nav = dom?.querySelector("nav");
|
|
202
|
+
expect(nav).toBeDefined();
|
|
203
|
+
|
|
204
|
+
const links = dom?.querySelectorAll("nav a");
|
|
205
|
+
expect(links).toBeDefined();
|
|
206
|
+
expect(links!.length).toBeGreaterThan(0);
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**TypeScript component (.com.ts):**
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
it("should have dynamically generated list", () => {
|
|
214
|
+
const dom = result?.["index.html"]?.dom;
|
|
215
|
+
|
|
216
|
+
const list = dom?.querySelector("#item-list");
|
|
217
|
+
expect(list).toBeDefined();
|
|
218
|
+
|
|
219
|
+
const items = list?.querySelectorAll("li");
|
|
220
|
+
expect(items).toBeDefined();
|
|
221
|
+
expect(items!.length).toBe(5);
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Pre-rendering (.pre.ts):**
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
it("should have pre-rendered content", () => {
|
|
229
|
+
const dom = result?.["index.html"]?.dom;
|
|
230
|
+
|
|
231
|
+
const generated = dom?.querySelector("#generated-content");
|
|
232
|
+
expect(generated).toBeDefined();
|
|
233
|
+
expect(generated!.textContent).not.toBe("");
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 6. Verify File Metadata
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
it("should have correct metadata", () => {
|
|
241
|
+
expect(result?.["index.html"]?.type).toBe("text/html");
|
|
242
|
+
expect(result?.["index.html"]?.size).toBeGreaterThan(0);
|
|
243
|
+
expect(result?.["index.html"]?.fileHash).toBeDefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should not have DOM on non-HTML files", () => {
|
|
247
|
+
expect(result?.["index.js"]?.dom).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should have correct path info for nested files", () => {
|
|
251
|
+
expect(result?.["pages/about.html"]?.fileName).toBe("about.html");
|
|
252
|
+
expect(result?.["pages/about.html"]?.filePath).toBe("pages");
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Complete Example
|
|
257
|
+
|
|
258
|
+
Here's a complete test file for a tkeron project with components:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { describe, it, expect, beforeAll } from "bun:test";
|
|
262
|
+
import { getBuildResult, type BuildResult } from "tkeron";
|
|
263
|
+
import { join } from "path";
|
|
264
|
+
|
|
265
|
+
describe("my-website build", () => {
|
|
266
|
+
const sourcePath = join(import.meta.dir, "websrc");
|
|
267
|
+
let result: BuildResult;
|
|
268
|
+
|
|
269
|
+
beforeAll(async () => {
|
|
270
|
+
result = await getBuildResult(sourcePath);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("should generate expected files", () => {
|
|
274
|
+
expect(result?.["index.html"]).toBeDefined();
|
|
275
|
+
expect(result?.["index.js"]).toBeDefined();
|
|
276
|
+
expect(result?.["about.html"]).toBeDefined();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("should have page title", () => {
|
|
280
|
+
const dom = result?.["index.html"]?.dom;
|
|
281
|
+
|
|
282
|
+
const title = dom?.querySelector("title");
|
|
283
|
+
expect(title).toBeDefined();
|
|
284
|
+
expect(title!.textContent).toBe("My Website");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should have navigation from component", () => {
|
|
288
|
+
const dom = result?.["index.html"]?.dom;
|
|
289
|
+
|
|
290
|
+
const nav = dom?.querySelector("nav");
|
|
291
|
+
expect(nav).toBeDefined();
|
|
292
|
+
|
|
293
|
+
const homeLink = dom?.querySelector('nav a[href="/"]');
|
|
294
|
+
expect(homeLink).toBeDefined();
|
|
295
|
+
expect(homeLink!.textContent).toBe("Home");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should have footer from component", () => {
|
|
299
|
+
const dom = result?.["index.html"]?.dom;
|
|
300
|
+
|
|
301
|
+
const footer = dom?.querySelector("footer");
|
|
302
|
+
expect(footer).toBeDefined();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("should have compiled JavaScript", () => {
|
|
306
|
+
const js = result?.["index.js"]?.getContentAsString!();
|
|
307
|
+
|
|
308
|
+
expect(js).toBeDefined();
|
|
309
|
+
expect(js!.length).toBeGreaterThan(0);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should have script injected in head", () => {
|
|
313
|
+
const dom = result?.["index.html"]?.dom;
|
|
314
|
+
|
|
315
|
+
const script = dom?.querySelector('head script[type="module"]');
|
|
316
|
+
expect(script).toBeDefined();
|
|
317
|
+
expect(script!.getAttribute("src")).toBe("./index.js");
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Rules Summary
|
|
323
|
+
|
|
324
|
+
1. **Build once** with `beforeAll()` — never inside individual tests
|
|
325
|
+
2. **Check existence first** — always `expect(element).toBeDefined()` before accessing properties
|
|
326
|
+
3. **Use optional chaining** (`?.`) for safe property access on result entries
|
|
327
|
+
4. **Test bounded content** — specific values, keywords, attributes; not full output strings
|
|
328
|
+
5. **Test specific elements** — query for elements that matter to your project
|
|
329
|
+
6. **Verify transformations** — components processed, scripts injected, pre-rendering applied
|
|
330
|
+
7. **Don't test the parser** — you're testing your project, not tkeron internals
|
|
331
|
+
8. **Concurrent-safe** — tests must work with `bun test --concurrent`
|
package/mcp-server.ts
CHANGED
|
@@ -1,57 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
5
|
-
ListResourcesRequestSchema,
|
|
6
|
-
ReadResourceRequestSchema,
|
|
7
|
-
ListToolsRequestSchema,
|
|
8
|
-
CallToolRequestSchema,
|
|
9
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
4
|
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
11
5
|
import { join, dirname } from "path";
|
|
12
6
|
import { fileURLToPath } from "url";
|
|
13
|
-
import { init } from "./src/init";
|
|
14
|
-
import { build } from "./src/build";
|
|
15
|
-
import { develop } from "./src/develop";
|
|
16
7
|
|
|
17
|
-
|
|
18
|
-
const getDocsDir = () => {
|
|
19
|
-
// Try import.meta.dir first (Bun specific)
|
|
8
|
+
const getBaseDir = () => {
|
|
20
9
|
if (import.meta.dir) {
|
|
21
|
-
|
|
22
|
-
if (existsSync(docsPath)) {
|
|
23
|
-
return docsPath;
|
|
24
|
-
}
|
|
10
|
+
return import.meta.dir;
|
|
25
11
|
}
|
|
26
|
-
|
|
27
|
-
// Fallback: resolve from the actual file location
|
|
28
12
|
const currentFile = import.meta.url
|
|
29
13
|
? fileURLToPath(import.meta.url)
|
|
30
14
|
: __filename;
|
|
31
|
-
|
|
32
|
-
return join(currentDir, "docs");
|
|
15
|
+
return dirname(currentFile);
|
|
33
16
|
};
|
|
34
17
|
|
|
35
|
-
const
|
|
18
|
+
const BASE_DIR = getBaseDir();
|
|
19
|
+
const DOCS_DIR = join(BASE_DIR, "docs");
|
|
20
|
+
const EXAMPLES_DIR = join(BASE_DIR, "examples");
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
return examplesPath;
|
|
43
|
-
}
|
|
22
|
+
const getVersion = (): string => {
|
|
23
|
+
const pkgPath = join(BASE_DIR, "package.json");
|
|
24
|
+
if (existsSync(pkgPath)) {
|
|
25
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
26
|
+
return pkg.version || "0.0.0";
|
|
44
27
|
}
|
|
45
|
-
|
|
46
|
-
const currentFile = import.meta.url
|
|
47
|
-
? fileURLToPath(import.meta.url)
|
|
48
|
-
: __filename;
|
|
49
|
-
const currentDir = dirname(currentFile);
|
|
50
|
-
return join(currentDir, "examples");
|
|
28
|
+
return "0.0.0";
|
|
51
29
|
};
|
|
52
30
|
|
|
53
|
-
const EXAMPLES_DIR = getExamplesDir();
|
|
54
|
-
|
|
55
31
|
const DOCS = [
|
|
56
32
|
{
|
|
57
33
|
uri: "tkeron://overview",
|
|
@@ -95,466 +71,191 @@ const DOCS = [
|
|
|
95
71
|
description: "Patterns, anti-patterns, and limitations",
|
|
96
72
|
file: "best-practices.md",
|
|
97
73
|
},
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
const server = new Server(
|
|
101
74
|
{
|
|
102
|
-
|
|
103
|
-
|
|
75
|
+
uri: "tkeron://testing",
|
|
76
|
+
name: "Testing Tkeron Projects",
|
|
77
|
+
description:
|
|
78
|
+
"How to test tkeron projects using getBuildResult(), DOM assertions, and bounded content verification",
|
|
79
|
+
file: "testing.md",
|
|
104
80
|
},
|
|
105
|
-
|
|
106
|
-
capabilities: {
|
|
107
|
-
resources: {},
|
|
108
|
-
tools: {},
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
114
|
-
return {
|
|
115
|
-
resources: DOCS.map((doc) => ({
|
|
116
|
-
uri: doc.uri,
|
|
117
|
-
name: doc.name,
|
|
118
|
-
description: doc.description,
|
|
119
|
-
mimeType: "text/markdown",
|
|
120
|
-
})),
|
|
121
|
-
};
|
|
122
|
-
});
|
|
123
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
124
|
-
try {
|
|
125
|
-
const uri = request.params.uri;
|
|
126
|
-
const doc = DOCS.find((d) => d.uri === uri);
|
|
127
|
-
|
|
128
|
-
if (!doc) {
|
|
129
|
-
throw new Error(`Unknown resource: ${uri}`);
|
|
130
|
-
}
|
|
81
|
+
];
|
|
131
82
|
|
|
132
|
-
|
|
83
|
+
const EXAMPLE_DESCRIPTIONS: { [key: string]: string } = {
|
|
84
|
+
init_sample:
|
|
85
|
+
"Complete starter template with all features (used by 'tk init')",
|
|
86
|
+
basic_build: "Simple HTML + TypeScript bundling",
|
|
87
|
+
with_assets: "HTML in multiple directories with asset references",
|
|
88
|
+
with_pre: "Pre-rendering HTML at build time with .pre.ts files",
|
|
89
|
+
with_com_html_priority:
|
|
90
|
+
"Static HTML components (.com.html) with local override priority",
|
|
91
|
+
with_com_ts: "TypeScript components (.com.ts) generating HTML at build time",
|
|
92
|
+
with_com_ts_priority:
|
|
93
|
+
"TypeScript components (.com.ts) with local override priority",
|
|
94
|
+
with_com_mixed_priority:
|
|
95
|
+
"Mixed .com.html and .com.ts showing processing order",
|
|
96
|
+
with_component_iteration:
|
|
97
|
+
"Component iteration: .com.ts with logic, .com.html for templates",
|
|
98
|
+
};
|
|
133
99
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
100
|
+
const readDirRecursive = (
|
|
101
|
+
dir: string,
|
|
102
|
+
baseDir: string = dir,
|
|
103
|
+
): { [key: string]: string } => {
|
|
104
|
+
const files: { [key: string]: string } = {};
|
|
105
|
+
const items = readdirSync(dir);
|
|
106
|
+
|
|
107
|
+
for (const item of items) {
|
|
108
|
+
const fullPath = join(dir, item);
|
|
109
|
+
const relativePath = fullPath.substring(baseDir.length + 1);
|
|
110
|
+
|
|
111
|
+
if (statSync(fullPath).isDirectory()) {
|
|
112
|
+
Object.assign(files, readDirRecursive(fullPath, baseDir));
|
|
113
|
+
} else {
|
|
114
|
+
files[relativePath] = readFileSync(fullPath, "utf-8");
|
|
137
115
|
}
|
|
116
|
+
}
|
|
138
117
|
|
|
139
|
-
|
|
118
|
+
return files;
|
|
119
|
+
};
|
|
140
120
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
uri: doc.uri,
|
|
145
|
-
mimeType: "text/markdown",
|
|
146
|
-
text: content,
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
};
|
|
150
|
-
} catch (error) {
|
|
151
|
-
throw error;
|
|
152
|
-
}
|
|
121
|
+
const server = new McpServer({
|
|
122
|
+
name: "tkeron-mcp",
|
|
123
|
+
version: getVersion(),
|
|
153
124
|
});
|
|
154
125
|
|
|
155
|
-
|
|
156
|
-
server.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"Get Tkeron documentation. Available topics: overview, getting-started, components-html, components-typescript, pre-rendering, cli-reference, best-practices",
|
|
163
|
-
inputSchema: {
|
|
164
|
-
type: "object",
|
|
165
|
-
properties: {
|
|
166
|
-
topic: {
|
|
167
|
-
type: "string",
|
|
168
|
-
description: "Documentation topic to retrieve",
|
|
169
|
-
enum: [
|
|
170
|
-
"overview",
|
|
171
|
-
"getting-started",
|
|
172
|
-
"components-html",
|
|
173
|
-
"components-typescript",
|
|
174
|
-
"pre-rendering",
|
|
175
|
-
"cli-reference",
|
|
176
|
-
"best-practices",
|
|
177
|
-
],
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
required: ["topic"],
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
name: "list_examples",
|
|
185
|
-
description:
|
|
186
|
-
"List all available Tkeron example projects with their descriptions",
|
|
187
|
-
inputSchema: {
|
|
188
|
-
type: "object",
|
|
189
|
-
properties: {},
|
|
190
|
-
required: [],
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
name: "get_example",
|
|
195
|
-
description:
|
|
196
|
-
"Get the source code and structure of a specific Tkeron example project",
|
|
197
|
-
inputSchema: {
|
|
198
|
-
type: "object",
|
|
199
|
-
properties: {
|
|
200
|
-
example: {
|
|
201
|
-
type: "string",
|
|
202
|
-
description:
|
|
203
|
-
"Example name (e.g., 'basic_build', 'with_component_iteration')",
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
required: ["example"],
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
{
|
|
210
|
-
name: "tkeron_init",
|
|
211
|
-
description:
|
|
212
|
-
"Initialize a new Tkeron project with the standard structure (websrc/ directory with sample files)",
|
|
213
|
-
inputSchema: {
|
|
214
|
-
type: "object",
|
|
215
|
-
properties: {
|
|
216
|
-
projectPath: {
|
|
217
|
-
type: "string",
|
|
218
|
-
description: "Absolute path where the project will be created",
|
|
219
|
-
},
|
|
220
|
-
force: {
|
|
221
|
-
type: "boolean",
|
|
222
|
-
description: "Overwrite existing files if they exist",
|
|
223
|
-
default: false,
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
required: ["projectPath"],
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
name: "tkeron_build",
|
|
231
|
-
description:
|
|
232
|
-
"Build a Tkeron project (processes .pre.ts, .com.html, and .com.ts files)",
|
|
233
|
-
inputSchema: {
|
|
234
|
-
type: "object",
|
|
235
|
-
properties: {
|
|
236
|
-
sourceDir: {
|
|
237
|
-
type: "string",
|
|
238
|
-
description:
|
|
239
|
-
"Absolute path to source directory (default: websrc/)",
|
|
240
|
-
},
|
|
241
|
-
targetDir: {
|
|
242
|
-
type: "string",
|
|
243
|
-
description: "Absolute path to output directory (default: web/)",
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
required: [],
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
name: "tkeron_develop",
|
|
251
|
-
description: "Start Tkeron development server with hot reload",
|
|
252
|
-
inputSchema: {
|
|
253
|
-
type: "object",
|
|
254
|
-
properties: {
|
|
255
|
-
sourceDir: {
|
|
256
|
-
type: "string",
|
|
257
|
-
description:
|
|
258
|
-
"Absolute path to source directory (default: websrc/)",
|
|
259
|
-
},
|
|
260
|
-
outputDir: {
|
|
261
|
-
type: "string",
|
|
262
|
-
description: "Absolute path to output directory (default: web/)",
|
|
263
|
-
},
|
|
264
|
-
port: {
|
|
265
|
-
type: "number",
|
|
266
|
-
description: "Port number for dev server (default: 3000)",
|
|
267
|
-
default: 3000,
|
|
268
|
-
},
|
|
269
|
-
host: {
|
|
270
|
-
type: "string",
|
|
271
|
-
description: "Host for dev server (default: localhost)",
|
|
272
|
-
default: "localhost",
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
required: [],
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
],
|
|
279
|
-
};
|
|
280
|
-
});
|
|
126
|
+
for (const doc of DOCS) {
|
|
127
|
+
server.registerResource(
|
|
128
|
+
doc.name,
|
|
129
|
+
doc.uri,
|
|
130
|
+
{ description: doc.description, mimeType: "text/markdown" },
|
|
131
|
+
() => {
|
|
132
|
+
const filePath = join(DOCS_DIR, doc.file);
|
|
281
133
|
|
|
282
|
-
|
|
283
|
-
|
|
134
|
+
if (!existsSync(filePath)) {
|
|
135
|
+
throw new Error(`Documentation file not found: ${doc.file}`);
|
|
136
|
+
}
|
|
284
137
|
|
|
285
|
-
|
|
286
|
-
const topic = (args as any).topic;
|
|
287
|
-
const doc = DOCS.find((d) => d.uri === `tkeron://${topic}`);
|
|
138
|
+
const content = readFileSync(filePath, "utf-8");
|
|
288
139
|
|
|
289
|
-
if (!doc) {
|
|
290
140
|
return {
|
|
291
|
-
|
|
141
|
+
contents: [
|
|
292
142
|
{
|
|
293
|
-
|
|
294
|
-
|
|
143
|
+
uri: doc.uri,
|
|
144
|
+
mimeType: "text/markdown",
|
|
145
|
+
text: content,
|
|
295
146
|
},
|
|
296
147
|
],
|
|
297
148
|
};
|
|
298
|
-
}
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
}
|
|
299
152
|
|
|
300
|
-
|
|
301
|
-
|
|
153
|
+
server.registerTool(
|
|
154
|
+
"list_examples",
|
|
155
|
+
{
|
|
156
|
+
description:
|
|
157
|
+
"List all available Tkeron example projects with their descriptions",
|
|
158
|
+
annotations: { readOnlyHint: true },
|
|
159
|
+
},
|
|
160
|
+
() => {
|
|
161
|
+
const examples = readdirSync(EXAMPLES_DIR).filter((name) => {
|
|
162
|
+
const examplePath = join(EXAMPLES_DIR, name);
|
|
163
|
+
return (
|
|
164
|
+
statSync(examplePath).isDirectory() &&
|
|
165
|
+
(existsSync(join(examplePath, "src")) ||
|
|
166
|
+
existsSync(join(examplePath, "websrc")))
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const exampleList = examples.map((name) => ({
|
|
171
|
+
name,
|
|
172
|
+
description: EXAMPLE_DESCRIPTIONS[name] || "No description available",
|
|
173
|
+
path: `examples/${name}`,
|
|
174
|
+
}));
|
|
302
175
|
|
|
303
176
|
return {
|
|
304
177
|
content: [
|
|
305
178
|
{
|
|
306
|
-
type: "text",
|
|
307
|
-
text:
|
|
179
|
+
type: "text" as const,
|
|
180
|
+
text: JSON.stringify(exampleList, null, 2),
|
|
308
181
|
},
|
|
309
182
|
],
|
|
310
183
|
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (name === "list_examples") {
|
|
314
|
-
try {
|
|
315
|
-
const examples = readdirSync(EXAMPLES_DIR).filter((name) => {
|
|
316
|
-
const examplePath = join(EXAMPLES_DIR, name);
|
|
317
|
-
// Accept both src/ and websrc/ directories (init_sample uses websrc/)
|
|
318
|
-
return (
|
|
319
|
-
statSync(examplePath).isDirectory() &&
|
|
320
|
-
(existsSync(join(examplePath, "src")) ||
|
|
321
|
-
existsSync(join(examplePath, "websrc")))
|
|
322
|
-
);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
const exampleDescriptions: { [key: string]: string } = {
|
|
326
|
-
init_sample:
|
|
327
|
-
"Complete starter template with all features (used by 'tk init')",
|
|
328
|
-
basic_build: "Simple HTML + TypeScript bundling",
|
|
329
|
-
with_assets: "HTML in multiple directories with asset references",
|
|
330
|
-
with_pre: "Pre-rendering HTML at build time with .pre.ts files",
|
|
331
|
-
with_com_html_priority:
|
|
332
|
-
"Static HTML components (.com.html) with local override priority",
|
|
333
|
-
with_com_ts_priority:
|
|
334
|
-
"TypeScript components (.com.ts) with local override priority",
|
|
335
|
-
with_com_mixed_priority:
|
|
336
|
-
"Mixed .com.html and .com.ts showing processing order",
|
|
337
|
-
with_component_iteration:
|
|
338
|
-
"Component iteration: .com.ts with logic, .com.html for templates",
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const exampleList = examples.map((name) => ({
|
|
342
|
-
name,
|
|
343
|
-
description: exampleDescriptions[name] || "No description available",
|
|
344
|
-
path: `examples/${name}`,
|
|
345
|
-
}));
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
content: [
|
|
349
|
-
{
|
|
350
|
-
type: "text",
|
|
351
|
-
text: JSON.stringify(exampleList, null, 2),
|
|
352
|
-
},
|
|
353
|
-
],
|
|
354
|
-
};
|
|
355
|
-
} catch (error) {
|
|
356
|
-
return {
|
|
357
|
-
content: [
|
|
358
|
-
{
|
|
359
|
-
type: "text",
|
|
360
|
-
text: `Error listing examples: ${error instanceof Error ? error.message : String(error)}`,
|
|
361
|
-
},
|
|
362
|
-
],
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
}
|
|
184
|
+
},
|
|
185
|
+
);
|
|
366
186
|
|
|
367
|
-
|
|
368
|
-
|
|
187
|
+
server.registerTool(
|
|
188
|
+
"get_example",
|
|
189
|
+
{
|
|
190
|
+
description:
|
|
191
|
+
"Get the source code and structure of a specific Tkeron example project",
|
|
192
|
+
inputSchema: {
|
|
193
|
+
example: {
|
|
194
|
+
type: "string",
|
|
195
|
+
description:
|
|
196
|
+
"Example name (e.g., 'basic_build', 'with_component_iteration')",
|
|
197
|
+
},
|
|
198
|
+
} as any,
|
|
199
|
+
annotations: { readOnlyHint: true },
|
|
200
|
+
},
|
|
201
|
+
({ example: exampleName }: { example: string }) => {
|
|
369
202
|
const examplePath = join(EXAMPLES_DIR, exampleName);
|
|
370
203
|
|
|
371
204
|
if (!existsSync(examplePath)) {
|
|
372
205
|
return {
|
|
373
206
|
content: [
|
|
374
207
|
{
|
|
375
|
-
type: "text",
|
|
376
|
-
text: `Example not found: ${exampleName}
|
|
208
|
+
type: "text" as const,
|
|
209
|
+
text: `Example not found: ${exampleName}. Use the list_examples tool to see available examples.`,
|
|
377
210
|
},
|
|
378
211
|
],
|
|
212
|
+
isError: true,
|
|
379
213
|
};
|
|
380
214
|
}
|
|
381
215
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (!existsSync(srcPath)) {
|
|
386
|
-
srcPath = join(examplePath, "websrc");
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (!existsSync(srcPath)) {
|
|
390
|
-
return {
|
|
391
|
-
content: [
|
|
392
|
-
{
|
|
393
|
-
type: "text",
|
|
394
|
-
text: `Example ${exampleName} does not have a src or websrc directory`,
|
|
395
|
-
},
|
|
396
|
-
],
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Read all files in src directory recursively
|
|
401
|
-
const readDirRecursive = (
|
|
402
|
-
dir: string,
|
|
403
|
-
baseDir: string = dir,
|
|
404
|
-
): { [key: string]: string } => {
|
|
405
|
-
const files: { [key: string]: string } = {};
|
|
406
|
-
const items = readdirSync(dir);
|
|
407
|
-
|
|
408
|
-
for (const item of items) {
|
|
409
|
-
const fullPath = join(dir, item);
|
|
410
|
-
const relativePath = fullPath.substring(baseDir.length + 1);
|
|
411
|
-
|
|
412
|
-
if (statSync(fullPath).isDirectory()) {
|
|
413
|
-
Object.assign(files, readDirRecursive(fullPath, baseDir));
|
|
414
|
-
} else {
|
|
415
|
-
files[relativePath] = readFileSync(fullPath, "utf-8");
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return files;
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
const sourceFiles = readDirRecursive(srcPath);
|
|
423
|
-
|
|
424
|
-
const result = {
|
|
425
|
-
example: exampleName,
|
|
426
|
-
path: `examples/${exampleName}`,
|
|
427
|
-
files: sourceFiles,
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
content: [
|
|
432
|
-
{
|
|
433
|
-
type: "text",
|
|
434
|
-
text: JSON.stringify(result, null, 2),
|
|
435
|
-
},
|
|
436
|
-
],
|
|
437
|
-
};
|
|
438
|
-
} catch (error) {
|
|
439
|
-
return {
|
|
440
|
-
content: [
|
|
441
|
-
{
|
|
442
|
-
type: "text",
|
|
443
|
-
text: `Error reading example: ${error instanceof Error ? error.message : String(error)}`,
|
|
444
|
-
},
|
|
445
|
-
],
|
|
446
|
-
};
|
|
216
|
+
let srcPath = join(examplePath, "src");
|
|
217
|
+
if (!existsSync(srcPath)) {
|
|
218
|
+
srcPath = join(examplePath, "websrc");
|
|
447
219
|
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (name === "tkeron_init") {
|
|
451
|
-
const projectPath = (args as any).projectPath;
|
|
452
|
-
const force = (args as any).force || false;
|
|
453
|
-
|
|
454
|
-
try {
|
|
455
|
-
await init({
|
|
456
|
-
projectName: projectPath,
|
|
457
|
-
force,
|
|
458
|
-
promptFn: async () => force, // Auto-confirm if force is true
|
|
459
|
-
});
|
|
460
220
|
|
|
221
|
+
if (!existsSync(srcPath)) {
|
|
461
222
|
return {
|
|
462
223
|
content: [
|
|
463
224
|
{
|
|
464
|
-
type: "text",
|
|
465
|
-
text:
|
|
466
|
-
},
|
|
467
|
-
],
|
|
468
|
-
};
|
|
469
|
-
} catch (error) {
|
|
470
|
-
return {
|
|
471
|
-
content: [
|
|
472
|
-
{
|
|
473
|
-
type: "text",
|
|
474
|
-
text: `Error initializing project: ${error instanceof Error ? error.message : String(error)}`,
|
|
225
|
+
type: "text" as const,
|
|
226
|
+
text: `Example ${exampleName} does not have a src or websrc directory`,
|
|
475
227
|
},
|
|
476
228
|
],
|
|
229
|
+
isError: true,
|
|
477
230
|
};
|
|
478
231
|
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (name === "tkeron_build") {
|
|
482
|
-
const sourceDir = (args as any).sourceDir;
|
|
483
|
-
const targetDir = (args as any).targetDir;
|
|
484
232
|
|
|
485
|
-
|
|
486
|
-
await build({ sourceDir, targetDir });
|
|
233
|
+
const sourceFiles = readDirRecursive(srcPath);
|
|
487
234
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
},
|
|
494
|
-
],
|
|
495
|
-
};
|
|
496
|
-
} catch (error) {
|
|
497
|
-
return {
|
|
498
|
-
content: [
|
|
499
|
-
{
|
|
500
|
-
type: "text",
|
|
501
|
-
text: `Error building project: ${error instanceof Error ? error.message : String(error)}`,
|
|
502
|
-
},
|
|
503
|
-
],
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (name === "tkeron_develop") {
|
|
509
|
-
const sourceDir = (args as any).sourceDir;
|
|
510
|
-
const outputDir = (args as any).outputDir;
|
|
511
|
-
const port = (args as any).port || 3000;
|
|
512
|
-
const host = (args as any).host || "localhost";
|
|
513
|
-
|
|
514
|
-
try {
|
|
515
|
-
const server = await develop({ sourceDir, outputDir, port, host });
|
|
516
|
-
|
|
517
|
-
return {
|
|
518
|
-
content: [
|
|
519
|
-
{
|
|
520
|
-
type: "text",
|
|
521
|
-
text: `✅ Development server started at http://${server.host}:${server.port}\n\n⚠️ Note: Server is running in the background. Use the returned server object to stop it later.`,
|
|
522
|
-
},
|
|
523
|
-
],
|
|
524
|
-
};
|
|
525
|
-
} catch (error) {
|
|
526
|
-
return {
|
|
527
|
-
content: [
|
|
528
|
-
{
|
|
529
|
-
type: "text",
|
|
530
|
-
text: `Error starting development server: ${error instanceof Error ? error.message : String(error)}`,
|
|
531
|
-
},
|
|
532
|
-
],
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
async function main() {
|
|
541
|
-
try {
|
|
542
|
-
console.error(`[tkeron-mcp] Starting server...`);
|
|
543
|
-
console.error(`[tkeron-mcp] Docs directory: ${DOCS_DIR}`);
|
|
544
|
-
console.error(`[tkeron-mcp] Available resources: ${DOCS.length}`);
|
|
235
|
+
const result = {
|
|
236
|
+
example: exampleName,
|
|
237
|
+
path: `examples/${exampleName}`,
|
|
238
|
+
files: sourceFiles,
|
|
239
|
+
};
|
|
545
240
|
|
|
546
|
-
|
|
547
|
-
|
|
241
|
+
return {
|
|
242
|
+
content: [
|
|
243
|
+
{
|
|
244
|
+
type: "text" as const,
|
|
245
|
+
text: JSON.stringify(result, null, 2),
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
);
|
|
548
251
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
}
|
|
252
|
+
const main = async () => {
|
|
253
|
+
const transport = new StdioServerTransport();
|
|
254
|
+
await server.connect(transport);
|
|
255
|
+
console.error(`[tkeron-mcp] Server v${getVersion()} started`);
|
|
256
|
+
};
|
|
556
257
|
|
|
557
258
|
main().catch((error) => {
|
|
558
|
-
console.error(`[tkeron-mcp]
|
|
259
|
+
console.error(`[tkeron-mcp] Fatal error:`, error);
|
|
559
260
|
process.exit(1);
|
|
560
261
|
});
|
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -24,10 +24,10 @@ export const build = async (options: BuildOptions) => {
|
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const source =
|
|
27
|
+
const source = resolve(options.sourceDir || "websrc");
|
|
28
28
|
const target = options.targetDir
|
|
29
|
-
?
|
|
30
|
-
:
|
|
29
|
+
? resolve(options.targetDir)
|
|
30
|
+
: resolve(dirname(source), "web");
|
|
31
31
|
|
|
32
32
|
const sourceParent = dirname(source);
|
|
33
33
|
const tempDir = join(
|
|
@@ -67,14 +67,6 @@ export const build = async (options: BuildOptions) => {
|
|
|
67
67
|
`\n💡 Tip: Create the directory first, check the path, or run 'tk init' to create a new project.`,
|
|
68
68
|
);
|
|
69
69
|
log.error(` Expected: ${source}\n`);
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
error.message &&
|
|
75
|
-
error.message.includes("must contain at least one hyphen")
|
|
76
|
-
) {
|
|
77
|
-
process.exit(1);
|
|
78
70
|
}
|
|
79
71
|
|
|
80
72
|
throw error;
|
package/src/develop.ts
CHANGED
|
@@ -13,8 +13,6 @@ export interface TkeronDevOptions {
|
|
|
13
13
|
logger?: Logger;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const reloadClients = new Set<ReadableStreamDefaultController>();
|
|
17
|
-
|
|
18
16
|
export interface DevelopServer {
|
|
19
17
|
stop: () => Promise<void>;
|
|
20
18
|
port: number;
|
|
@@ -28,6 +26,8 @@ export const develop = async (
|
|
|
28
26
|
throw new Error("Invalid options provided for develop");
|
|
29
27
|
}
|
|
30
28
|
|
|
29
|
+
const reloadClients = new Set<ReadableStreamDefaultController>();
|
|
30
|
+
|
|
31
31
|
const {
|
|
32
32
|
port = 3000,
|
|
33
33
|
host = "localhost",
|
|
@@ -36,28 +36,13 @@ export const develop = async (
|
|
|
36
36
|
logger: log = defaultLogger,
|
|
37
37
|
} = options;
|
|
38
38
|
|
|
39
|
-
const source =
|
|
39
|
+
const source = resolve(sourceDir || "websrc");
|
|
40
40
|
const target = outputDir
|
|
41
|
-
?
|
|
42
|
-
:
|
|
41
|
+
? resolve(outputDir)
|
|
42
|
+
: resolve(dirname(source), "web");
|
|
43
43
|
|
|
44
44
|
log.log("🔨 Building project...");
|
|
45
|
-
|
|
46
|
-
await build({ sourceDir: source, targetDir: target, logger: log });
|
|
47
|
-
} catch (error: any) {
|
|
48
|
-
if (error.code === "ENOENT") {
|
|
49
|
-
const actualSourceDir = sourceDir || "websrc";
|
|
50
|
-
log.error(
|
|
51
|
-
`\n❌ Error: Source directory "${actualSourceDir}" does not exist.`,
|
|
52
|
-
);
|
|
53
|
-
log.error(
|
|
54
|
-
`\n💡 Tip: Create the directory first, check the path, or run 'tk init' to create a new project.`,
|
|
55
|
-
);
|
|
56
|
-
log.error(` Expected: ${source}\n`);
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
45
|
+
await build({ sourceDir: source, targetDir: target, logger: log });
|
|
61
46
|
log.log("✅ Build complete!");
|
|
62
47
|
|
|
63
48
|
const server = Bun.serve({
|
|
@@ -182,5 +167,3 @@ export const develop = async (
|
|
|
182
167
|
host: server.hostname ?? host,
|
|
183
168
|
};
|
|
184
169
|
};
|
|
185
|
-
|
|
186
|
-
export { setupSigintHandler } from "./setupSigintHandler";
|
package/src/init.ts
CHANGED
package/src/initWrapper.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface InitWrapperOptions {
|
|
|
9
9
|
[key: string]: any;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export async
|
|
12
|
+
export const initWrapper = async (options: InitWrapperOptions) => {
|
|
13
13
|
const log = options?.logger || defaultLogger;
|
|
14
14
|
|
|
15
15
|
if (!options || typeof options !== "object") {
|
|
@@ -60,4 +60,4 @@ export async function initWrapper(options: InitWrapperOptions) {
|
|
|
60
60
|
}
|
|
61
61
|
process.exit(1);
|
|
62
62
|
}
|
|
63
|
-
}
|
|
63
|
+
};
|
package/src/processCom.ts
CHANGED
|
@@ -60,14 +60,14 @@ export const processCom = async (
|
|
|
60
60
|
return hasChanges;
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
async
|
|
63
|
+
const processComponents = async (
|
|
64
64
|
element: any,
|
|
65
65
|
currentDir: string,
|
|
66
66
|
rootDir: string,
|
|
67
67
|
componentStack: string[],
|
|
68
68
|
depth: number = 0,
|
|
69
69
|
log: Logger = silentLogger,
|
|
70
|
-
): Promise<boolean> {
|
|
70
|
+
): Promise<boolean> => {
|
|
71
71
|
let hasChanges = false;
|
|
72
72
|
const MAX_DEPTH = 50;
|
|
73
73
|
if (depth > MAX_DEPTH) {
|
|
@@ -183,7 +183,6 @@ async function processComponents(
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
// Recursively process child elements
|
|
187
186
|
const childChanged = await processComponents(
|
|
188
187
|
child,
|
|
189
188
|
currentDir,
|
|
@@ -196,4 +195,4 @@ async function processComponents(
|
|
|
196
195
|
}
|
|
197
196
|
|
|
198
197
|
return hasChanges;
|
|
199
|
-
}
|
|
198
|
+
};
|
package/src/processComTs.ts
CHANGED
|
@@ -61,7 +61,7 @@ export const processComTs = async (
|
|
|
61
61
|
return hasChanges;
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
async
|
|
64
|
+
const processComponentsTs = async (
|
|
65
65
|
element: any,
|
|
66
66
|
currentDir: string,
|
|
67
67
|
rootDir: string,
|
|
@@ -69,7 +69,7 @@ async function processComponentsTs(
|
|
|
69
69
|
depth: number = 0,
|
|
70
70
|
log: Logger = silentLogger,
|
|
71
71
|
options: ProcessComTsOptions = {},
|
|
72
|
-
): Promise<boolean> {
|
|
72
|
+
): Promise<boolean> => {
|
|
73
73
|
let hasChanges = false;
|
|
74
74
|
const MAX_DEPTH = 50;
|
|
75
75
|
if (depth > MAX_DEPTH) {
|
|
@@ -251,7 +251,6 @@ ${codeWithoutImports}
|
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
// Recursively process child elements
|
|
255
254
|
const childChanged = await processComponentsTs(
|
|
256
255
|
child,
|
|
257
256
|
currentDir,
|
|
@@ -265,4 +264,4 @@ ${codeWithoutImports}
|
|
|
265
264
|
}
|
|
266
265
|
|
|
267
266
|
return hasChanges;
|
|
268
|
-
}
|
|
267
|
+
};
|
package/src/processPre.ts
CHANGED
package/tests/test-helpers.ts
CHANGED
|
@@ -11,7 +11,7 @@ const hashString = (str: string): number => {
|
|
|
11
11
|
for (let i = 0; i < str.length; i++) {
|
|
12
12
|
const char = str.charCodeAt(i);
|
|
13
13
|
hash = (hash << 5) - hash + char;
|
|
14
|
-
hash = hash & hash;
|
|
14
|
+
hash = hash & hash;
|
|
15
15
|
}
|
|
16
16
|
return Math.abs(hash);
|
|
17
17
|
};
|
|
@@ -22,7 +22,7 @@ export const getTestResources = (
|
|
|
22
22
|
testName: string,
|
|
23
23
|
): { port: number; dir: string } => {
|
|
24
24
|
const hash = hashString(testName);
|
|
25
|
-
const port = 9000 + (hash % 10000);
|
|
25
|
+
const port = 9000 + (hash % 10000);
|
|
26
26
|
const uniqueId = `${hash}-${resourceCounter++}`;
|
|
27
27
|
const dir = join(tmpdir(), `tkeron-test-${uniqueId}`);
|
|
28
28
|
|