@pyreon/mcp 0.11.4 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +13 -13
- package/src/api-reference.ts +176 -176
- package/src/index.ts +43 -43
- package/src/project-scanner.ts +1 -1
- package/src/tests/api-reference.test.ts +4 -4
- package/src/tests/project-scanner.test.ts +82 -82
package/src/index.ts
CHANGED
|
@@ -17,20 +17,20 @@
|
|
|
17
17
|
* bunx @pyreon/mcp # stdio transport (for IDE integration)
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { McpServer } from
|
|
21
|
-
import { StdioServerTransport } from
|
|
22
|
-
import { detectReactPatterns, diagnoseError, migrateReactCode } from
|
|
23
|
-
import { z } from
|
|
24
|
-
import packageJson from
|
|
25
|
-
import { API_REFERENCE } from
|
|
26
|
-
import { generateContext, type ProjectContext } from
|
|
20
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
21
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
22
|
+
import { detectReactPatterns, diagnoseError, migrateReactCode } from '@pyreon/compiler'
|
|
23
|
+
import { z } from 'zod'
|
|
24
|
+
import packageJson from '../package.json' with { type: 'json' }
|
|
25
|
+
import { API_REFERENCE } from './api-reference'
|
|
26
|
+
import { generateContext, type ProjectContext } from './project-scanner'
|
|
27
27
|
|
|
28
28
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
29
29
|
// Server setup
|
|
30
30
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
31
31
|
|
|
32
32
|
const server = new McpServer({
|
|
33
|
-
name:
|
|
33
|
+
name: 'pyreon',
|
|
34
34
|
version: packageJson.version,
|
|
35
35
|
})
|
|
36
36
|
|
|
@@ -47,7 +47,7 @@ function getContext(): ProjectContext {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function textResult(text: string) {
|
|
50
|
-
return { content: [{ type:
|
|
50
|
+
return { content: [{ type: 'text' as const, text }] }
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -55,7 +55,7 @@ function textResult(text: string) {
|
|
|
55
55
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
56
56
|
|
|
57
57
|
server.tool(
|
|
58
|
-
|
|
58
|
+
'get_api',
|
|
59
59
|
{
|
|
60
60
|
package: z.string(),
|
|
61
61
|
symbol: z.string(),
|
|
@@ -73,14 +73,14 @@ server.tool(
|
|
|
73
73
|
return textResult(
|
|
74
74
|
`Symbol '${symbol}' not found in @pyreon/${pkg}.\n\n${
|
|
75
75
|
suggestions.length > 0
|
|
76
|
-
? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join(
|
|
77
|
-
:
|
|
76
|
+
? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join('\n')}`
|
|
77
|
+
: 'No similar symbols found.'
|
|
78
78
|
}`,
|
|
79
79
|
)
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
return textResult(
|
|
83
|
-
`## @pyreon/${pkg} — ${symbol}\n\n**Signature:**\n\`\`\`typescript\n${entry.signature}\n\`\`\`\n\n**Usage:**\n\`\`\`typescript\n${entry.example}\n\`\`\`\n\n${entry.notes ? `**Notes:** ${entry.notes}\n\n` :
|
|
83
|
+
`## @pyreon/${pkg} — ${symbol}\n\n**Signature:**\n\`\`\`typescript\n${entry.signature}\n\`\`\`\n\n**Usage:**\n\`\`\`typescript\n${entry.example}\n\`\`\`\n\n${entry.notes ? `**Notes:** ${entry.notes}\n\n` : ''}${entry.mistakes ? `**Common mistakes:**\n${entry.mistakes}\n` : ''}`,
|
|
84
84
|
)
|
|
85
85
|
},
|
|
86
86
|
)
|
|
@@ -90,27 +90,27 @@ server.tool(
|
|
|
90
90
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
91
91
|
|
|
92
92
|
server.tool(
|
|
93
|
-
|
|
93
|
+
'validate',
|
|
94
94
|
{
|
|
95
95
|
code: z.string(),
|
|
96
96
|
filename: z.string().optional(),
|
|
97
97
|
},
|
|
98
98
|
async ({ code, filename }) => {
|
|
99
|
-
const diagnostics = detectReactPatterns(code, filename ??
|
|
99
|
+
const diagnostics = detectReactPatterns(code, filename ?? 'snippet.tsx')
|
|
100
100
|
|
|
101
101
|
if (diagnostics.length === 0) {
|
|
102
|
-
return textResult(
|
|
102
|
+
return textResult('✓ No issues found. The code follows Pyreon patterns correctly.')
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const issueText = diagnostics
|
|
106
106
|
.map(
|
|
107
107
|
(d, i) =>
|
|
108
|
-
`${i + 1}. **${d.code}** (line ${d.line})\n ${d.message}\n Current: \`${d.current}\`\n Fix: \`${d.suggested}\`\n Auto-fixable: ${d.fixable ?
|
|
108
|
+
`${i + 1}. **${d.code}** (line ${d.line})\n ${d.message}\n Current: \`${d.current}\`\n Fix: \`${d.suggested}\`\n Auto-fixable: ${d.fixable ? 'yes' : 'no'}`,
|
|
109
109
|
)
|
|
110
|
-
.join(
|
|
110
|
+
.join('\n\n')
|
|
111
111
|
|
|
112
112
|
return textResult(
|
|
113
|
-
`Found ${diagnostics.length} issue${diagnostics.length === 1 ?
|
|
113
|
+
`Found ${diagnostics.length} issue${diagnostics.length === 1 ? '' : 's'}:\n\n${issueText}`,
|
|
114
114
|
)
|
|
115
115
|
},
|
|
116
116
|
)
|
|
@@ -120,24 +120,24 @@ server.tool(
|
|
|
120
120
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
121
121
|
|
|
122
122
|
server.tool(
|
|
123
|
-
|
|
123
|
+
'migrate_react',
|
|
124
124
|
{
|
|
125
125
|
code: z.string(),
|
|
126
126
|
filename: z.string().optional(),
|
|
127
127
|
},
|
|
128
128
|
async ({ code, filename }) => {
|
|
129
|
-
const result = migrateReactCode(code, filename ??
|
|
129
|
+
const result = migrateReactCode(code, filename ?? 'component.tsx')
|
|
130
130
|
|
|
131
|
-
const changeList = result.changes.map((c) => `- Line ${c.line}: ${c.description}`).join(
|
|
131
|
+
const changeList = result.changes.map((c) => `- Line ${c.line}: ${c.description}`).join('\n')
|
|
132
132
|
|
|
133
133
|
const remainingIssues = result.diagnostics.filter((d) => !d.fixable)
|
|
134
134
|
const manualText =
|
|
135
135
|
remainingIssues.length > 0
|
|
136
|
-
? `\n\n**Remaining issues (manual fix needed):**\n${remainingIssues.map((d) => `- Line ${d.line}: ${d.message}\n Suggested: \`${d.suggested}\``).join(
|
|
137
|
-
:
|
|
136
|
+
? `\n\n**Remaining issues (manual fix needed):**\n${remainingIssues.map((d) => `- Line ${d.line}: ${d.message}\n Suggested: \`${d.suggested}\``).join('\n')}`
|
|
137
|
+
: ''
|
|
138
138
|
|
|
139
139
|
return textResult(
|
|
140
|
-
`## Migrated Code\n\n\`\`\`tsx\n${result.code}\n\`\`\`\n\n**Changes applied (${result.changes.length}):**\n${changeList ||
|
|
140
|
+
`## Migrated Code\n\n\`\`\`tsx\n${result.code}\n\`\`\`\n\n**Changes applied (${result.changes.length}):**\n${changeList || 'No changes needed.'}${manualText}`,
|
|
141
141
|
)
|
|
142
142
|
},
|
|
143
143
|
)
|
|
@@ -147,7 +147,7 @@ server.tool(
|
|
|
147
147
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
148
148
|
|
|
149
149
|
server.tool(
|
|
150
|
-
|
|
150
|
+
'diagnose',
|
|
151
151
|
{
|
|
152
152
|
error: z.string(),
|
|
153
153
|
},
|
|
@@ -176,29 +176,29 @@ server.tool(
|
|
|
176
176
|
// Tool: get_routes
|
|
177
177
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
178
178
|
|
|
179
|
-
server.tool(
|
|
179
|
+
server.tool('get_routes', {}, async () => {
|
|
180
180
|
const ctx = getContext()
|
|
181
181
|
|
|
182
182
|
if (ctx.routes.length === 0) {
|
|
183
183
|
return textResult(
|
|
184
|
-
|
|
184
|
+
'No routes detected. Routes are defined via createRouter() or a routes array.',
|
|
185
185
|
)
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
const routeTable = ctx.routes
|
|
189
189
|
.map((r) => {
|
|
190
190
|
const flags = [
|
|
191
|
-
r.hasLoader ?
|
|
192
|
-
r.hasGuard ?
|
|
193
|
-
r.params.length > 0 ? `params: ${r.params.join(
|
|
194
|
-
r.name ? `name: "${r.name}"` :
|
|
191
|
+
r.hasLoader ? 'loader' : '',
|
|
192
|
+
r.hasGuard ? 'guard' : '',
|
|
193
|
+
r.params.length > 0 ? `params: ${r.params.join(', ')}` : '',
|
|
194
|
+
r.name ? `name: "${r.name}"` : '',
|
|
195
195
|
]
|
|
196
196
|
.filter(Boolean)
|
|
197
|
-
.join(
|
|
197
|
+
.join(', ')
|
|
198
198
|
|
|
199
|
-
return ` ${r.path}${flags ? ` (${flags})` :
|
|
199
|
+
return ` ${r.path}${flags ? ` (${flags})` : ''}`
|
|
200
200
|
})
|
|
201
|
-
.join(
|
|
201
|
+
.join('\n')
|
|
202
202
|
|
|
203
203
|
return textResult(`**Routes (${ctx.routes.length}):**\n\n${routeTable}`)
|
|
204
204
|
})
|
|
@@ -207,25 +207,25 @@ server.tool("get_routes", {}, async () => {
|
|
|
207
207
|
// Tool: get_components
|
|
208
208
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
209
209
|
|
|
210
|
-
server.tool(
|
|
210
|
+
server.tool('get_components', {}, async () => {
|
|
211
211
|
const ctx = getContext()
|
|
212
212
|
|
|
213
213
|
if (ctx.components.length === 0) {
|
|
214
|
-
return textResult(
|
|
214
|
+
return textResult('No components detected.')
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
const compList = ctx.components
|
|
218
218
|
.map((c) => {
|
|
219
219
|
const details = [
|
|
220
|
-
c.props.length > 0 ? `props: { ${c.props.join(
|
|
221
|
-
c.hasSignals ? `signals: [${c.signalNames.join(
|
|
220
|
+
c.props.length > 0 ? `props: { ${c.props.join(', ')} }` : '',
|
|
221
|
+
c.hasSignals ? `signals: [${c.signalNames.join(', ')}]` : '',
|
|
222
222
|
]
|
|
223
223
|
.filter(Boolean)
|
|
224
|
-
.join(
|
|
224
|
+
.join(', ')
|
|
225
225
|
|
|
226
|
-
return ` ${c.name} — ${c.file}${details ? `\n ${details}` :
|
|
226
|
+
return ` ${c.name} — ${c.file}${details ? `\n ${details}` : ''}`
|
|
227
227
|
})
|
|
228
|
-
.join(
|
|
228
|
+
.join('\n')
|
|
229
229
|
|
|
230
230
|
return textResult(`**Components (${ctx.components.length}):**\n\n${compList}`)
|
|
231
231
|
})
|
|
@@ -240,6 +240,6 @@ async function main(): Promise<void> {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
main().catch((err) => {
|
|
243
|
-
console.error(
|
|
243
|
+
console.error('MCP server error:', err)
|
|
244
244
|
process.exit(1)
|
|
245
245
|
})
|
package/src/project-scanner.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { API_REFERENCE } from
|
|
1
|
+
import { API_REFERENCE } from '../api-reference'
|
|
2
2
|
|
|
3
|
-
describe(
|
|
4
|
-
it(
|
|
3
|
+
describe('api-reference', () => {
|
|
4
|
+
it('has entries', () => {
|
|
5
5
|
expect(Object.keys(API_REFERENCE).length).toBeGreaterThan(0)
|
|
6
6
|
})
|
|
7
7
|
|
|
8
|
-
it(
|
|
8
|
+
it('entries have required fields', () => {
|
|
9
9
|
for (const [key, entry] of Object.entries(API_REFERENCE)) {
|
|
10
10
|
expect(entry.signature, `${key} missing signature`).toBeTruthy()
|
|
11
11
|
expect(entry.example, `${key} missing example`).toBeTruthy()
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as os from
|
|
3
|
-
import * as path from
|
|
4
|
-
import { generateContext } from
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as os from 'node:os'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
import { generateContext } from '../project-scanner'
|
|
5
5
|
|
|
6
6
|
function makeTmpDir(): string {
|
|
7
|
-
return fs.mkdtempSync(path.join(os.tmpdir(),
|
|
7
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'pyreon-scanner-'))
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function writeFile(dir: string, relPath: string, content: string): void {
|
|
11
11
|
const full = path.join(dir, relPath)
|
|
12
12
|
fs.mkdirSync(path.dirname(full), { recursive: true })
|
|
13
|
-
fs.writeFileSync(full, content,
|
|
13
|
+
fs.writeFileSync(full, content, 'utf-8')
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
describe(
|
|
16
|
+
describe('generateContext', () => {
|
|
17
17
|
let tmpDir: string
|
|
18
18
|
|
|
19
19
|
beforeEach(() => {
|
|
@@ -24,34 +24,34 @@ describe("generateContext", () => {
|
|
|
24
24
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
it(
|
|
27
|
+
it('returns correct framework and version from @pyreon dependency', () => {
|
|
28
28
|
writeFile(
|
|
29
29
|
tmpDir,
|
|
30
|
-
|
|
31
|
-
JSON.stringify({ dependencies: {
|
|
30
|
+
'package.json',
|
|
31
|
+
JSON.stringify({ dependencies: { '@pyreon/core': '^1.2.3' } }),
|
|
32
32
|
)
|
|
33
33
|
const ctx = generateContext(tmpDir)
|
|
34
|
-
expect(ctx.framework).toBe(
|
|
35
|
-
expect(ctx.version).toBe(
|
|
34
|
+
expect(ctx.framework).toBe('pyreon')
|
|
35
|
+
expect(ctx.version).toBe('1.2.3')
|
|
36
36
|
expect(ctx.generatedAt).toBeTruthy()
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
it(
|
|
40
|
-
writeFile(tmpDir,
|
|
39
|
+
it('falls back to package version when no @pyreon dep', () => {
|
|
40
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '0.5.0' }))
|
|
41
41
|
const ctx = generateContext(tmpDir)
|
|
42
|
-
expect(ctx.version).toBe(
|
|
42
|
+
expect(ctx.version).toBe('0.5.0')
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
-
it(
|
|
45
|
+
it('returns unknown version when no package.json', () => {
|
|
46
46
|
const ctx = generateContext(tmpDir)
|
|
47
|
-
expect(ctx.version).toBe(
|
|
47
|
+
expect(ctx.version).toBe('unknown')
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
it(
|
|
51
|
-
writeFile(tmpDir,
|
|
50
|
+
it('extracts routes from createRouter calls', () => {
|
|
51
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
52
52
|
writeFile(
|
|
53
53
|
tmpDir,
|
|
54
|
-
|
|
54
|
+
'src/router.ts',
|
|
55
55
|
`
|
|
56
56
|
import { createRouter } from "@pyreon/router"
|
|
57
57
|
const router = createRouter([
|
|
@@ -62,15 +62,15 @@ const router = createRouter([
|
|
|
62
62
|
)
|
|
63
63
|
const ctx = generateContext(tmpDir)
|
|
64
64
|
expect(ctx.routes).toHaveLength(2)
|
|
65
|
-
expect(ctx.routes[0]?.path).toBe(
|
|
66
|
-
expect(ctx.routes[1]?.path).toBe(
|
|
65
|
+
expect(ctx.routes[0]?.path).toBe('/')
|
|
66
|
+
expect(ctx.routes[1]?.path).toBe('/about')
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
-
it(
|
|
70
|
-
writeFile(tmpDir,
|
|
69
|
+
it('extracts routes from routes variable', () => {
|
|
70
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
71
71
|
writeFile(
|
|
72
72
|
tmpDir,
|
|
73
|
-
|
|
73
|
+
'src/routes.ts',
|
|
74
74
|
`
|
|
75
75
|
const routes = [
|
|
76
76
|
{ path: "/home", component: Home },
|
|
@@ -79,14 +79,14 @@ const routes = [
|
|
|
79
79
|
)
|
|
80
80
|
const ctx = generateContext(tmpDir)
|
|
81
81
|
expect(ctx.routes).toHaveLength(1)
|
|
82
|
-
expect(ctx.routes[0]?.path).toBe(
|
|
82
|
+
expect(ctx.routes[0]?.path).toBe('/home')
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
-
it(
|
|
86
|
-
writeFile(tmpDir,
|
|
85
|
+
it('extracts route params', () => {
|
|
86
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
87
87
|
writeFile(
|
|
88
88
|
tmpDir,
|
|
89
|
-
|
|
89
|
+
'src/router.ts',
|
|
90
90
|
`
|
|
91
91
|
const routes = [
|
|
92
92
|
{ path: "/users/:id/posts/:postId" },
|
|
@@ -94,14 +94,14 @@ const routes = [
|
|
|
94
94
|
`,
|
|
95
95
|
)
|
|
96
96
|
const ctx = generateContext(tmpDir)
|
|
97
|
-
expect(ctx.routes[0]?.params).toEqual([
|
|
97
|
+
expect(ctx.routes[0]?.params).toEqual(['id', 'postId'])
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
-
it(
|
|
101
|
-
writeFile(tmpDir,
|
|
100
|
+
it('extracts optional route params', () => {
|
|
101
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
102
102
|
writeFile(
|
|
103
103
|
tmpDir,
|
|
104
|
-
|
|
104
|
+
'src/router.ts',
|
|
105
105
|
`
|
|
106
106
|
const routes = [
|
|
107
107
|
{ path: "/search/:query?" },
|
|
@@ -109,14 +109,14 @@ const routes = [
|
|
|
109
109
|
`,
|
|
110
110
|
)
|
|
111
111
|
const ctx = generateContext(tmpDir)
|
|
112
|
-
expect(ctx.routes[0]?.params).toEqual([
|
|
112
|
+
expect(ctx.routes[0]?.params).toEqual(['query'])
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
-
it(
|
|
116
|
-
writeFile(tmpDir,
|
|
115
|
+
it('detects loaders and guards', () => {
|
|
116
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
117
117
|
writeFile(
|
|
118
118
|
tmpDir,
|
|
119
|
-
|
|
119
|
+
'src/router.ts',
|
|
120
120
|
`
|
|
121
121
|
const routes = [
|
|
122
122
|
{ path: "/dash", loader: fetchData, beforeEnter: authGuard },
|
|
@@ -128,11 +128,11 @@ const routes = [
|
|
|
128
128
|
expect(ctx.routes[0]?.hasGuard).toBe(true)
|
|
129
129
|
})
|
|
130
130
|
|
|
131
|
-
it(
|
|
132
|
-
writeFile(tmpDir,
|
|
131
|
+
it('detects beforeLeave guard', () => {
|
|
132
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
133
133
|
writeFile(
|
|
134
134
|
tmpDir,
|
|
135
|
-
|
|
135
|
+
'src/router.ts',
|
|
136
136
|
`
|
|
137
137
|
const routes = [
|
|
138
138
|
{ path: "/form", beforeLeave: confirmLeave },
|
|
@@ -143,11 +143,11 @@ const routes = [
|
|
|
143
143
|
expect(ctx.routes[0]?.hasGuard).toBe(true)
|
|
144
144
|
})
|
|
145
145
|
|
|
146
|
-
it(
|
|
147
|
-
writeFile(tmpDir,
|
|
146
|
+
it('extracts route names', () => {
|
|
147
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
148
148
|
writeFile(
|
|
149
149
|
tmpDir,
|
|
150
|
-
|
|
150
|
+
'src/router.ts',
|
|
151
151
|
`
|
|
152
152
|
const routes = [
|
|
153
153
|
{ path: "/profile", name: "profile", component: Profile },
|
|
@@ -155,14 +155,14 @@ const routes = [
|
|
|
155
155
|
`,
|
|
156
156
|
)
|
|
157
157
|
const ctx = generateContext(tmpDir)
|
|
158
|
-
expect(ctx.routes[0]?.name).toBe(
|
|
158
|
+
expect(ctx.routes[0]?.name).toBe('profile')
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
it(
|
|
162
|
-
writeFile(tmpDir,
|
|
161
|
+
it('extracts component names with props', () => {
|
|
162
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
163
163
|
writeFile(
|
|
164
164
|
tmpDir,
|
|
165
|
-
|
|
165
|
+
'src/Button.tsx',
|
|
166
166
|
`
|
|
167
167
|
export function Button(props: { label: string; disabled: boolean }) {
|
|
168
168
|
return <button>{props.label}</button>
|
|
@@ -171,15 +171,15 @@ export function Button(props: { label: string; disabled: boolean }) {
|
|
|
171
171
|
)
|
|
172
172
|
const ctx = generateContext(tmpDir)
|
|
173
173
|
expect(ctx.components).toHaveLength(1)
|
|
174
|
-
expect(ctx.components[0]?.name).toBe(
|
|
175
|
-
expect(ctx.components[0]?.file).toBe(
|
|
174
|
+
expect(ctx.components[0]?.name).toBe('Button')
|
|
175
|
+
expect(ctx.components[0]?.file).toBe('src/Button.tsx')
|
|
176
176
|
})
|
|
177
177
|
|
|
178
|
-
it(
|
|
179
|
-
writeFile(tmpDir,
|
|
178
|
+
it('detects signals in components', () => {
|
|
179
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
180
180
|
writeFile(
|
|
181
181
|
tmpDir,
|
|
182
|
-
|
|
182
|
+
'src/Counter.tsx',
|
|
183
183
|
`
|
|
184
184
|
function Counter() {
|
|
185
185
|
const count = signal<number>(0)
|
|
@@ -190,14 +190,14 @@ function Counter() {
|
|
|
190
190
|
)
|
|
191
191
|
const ctx = generateContext(tmpDir)
|
|
192
192
|
expect(ctx.components[0]?.hasSignals).toBe(true)
|
|
193
|
-
expect(ctx.components[0]?.signalNames).toEqual([
|
|
193
|
+
expect(ctx.components[0]?.signalNames).toEqual(['count', 'name'])
|
|
194
194
|
})
|
|
195
195
|
|
|
196
|
-
it(
|
|
197
|
-
writeFile(tmpDir,
|
|
196
|
+
it('reports no signals when none present', () => {
|
|
197
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
198
198
|
writeFile(
|
|
199
199
|
tmpDir,
|
|
200
|
-
|
|
200
|
+
'src/Static.tsx',
|
|
201
201
|
`
|
|
202
202
|
function Static() {
|
|
203
203
|
return <div>hello</div>
|
|
@@ -209,11 +209,11 @@ function Static() {
|
|
|
209
209
|
expect(ctx.components[0]?.signalNames).toEqual([])
|
|
210
210
|
})
|
|
211
211
|
|
|
212
|
-
it(
|
|
213
|
-
writeFile(tmpDir,
|
|
212
|
+
it('extracts island declarations with hydrate strategy', () => {
|
|
213
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
214
214
|
writeFile(
|
|
215
215
|
tmpDir,
|
|
216
|
-
|
|
216
|
+
'src/islands.ts',
|
|
217
217
|
`
|
|
218
218
|
const Counter = island(() => import("./Counter"), { name: "Counter", hydrate: "visible" })
|
|
219
219
|
const Nav = island(() => import("./Nav"), { name: "Nav", hydrate: "idle" })
|
|
@@ -221,66 +221,66 @@ const Nav = island(() => import("./Nav"), { name: "Nav", hydrate: "idle" })
|
|
|
221
221
|
)
|
|
222
222
|
const ctx = generateContext(tmpDir)
|
|
223
223
|
expect(ctx.islands).toHaveLength(2)
|
|
224
|
-
expect(ctx.islands[0]?.name).toBe(
|
|
225
|
-
expect(ctx.islands[1]?.name).toBe(
|
|
224
|
+
expect(ctx.islands[0]?.name).toBe('Counter')
|
|
225
|
+
expect(ctx.islands[1]?.name).toBe('Nav')
|
|
226
226
|
})
|
|
227
227
|
|
|
228
|
-
it(
|
|
229
|
-
writeFile(tmpDir,
|
|
228
|
+
it('defaults hydrate to load when not specified', () => {
|
|
229
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
230
230
|
writeFile(
|
|
231
231
|
tmpDir,
|
|
232
|
-
|
|
232
|
+
'src/islands.ts',
|
|
233
233
|
`
|
|
234
234
|
const Widget = island(() => import("./Widget"), { name: "Widget" })
|
|
235
235
|
`,
|
|
236
236
|
)
|
|
237
237
|
const ctx = generateContext(tmpDir)
|
|
238
|
-
expect(ctx.islands[0]?.hydrate).toBe(
|
|
238
|
+
expect(ctx.islands[0]?.hydrate).toBe('load')
|
|
239
239
|
})
|
|
240
240
|
|
|
241
|
-
it(
|
|
242
|
-
writeFile(tmpDir,
|
|
241
|
+
it('returns empty arrays for empty project', () => {
|
|
242
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
243
243
|
const ctx = generateContext(tmpDir)
|
|
244
244
|
expect(ctx.routes).toEqual([])
|
|
245
245
|
expect(ctx.components).toEqual([])
|
|
246
246
|
expect(ctx.islands).toEqual([])
|
|
247
247
|
})
|
|
248
248
|
|
|
249
|
-
it(
|
|
250
|
-
writeFile(tmpDir,
|
|
249
|
+
it('skips node_modules and dist directories', () => {
|
|
250
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
251
251
|
writeFile(
|
|
252
252
|
tmpDir,
|
|
253
|
-
|
|
253
|
+
'node_modules/@pyreon/core/src/index.ts',
|
|
254
254
|
`function Internal() { return null }`,
|
|
255
255
|
)
|
|
256
|
-
writeFile(tmpDir,
|
|
257
|
-
writeFile(tmpDir,
|
|
256
|
+
writeFile(tmpDir, 'dist/bundle.ts', `function Bundled() { return null }`)
|
|
257
|
+
writeFile(tmpDir, 'src/App.tsx', `function App() { return <div /> }`)
|
|
258
258
|
const ctx = generateContext(tmpDir)
|
|
259
259
|
expect(ctx.components).toHaveLength(1)
|
|
260
|
-
expect(ctx.components[0]?.name).toBe(
|
|
260
|
+
expect(ctx.components[0]?.name).toBe('App')
|
|
261
261
|
})
|
|
262
262
|
|
|
263
|
-
it(
|
|
264
|
-
writeFile(tmpDir,
|
|
265
|
-
writeFile(tmpDir,
|
|
266
|
-
const badDir = path.join(tmpDir,
|
|
263
|
+
it('handles unreadable directories gracefully', () => {
|
|
264
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
265
|
+
writeFile(tmpDir, 'src/App.tsx', `function App() { return <div /> }`)
|
|
266
|
+
const badDir = path.join(tmpDir, 'restricted')
|
|
267
267
|
fs.mkdirSync(badDir)
|
|
268
268
|
fs.chmodSync(badDir, 0o000)
|
|
269
269
|
|
|
270
270
|
try {
|
|
271
271
|
const ctx = generateContext(tmpDir)
|
|
272
272
|
expect(ctx.components).toHaveLength(1)
|
|
273
|
-
expect(ctx.components[0]?.name).toBe(
|
|
273
|
+
expect(ctx.components[0]?.name).toBe('App')
|
|
274
274
|
} finally {
|
|
275
275
|
fs.chmodSync(badDir, 0o755)
|
|
276
276
|
}
|
|
277
277
|
})
|
|
278
278
|
|
|
279
|
-
it(
|
|
280
|
-
writeFile(tmpDir,
|
|
279
|
+
it('extracts const arrow function components', () => {
|
|
280
|
+
writeFile(tmpDir, 'package.json', JSON.stringify({ version: '1.0.0' }))
|
|
281
281
|
writeFile(
|
|
282
282
|
tmpDir,
|
|
283
|
-
|
|
283
|
+
'src/Card.tsx',
|
|
284
284
|
`
|
|
285
285
|
export const Card = (props: CardProps) => {
|
|
286
286
|
return <div>{props.title}</div>
|
|
@@ -289,6 +289,6 @@ export const Card = (props: CardProps) => {
|
|
|
289
289
|
)
|
|
290
290
|
const ctx = generateContext(tmpDir)
|
|
291
291
|
expect(ctx.components).toHaveLength(1)
|
|
292
|
-
expect(ctx.components[0]?.name).toBe(
|
|
292
|
+
expect(ctx.components[0]?.name).toBe('Card')
|
|
293
293
|
})
|
|
294
294
|
})
|