@jlcpcb/core 0.1.0 → 0.2.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/CHANGELOG.md +33 -0
- package/dist/api/easyeda-community.d.ts +36 -0
- package/dist/api/easyeda-community.d.ts.map +1 -0
- package/dist/api/easyeda.d.ts +23 -0
- package/dist/api/easyeda.d.ts.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/jlc.d.ts +41 -0
- package/dist/api/jlc.d.ts.map +1 -0
- package/dist/assets/search.html +528 -0
- package/dist/browser/index.d.ts +8 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/kicad-renderer.d.ts +13 -0
- package/dist/browser/kicad-renderer.d.ts.map +1 -0
- package/dist/browser/sexpr-parser.d.ts +84 -0
- package/dist/browser/sexpr-parser.d.ts.map +1 -0
- package/dist/constants/design-rules.d.ts +34 -0
- package/dist/constants/design-rules.d.ts.map +1 -0
- package/dist/constants/footprints.d.ts +134 -0
- package/dist/constants/footprints.d.ts.map +1 -0
- package/dist/constants/index.d.ts +7 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/kicad.d.ts +67 -0
- package/dist/constants/kicad.d.ts.map +1 -0
- package/dist/converter/category-router.d.ts +47 -0
- package/dist/converter/category-router.d.ts.map +1 -0
- package/dist/converter/footprint-mapper.d.ts +40 -0
- package/dist/converter/footprint-mapper.d.ts.map +1 -0
- package/dist/converter/footprint-mapper.test.d.ts +2 -0
- package/dist/converter/footprint-mapper.test.d.ts.map +1 -0
- package/dist/converter/footprint.d.ts +116 -0
- package/dist/converter/footprint.d.ts.map +1 -0
- package/dist/converter/global-lib-table.d.ts +29 -0
- package/dist/converter/global-lib-table.d.ts.map +1 -0
- package/dist/converter/index.d.ts +12 -0
- package/dist/converter/index.d.ts.map +1 -0
- package/dist/converter/lib-table.d.ts +61 -0
- package/dist/converter/lib-table.d.ts.map +1 -0
- package/dist/converter/svg-arc.d.ts +45 -0
- package/dist/converter/svg-arc.d.ts.map +1 -0
- package/dist/converter/symbol-templates.d.ts +34 -0
- package/dist/converter/symbol-templates.d.ts.map +1 -0
- package/dist/converter/symbol.d.ts +223 -0
- package/dist/converter/symbol.d.ts.map +1 -0
- package/dist/converter/value-normalizer.d.ts +33 -0
- package/dist/converter/value-normalizer.d.ts.map +1 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/routes.d.ts +12 -0
- package/dist/http/routes.d.ts.map +1 -0
- package/dist/http/server.d.ts +18 -0
- package/dist/http/server.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9633 -0
- package/dist/parsers/easyeda-shapes.d.ts +115 -0
- package/dist/parsers/easyeda-shapes.d.ts.map +1 -0
- package/dist/parsers/http-client.d.ts +16 -0
- package/dist/parsers/http-client.d.ts.map +1 -0
- package/dist/parsers/index.d.ts +11 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/utils.d.ts +17 -0
- package/dist/parsers/utils.d.ts.map +1 -0
- package/dist/services/component-service.d.ts +31 -0
- package/dist/services/component-service.d.ts.map +1 -0
- package/dist/services/fix-service.d.ts +40 -0
- package/dist/services/fix-service.d.ts.map +1 -0
- package/dist/services/index.d.ts +8 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/library-service.d.ts +112 -0
- package/dist/services/library-service.d.ts.map +1 -0
- package/dist/types/component.d.ts +56 -0
- package/dist/types/component.d.ts.map +1 -0
- package/dist/types/easyeda-community.d.ts +74 -0
- package/dist/types/easyeda-community.d.ts.map +1 -0
- package/dist/types/easyeda.d.ts +326 -0
- package/dist/types/easyeda.d.ts.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/jlc.d.ts +78 -0
- package/dist/types/jlc.d.ts.map +1 -0
- package/dist/types/kicad.d.ts +141 -0
- package/dist/types/kicad.d.ts.map +1 -0
- package/dist/types/mcp.d.ts +66 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/project.d.ts +60 -0
- package/dist/types/project.d.ts.map +1 -0
- package/dist/utils/conversion.d.ts +59 -0
- package/dist/utils/conversion.d.ts.map +1 -0
- package/dist/utils/file-system.d.ts +59 -0
- package/dist/utils/file-system.d.ts.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +26 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/validation.d.ts +259 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/package.json +5 -3
- package/scripts/build-search-page.ts +68 -0
- package/src/assets/search-built.html +528 -0
- package/src/assets/search.html +458 -0
- package/src/browser/index.ts +389 -0
- package/src/browser/kicad-renderer.ts +813 -0
- package/src/browser/sexpr-parser.ts +333 -0
- package/src/converter/footprint-mapper.test.ts +159 -0
- package/src/converter/footprint-mapper.ts +42 -134
- package/src/converter/footprint.ts +208 -36
- package/src/converter/global-lib-table.ts +71 -0
- package/src/http/index.ts +5 -0
- package/src/http/routes.ts +266 -0
- package/src/http/server.ts +83 -0
- package/src/index.ts +3 -0
- package/src/parsers/easyeda-shapes.ts +2 -1
- package/src/services/library-service.ts +73 -22
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP routes for the EasyEDA Component Browser
|
|
3
|
+
* Handles serving the HTML page and proxying API requests
|
|
4
|
+
*
|
|
5
|
+
* Returns KiCad S-expression strings for symbol/footprint preview rendering
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'fs'
|
|
9
|
+
import { join, dirname } from 'path'
|
|
10
|
+
import { fileURLToPath } from 'url'
|
|
11
|
+
import type { IncomingMessage, ServerResponse } from 'http'
|
|
12
|
+
import { createLogger } from '../utils/logger.js'
|
|
13
|
+
import type { EasyEDAComponentData, EasyEDACommunityComponent } from '../types/index.js'
|
|
14
|
+
import { easyedaCommunityClient } from '../api/easyeda-community.js'
|
|
15
|
+
import { symbolConverter } from '../converter/symbol.js'
|
|
16
|
+
import { footprintConverter } from '../converter/footprint.js'
|
|
17
|
+
|
|
18
|
+
const logger = createLogger('http-routes')
|
|
19
|
+
|
|
20
|
+
// Get the directory of this file to find assets
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
22
|
+
const __dirname = dirname(__filename)
|
|
23
|
+
|
|
24
|
+
// Cache the HTML content in memory
|
|
25
|
+
let htmlCache: string | null = null
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load the HTML page from the assets directory
|
|
29
|
+
*/
|
|
30
|
+
function getHtmlPage(): string {
|
|
31
|
+
if (htmlCache) return htmlCache
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// When bundled, __dirname points to dist/ since everything is in index.js
|
|
35
|
+
// When running from source, __dirname is src/http/
|
|
36
|
+
const possiblePaths = [
|
|
37
|
+
// Bundled: dist/index.js looking for dist/assets/search.html
|
|
38
|
+
join(__dirname, 'assets/search.html'),
|
|
39
|
+
// Bundled alternative path
|
|
40
|
+
join(__dirname, '../dist/assets/search.html'),
|
|
41
|
+
// Source: src/http/ looking for src/assets/
|
|
42
|
+
join(__dirname, '../assets/search.html'),
|
|
43
|
+
join(__dirname, '../assets/search-built.html'),
|
|
44
|
+
// From project root (core package)
|
|
45
|
+
join(process.cwd(), 'dist/assets/search.html'),
|
|
46
|
+
join(process.cwd(), 'packages/core/dist/assets/search.html'),
|
|
47
|
+
// When imported from other packages
|
|
48
|
+
join(__dirname, '../../dist/assets/search.html'),
|
|
49
|
+
// When bundled into CLI or other package, look for core's assets relative to monorepo
|
|
50
|
+
join(__dirname, '../../../core/dist/assets/search.html'),
|
|
51
|
+
join(__dirname, '../../../../packages/core/dist/assets/search.html'),
|
|
52
|
+
// Look in node_modules if installed as dependency
|
|
53
|
+
join(__dirname, '../../node_modules/@jlcpcb/core/dist/assets/search.html'),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
for (const path of possiblePaths) {
|
|
57
|
+
try {
|
|
58
|
+
htmlCache = readFileSync(path, 'utf-8')
|
|
59
|
+
logger.debug(`Loaded HTML from: ${path}`)
|
|
60
|
+
return htmlCache
|
|
61
|
+
} catch {
|
|
62
|
+
// Try next path
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new Error('HTML file not found')
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.error('Failed to load HTML page:', error)
|
|
69
|
+
return `<!DOCTYPE html>
|
|
70
|
+
<html>
|
|
71
|
+
<head><title>Error</title></head>
|
|
72
|
+
<body>
|
|
73
|
+
<h1>Error: Search page not found</h1>
|
|
74
|
+
<p>The search page has not been built. Run: bun run build</p>
|
|
75
|
+
</body>
|
|
76
|
+
</html>`
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Main request handler
|
|
82
|
+
*/
|
|
83
|
+
export async function handleRequest(
|
|
84
|
+
req: IncomingMessage,
|
|
85
|
+
res: ServerResponse
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
|
|
88
|
+
const pathname = url.pathname
|
|
89
|
+
|
|
90
|
+
logger.debug(`${req.method} ${pathname}`)
|
|
91
|
+
|
|
92
|
+
// Route requests
|
|
93
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
94
|
+
const query = url.searchParams.get('q') || undefined
|
|
95
|
+
serveHtml(res, query)
|
|
96
|
+
} else if (pathname === '/api/search') {
|
|
97
|
+
await handleSearch(url, res)
|
|
98
|
+
} else if (pathname.startsWith('/api/component/')) {
|
|
99
|
+
const uuid = pathname.replace('/api/component/', '')
|
|
100
|
+
await handleComponent(uuid, res)
|
|
101
|
+
} else if (pathname === '/health') {
|
|
102
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
103
|
+
res.end(JSON.stringify({ status: 'ok' }))
|
|
104
|
+
} else {
|
|
105
|
+
res.writeHead(404, { 'Content-Type': 'application/json' })
|
|
106
|
+
res.end(JSON.stringify({ error: 'Not found' }))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Serve the HTML page with optional initial query injection
|
|
112
|
+
*/
|
|
113
|
+
function serveHtml(res: ServerResponse, initialQuery?: string): void {
|
|
114
|
+
let html = getHtmlPage()
|
|
115
|
+
|
|
116
|
+
if (initialQuery) {
|
|
117
|
+
// Inject initial query via a script tag before </head>
|
|
118
|
+
const script = `<script>window.__INITIAL_QUERY__ = ${JSON.stringify(initialQuery)};</script>`
|
|
119
|
+
html = html.replace('</head>', `${script}</head>`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
123
|
+
res.end(html)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Handle search API requests
|
|
128
|
+
* GET /api/search?q=query&source=user|lcsc|all&page=1&limit=20
|
|
129
|
+
*/
|
|
130
|
+
async function handleSearch(
|
|
131
|
+
url: URL,
|
|
132
|
+
res: ServerResponse
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
const query = url.searchParams.get('q') || ''
|
|
135
|
+
const source = url.searchParams.get('source') || 'user'
|
|
136
|
+
const page = parseInt(url.searchParams.get('page') || '1', 10)
|
|
137
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10)
|
|
138
|
+
|
|
139
|
+
if (!query) {
|
|
140
|
+
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
141
|
+
res.end(JSON.stringify({ error: 'Missing query parameter' }))
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// Fetch more than we need for pagination (EasyEDA doesn't support offset)
|
|
147
|
+
const allResults = await easyedaCommunityClient.search({
|
|
148
|
+
query,
|
|
149
|
+
source: source as 'user' | 'lcsc' | 'easyeda' | 'all',
|
|
150
|
+
limit: Math.min(limit * page + limit, 100), // Max 100 results
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Calculate pagination
|
|
154
|
+
const startIndex = (page - 1) * limit
|
|
155
|
+
const endIndex = startIndex + limit
|
|
156
|
+
const results = allResults.slice(startIndex, endIndex)
|
|
157
|
+
const totalPages = Math.ceil(allResults.length / limit)
|
|
158
|
+
|
|
159
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
160
|
+
res.end(JSON.stringify({
|
|
161
|
+
results,
|
|
162
|
+
pagination: {
|
|
163
|
+
page,
|
|
164
|
+
limit,
|
|
165
|
+
total: allResults.length,
|
|
166
|
+
totalPages,
|
|
167
|
+
hasNext: page < totalPages,
|
|
168
|
+
hasPrev: page > 1,
|
|
169
|
+
},
|
|
170
|
+
}))
|
|
171
|
+
} catch (error) {
|
|
172
|
+
logger.error('Search error:', error)
|
|
173
|
+
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
174
|
+
res.end(JSON.stringify({ error: 'Search failed' }))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Convert EasyEDACommunityComponent to EasyEDAComponentData format
|
|
180
|
+
* Required because converters expect the LCSC-style structure
|
|
181
|
+
*/
|
|
182
|
+
function communityToComponentData(
|
|
183
|
+
community: EasyEDACommunityComponent
|
|
184
|
+
): EasyEDAComponentData {
|
|
185
|
+
// Extract component info from the head c_para
|
|
186
|
+
const cPara = community.symbol.head?.c_para as Record<string, string> | undefined ?? {}
|
|
187
|
+
const fpCPara = community.footprint.head?.c_para as Record<string, string> | undefined ?? {}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
info: {
|
|
191
|
+
name: community.title || cPara.name || 'Unknown',
|
|
192
|
+
prefix: cPara.pre || cPara.Prefix || 'U',
|
|
193
|
+
package: fpCPara.package || community.footprint.name,
|
|
194
|
+
manufacturer: cPara.Manufacturer || cPara.BOM_Manufacturer,
|
|
195
|
+
description: community.description || cPara.BOM_Manufacturer_Part,
|
|
196
|
+
category: cPara.package, // Best guess for category
|
|
197
|
+
},
|
|
198
|
+
symbol: community.symbol,
|
|
199
|
+
footprint: community.footprint,
|
|
200
|
+
model3d: community.model3d,
|
|
201
|
+
rawData: community.rawData,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Handle component fetch API requests
|
|
207
|
+
* GET /api/component/:uuid
|
|
208
|
+
*
|
|
209
|
+
* Returns KiCad S-expression strings for browser-side rendering
|
|
210
|
+
*/
|
|
211
|
+
async function handleComponent(
|
|
212
|
+
uuid: string,
|
|
213
|
+
res: ServerResponse
|
|
214
|
+
): Promise<void> {
|
|
215
|
+
if (!uuid) {
|
|
216
|
+
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
217
|
+
res.end(JSON.stringify({ error: 'Missing UUID' }))
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const component = await easyedaCommunityClient.getComponent(uuid)
|
|
223
|
+
|
|
224
|
+
if (!component) {
|
|
225
|
+
res.writeHead(404, { 'Content-Type': 'application/json' })
|
|
226
|
+
res.end(JSON.stringify({ error: 'Component not found' }))
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Convert community component to converter-compatible format
|
|
231
|
+
const componentData = communityToComponentData(component)
|
|
232
|
+
|
|
233
|
+
// Generate KiCad S-expression strings using existing converters
|
|
234
|
+
let symbolSexpr = ''
|
|
235
|
+
let footprintSexpr = ''
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Get just the symbol entry (not the full library wrapper)
|
|
239
|
+
symbolSexpr = symbolConverter.convertToSymbolEntry(componentData)
|
|
240
|
+
} catch (e) {
|
|
241
|
+
logger.warn('Symbol conversion failed:', e)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
footprintSexpr = footprintConverter.convert(componentData)
|
|
246
|
+
} catch (e) {
|
|
247
|
+
logger.warn('Footprint conversion failed:', e)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Return S-expr strings for browser rendering
|
|
251
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
252
|
+
res.end(JSON.stringify({
|
|
253
|
+
uuid: component.uuid,
|
|
254
|
+
title: component.title,
|
|
255
|
+
description: component.description,
|
|
256
|
+
owner: component.owner,
|
|
257
|
+
symbolSexpr,
|
|
258
|
+
footprintSexpr,
|
|
259
|
+
model3d: component.model3d,
|
|
260
|
+
}))
|
|
261
|
+
} catch (error) {
|
|
262
|
+
logger.error('Component fetch error:', error)
|
|
263
|
+
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
264
|
+
res.end(JSON.stringify({ error: 'Failed to fetch component' }))
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP server for the EasyEDA Component Browser
|
|
3
|
+
* Runs alongside the MCP stdio server to serve the web UI and proxy API requests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'http'
|
|
7
|
+
import { createLogger } from '../utils/logger.js'
|
|
8
|
+
import { handleRequest } from './routes.js'
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('http-server')
|
|
11
|
+
|
|
12
|
+
const DEFAULT_PORT = 3847
|
|
13
|
+
|
|
14
|
+
let serverInstance: ReturnType<typeof createServer> | null = null
|
|
15
|
+
|
|
16
|
+
export interface HttpServerOptions {
|
|
17
|
+
port?: number
|
|
18
|
+
onReady?: (url: string) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Start the HTTP server
|
|
23
|
+
* @returns The port the server is listening on
|
|
24
|
+
*/
|
|
25
|
+
export function startHttpServer(options: HttpServerOptions = {}): number {
|
|
26
|
+
if (serverInstance) {
|
|
27
|
+
logger.debug('HTTP server already running')
|
|
28
|
+
const port = options.port ?? parseInt(process.env.JLC_MCP_HTTP_PORT || String(DEFAULT_PORT), 10)
|
|
29
|
+
options.onReady?.(`http://localhost:${port}`)
|
|
30
|
+
return port
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const port = options.port ?? parseInt(process.env.JLC_MCP_HTTP_PORT || String(DEFAULT_PORT), 10)
|
|
34
|
+
|
|
35
|
+
serverInstance = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
36
|
+
// Add CORS headers to all responses
|
|
37
|
+
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
38
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
39
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
|
40
|
+
|
|
41
|
+
// Handle preflight requests
|
|
42
|
+
if (req.method === 'OPTIONS') {
|
|
43
|
+
res.writeHead(204)
|
|
44
|
+
res.end()
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await handleRequest(req, res)
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.error('Request error:', error)
|
|
52
|
+
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
53
|
+
res.end(JSON.stringify({ error: 'Internal server error' }))
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
serverInstance.listen(port, () => {
|
|
58
|
+
const url = `http://localhost:${port}`
|
|
59
|
+
logger.info(`HTTP server listening on ${url}`)
|
|
60
|
+
options.onReady?.(url)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
serverInstance.on('error', (error: NodeJS.ErrnoException) => {
|
|
64
|
+
if (error.code === 'EADDRINUSE') {
|
|
65
|
+
logger.warn(`Port ${port} already in use, HTTP server not started`)
|
|
66
|
+
} else {
|
|
67
|
+
logger.error('HTTP server error:', error)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return port
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Stop the HTTP server
|
|
76
|
+
*/
|
|
77
|
+
export function stopHttpServer(): void {
|
|
78
|
+
if (serverInstance) {
|
|
79
|
+
serverInstance.close()
|
|
80
|
+
serverInstance = null
|
|
81
|
+
logger.info('HTTP server stopped')
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -609,7 +609,8 @@ export function parseFootprintShapes(shapes: string[]): ParsedFootprintData {
|
|
|
609
609
|
}
|
|
610
610
|
|
|
611
611
|
// Determine type based on pads
|
|
612
|
-
|
|
612
|
+
// Check both holeRadius > 0 and isPlated polygon pads (EasyEDA often has holeRadius=0 for polygon THT)
|
|
613
|
+
const type = pads.some(p => p.holeRadius > 0 || (p.shape === 'POLYGON' && p.isPlated)) ? 'tht' : 'smd';
|
|
613
614
|
|
|
614
615
|
return {
|
|
615
616
|
name: 'Unknown', // Will be set by caller
|
|
@@ -38,12 +38,18 @@ const LIBRARY_NAMESPACE = 'jlc_mcp';
|
|
|
38
38
|
// KiCad versions to check
|
|
39
39
|
const KICAD_VERSIONS = ['9.0', '8.0'];
|
|
40
40
|
|
|
41
|
-
// EasyEDA community library naming
|
|
41
|
+
// EasyEDA community library naming (global)
|
|
42
42
|
const EASYEDA_LIBRARY_NAME = 'EasyEDA';
|
|
43
43
|
const EASYEDA_SYMBOL_LIBRARY_NAME = 'EasyEDA.kicad_sym';
|
|
44
44
|
const EASYEDA_FOOTPRINT_LIBRARY_NAME = 'EasyEDA.pretty';
|
|
45
45
|
const EASYEDA_LIBRARY_DESCRIPTION = 'EasyEDA Community Component Library';
|
|
46
46
|
|
|
47
|
+
// EasyEDA community library naming (project-local) - different name to avoid collision
|
|
48
|
+
const EASYEDA_LOCAL_LIBRARY_NAME = 'EasyEDA-local';
|
|
49
|
+
const EASYEDA_LOCAL_SYMBOL_LIBRARY_NAME = 'EasyEDA-local.kicad_sym';
|
|
50
|
+
const EASYEDA_LOCAL_FOOTPRINT_LIBRARY_NAME = 'EasyEDA-local.pretty';
|
|
51
|
+
const EASYEDA_LOCAL_LIBRARY_DESCRIPTION = 'EasyEDA Community Component Library (Project-local)';
|
|
52
|
+
|
|
47
53
|
export interface InstallOptions {
|
|
48
54
|
projectPath?: string;
|
|
49
55
|
include3d?: boolean;
|
|
@@ -144,6 +150,7 @@ export interface LibraryService {
|
|
|
144
150
|
update(options?: UpdateOptions): Promise<UpdateResult>;
|
|
145
151
|
ensureGlobalTables(): Promise<void>;
|
|
146
152
|
getStatus(): Promise<LibraryStatus>;
|
|
153
|
+
isEasyEDAInstalled(componentName: string): Promise<boolean>;
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
interface LibraryPaths {
|
|
@@ -335,13 +342,8 @@ export function createLibraryService(): LibraryService {
|
|
|
335
342
|
async install(id: string, options: InstallOptions = {}): Promise<InstallResult> {
|
|
336
343
|
const isCommunityComponent = !isLcscId(id);
|
|
337
344
|
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
throw new Error('EasyEDA community components require projectPath for local storage');
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Determine storage location
|
|
344
|
-
const isGlobal = !isCommunityComponent && !options.projectPath;
|
|
345
|
+
// Determine storage location (global if no projectPath provided)
|
|
346
|
+
const isGlobal = !options.projectPath;
|
|
345
347
|
const paths = isGlobal
|
|
346
348
|
? getGlobalLibraryPaths()
|
|
347
349
|
: getProjectLibraryPaths(options.projectPath!);
|
|
@@ -397,16 +399,35 @@ export function createLibraryService(): LibraryService {
|
|
|
397
399
|
let modelPath: string | undefined;
|
|
398
400
|
|
|
399
401
|
if (isCommunityComponent) {
|
|
400
|
-
// EasyEDA community component → project-local
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
402
|
+
// EasyEDA community component → EasyEDA library (global or project-local)
|
|
403
|
+
// Use different library names to avoid collision
|
|
404
|
+
const libName = isGlobal ? EASYEDA_LIBRARY_NAME : EASYEDA_LOCAL_LIBRARY_NAME;
|
|
405
|
+
const symLibFile = isGlobal ? EASYEDA_SYMBOL_LIBRARY_NAME : EASYEDA_LOCAL_SYMBOL_LIBRARY_NAME;
|
|
406
|
+
const fpLibDir = isGlobal ? EASYEDA_FOOTPRINT_LIBRARY_NAME : EASYEDA_LOCAL_FOOTPRINT_LIBRARY_NAME;
|
|
407
|
+
const libDesc = isGlobal ? EASYEDA_LIBRARY_DESCRIPTION : EASYEDA_LOCAL_LIBRARY_DESCRIPTION;
|
|
408
|
+
const models3dDirName = isGlobal ? 'EasyEDA.3dshapes' : 'EasyEDA-local.3dshapes';
|
|
409
|
+
|
|
410
|
+
let symbolsDir: string;
|
|
411
|
+
let easyedaModelsDir: string;
|
|
412
|
+
|
|
413
|
+
if (isGlobal) {
|
|
414
|
+
// Global: use KiCad 3rd party directory
|
|
415
|
+
symbolsDir = paths.symbolsDir;
|
|
416
|
+
footprintDir = join(paths.footprintsDir, fpLibDir);
|
|
417
|
+
easyedaModelsDir = join(paths.models3dDir, models3dDirName);
|
|
418
|
+
} else {
|
|
419
|
+
// Project-local
|
|
420
|
+
const librariesDir = join(options.projectPath!, 'libraries');
|
|
421
|
+
symbolsDir = join(librariesDir, 'symbols');
|
|
422
|
+
footprintDir = join(librariesDir, 'footprints', fpLibDir);
|
|
423
|
+
easyedaModelsDir = join(librariesDir, '3dmodels', models3dDirName);
|
|
424
|
+
}
|
|
425
|
+
models3dDir = easyedaModelsDir;
|
|
405
426
|
|
|
406
427
|
await ensureDir(symbolsDir);
|
|
407
428
|
await ensureDir(footprintDir);
|
|
408
429
|
|
|
409
|
-
symbolFile = join(symbolsDir,
|
|
430
|
+
symbolFile = join(symbolsDir, symLibFile);
|
|
410
431
|
symbolName = component.info.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
411
432
|
|
|
412
433
|
// Download 3D model first if available (needed for footprint generation)
|
|
@@ -421,28 +442,38 @@ export function createLibraryService(): LibraryService {
|
|
|
421
442
|
const modelFilename = `${symbolName}.step`;
|
|
422
443
|
modelPath = join(models3dDir, modelFilename);
|
|
423
444
|
await writeBinary(modelPath, model);
|
|
424
|
-
// Use
|
|
425
|
-
|
|
445
|
+
// Use appropriate path variable
|
|
446
|
+
if (isGlobal) {
|
|
447
|
+
modelRelativePath = `\${KICAD9_3RD_PARTY}/${LIBRARY_NAMESPACE}/3dmodels/${models3dDirName}/${modelFilename}`;
|
|
448
|
+
} else {
|
|
449
|
+
modelRelativePath = `\${KIPRJMOD}/libraries/3dmodels/${models3dDirName}/${modelFilename}`;
|
|
450
|
+
}
|
|
426
451
|
}
|
|
427
452
|
}
|
|
428
453
|
|
|
429
454
|
// Generate custom footprint with 3D model
|
|
430
455
|
const footprint = footprintConverter.convert(component, {
|
|
431
|
-
libraryName:
|
|
456
|
+
libraryName: libName,
|
|
432
457
|
include3DModel: !!modelRelativePath,
|
|
433
458
|
modelPath: modelRelativePath,
|
|
434
459
|
});
|
|
435
460
|
footprintPath = join(footprintDir, `${symbolName}.kicad_mod`);
|
|
436
|
-
footprintRef = `${
|
|
461
|
+
footprintRef = `${libName}:${symbolName}`;
|
|
437
462
|
await writeText(footprintPath, footprint);
|
|
438
463
|
|
|
439
464
|
component.info.package = footprintRef;
|
|
440
465
|
|
|
441
|
-
// Update lib tables
|
|
442
|
-
|
|
443
|
-
|
|
466
|
+
// Update lib tables (global or project-local)
|
|
467
|
+
if (isGlobal) {
|
|
468
|
+
// For global, use the global lib table registration
|
|
469
|
+
const { ensureGlobalEasyEDALibrary } = await import('../converter/global-lib-table.js');
|
|
470
|
+
await ensureGlobalEasyEDALibrary();
|
|
471
|
+
} else {
|
|
472
|
+
await ensureSymLibTable(options.projectPath!, symbolFile, libName, libDesc);
|
|
473
|
+
await ensureFpLibTable(options.projectPath!, footprintDir, libName, libDesc);
|
|
474
|
+
}
|
|
444
475
|
|
|
445
|
-
symbolRef = `${
|
|
476
|
+
symbolRef = `${libName}:${symbolName}`;
|
|
446
477
|
} else {
|
|
447
478
|
// LCSC component → JLC-MCP category-based library
|
|
448
479
|
category = getLibraryCategory(
|
|
@@ -692,5 +723,25 @@ export function createLibraryService(): LibraryService {
|
|
|
692
723
|
},
|
|
693
724
|
};
|
|
694
725
|
},
|
|
726
|
+
|
|
727
|
+
async isEasyEDAInstalled(componentName: string): Promise<boolean> {
|
|
728
|
+
const paths = getGlobalLibraryPaths();
|
|
729
|
+
const easyedaLibPath = join(paths.symbolsDir, EASYEDA_SYMBOL_LIBRARY_NAME);
|
|
730
|
+
|
|
731
|
+
if (!existsSync(easyedaLibPath)) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
const content = await readFile(easyedaLibPath, 'utf-8');
|
|
737
|
+
// Sanitize the name the same way we do when installing
|
|
738
|
+
const sanitizedName = componentName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
739
|
+
// Check if symbol exists in library
|
|
740
|
+
const pattern = new RegExp(`\\(symbol\\s+"${sanitizedName}"`, 'm');
|
|
741
|
+
return pattern.test(content);
|
|
742
|
+
} catch {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
},
|
|
695
746
|
};
|
|
696
747
|
}
|