@jlcpcb/cli 0.3.1 → 0.4.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 +18 -0
- package/dist/index.js +501 -172
- package/package.json +2 -2
- package/src/app/components/ListView.tsx +1 -1
- package/src/app/components/list-view-format.test.ts +2 -0
- package/src/app/screens/InfoScreen.tsx +22 -6
- package/src/app/screens/InstalledScreen.tsx +1 -1
- package/src/app/screens/SearchScreen.tsx +11 -4
- package/src/commands/agent-json.test.ts +203 -0
- package/src/commands/easyeda.ts +63 -10
- package/src/commands/info.ts +6 -9
- package/src/commands/install.ts +39 -8
- package/src/commands/library.ts +13 -23
- package/src/commands/search.ts +35 -3
- package/src/commands/validate.ts +95 -43
- package/src/index.ts +38 -6
- package/src/package-metadata.test.ts +25 -0
- package/src/utils/agent-output.ts +35 -0
- package/src/utils/search-result-output.ts +20 -0
package/src/commands/search.ts
CHANGED
|
@@ -5,11 +5,19 @@
|
|
|
5
5
|
|
|
6
6
|
import { createComponentService, type SearchOptions } from '@jlcpcb/core';
|
|
7
7
|
import { renderApp } from '../app/App.js';
|
|
8
|
+
import { printJson, printJsonError, getErrorMessage } from '../utils/agent-output.js';
|
|
9
|
+
import { formatSearchResultForJson } from '../utils/search-result-output.js';
|
|
8
10
|
|
|
9
11
|
const componentService = createComponentService();
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
interface SearchCommandOptions extends SearchOptions {
|
|
14
|
+
json?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function searchCommand(query: string, options: SearchCommandOptions): Promise<void> {
|
|
18
|
+
if (!options.json) {
|
|
19
|
+
console.log(`Searching for "${query}"...`);
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
try {
|
|
15
23
|
let results = await componentService.search(query, options);
|
|
@@ -24,15 +32,39 @@ export async function searchCommand(query: string, options: SearchOptions): Prom
|
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
if (results.length === 0) {
|
|
35
|
+
if (options.json) {
|
|
36
|
+
printJson({
|
|
37
|
+
success: true,
|
|
38
|
+
query,
|
|
39
|
+
count: 0,
|
|
40
|
+
results: [],
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
console.log('No components found. Try a different search term.');
|
|
28
46
|
return;
|
|
29
47
|
}
|
|
30
48
|
|
|
49
|
+
if (options.json) {
|
|
50
|
+
printJson({
|
|
51
|
+
success: true,
|
|
52
|
+
query,
|
|
53
|
+
count: results.length,
|
|
54
|
+
results: results.map(formatSearchResultForJson),
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
// Clear the "Searching..." line and launch interactive UI
|
|
32
60
|
process.stdout.write('\x1b[1A\x1b[2K');
|
|
33
61
|
await renderApp('search', { query, results });
|
|
34
62
|
} catch (error) {
|
|
35
|
-
|
|
63
|
+
if (options.json) {
|
|
64
|
+
printJsonError('search_failed', getErrorMessage(error), { retryable: true });
|
|
65
|
+
} else {
|
|
66
|
+
console.error(`Search failed: ${getErrorMessage(error)}`);
|
|
67
|
+
}
|
|
36
68
|
process.exit(1);
|
|
37
69
|
}
|
|
38
70
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
footprintConverter,
|
|
31
31
|
symbolConverter,
|
|
32
32
|
} from '@jlcpcb/core';
|
|
33
|
+
import { printJson, printJsonError } from '../utils/agent-output.js';
|
|
33
34
|
|
|
34
35
|
interface ValidateOptions {
|
|
35
36
|
footprintOnly?: boolean;
|
|
@@ -130,46 +131,32 @@ export async function validateCommand(
|
|
|
130
131
|
id: string | undefined,
|
|
131
132
|
options: ValidateOptions
|
|
132
133
|
): Promise<void> {
|
|
134
|
+
if (options.footprintOnly && options.symbolOnly) {
|
|
135
|
+
if (options.json) {
|
|
136
|
+
printJsonError('invalid_options', '--footprint-only and --symbol-only are mutually exclusive');
|
|
137
|
+
} else {
|
|
138
|
+
p.log.error('--footprint-only and --symbol-only are mutually exclusive');
|
|
139
|
+
}
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
133
143
|
const validateFootprint = !options.symbolOnly;
|
|
134
144
|
const validateSymbol = !options.footprintOnly;
|
|
135
145
|
|
|
136
146
|
// Single component validation
|
|
137
147
|
if (id && !options.category && !options.all) {
|
|
138
|
-
const spinner = p.spinner();
|
|
139
|
-
spinner
|
|
148
|
+
const spinner = options.json ? null : p.spinner();
|
|
149
|
+
spinner?.start(`Validating ${id}...`);
|
|
140
150
|
|
|
141
151
|
const result = await validateComponent(id, {
|
|
142
152
|
footprint: validateFootprint,
|
|
143
153
|
symbol: validateSymbol,
|
|
144
154
|
});
|
|
145
155
|
|
|
146
|
-
spinner
|
|
147
|
-
|
|
148
|
-
// Display results
|
|
149
|
-
console.log();
|
|
150
|
-
console.log(chalk.bold(`${result.componentName} (${result.lcscCode})`));
|
|
151
|
-
console.log(chalk.dim(`Duration: ${result.durationMs}ms`));
|
|
152
|
-
console.log();
|
|
153
|
-
|
|
154
|
-
if (result.error) {
|
|
155
|
-
console.log(chalk.red(`Error: ${result.error}`));
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
156
|
+
spinner?.stop(result.passed ? chalk.green('✓ Validation passed') : chalk.red('✗ Validation failed'));
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
console.log(formatComparisonResult(result.footprint));
|
|
161
|
-
}
|
|
158
|
+
let htmlPath: string | undefined;
|
|
162
159
|
|
|
163
|
-
if (result.symbol) {
|
|
164
|
-
console.log(formatSymbolComparisonResult(result.symbol));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// JSON output
|
|
168
|
-
if (options.json) {
|
|
169
|
-
console.log(JSON.stringify(result, null, 2));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// HTML output
|
|
173
160
|
if (options.html) {
|
|
174
161
|
const reference = await fetchReferenceSVG(id);
|
|
175
162
|
const componentData = await easyedaClient.getComponentData(id);
|
|
@@ -195,9 +182,42 @@ export async function validateCommand(
|
|
|
195
182
|
|
|
196
183
|
const html = generateValidationReport(result, svgs);
|
|
197
184
|
writeFileSync(options.html, html);
|
|
198
|
-
|
|
185
|
+
htmlPath = options.html;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (options.json) {
|
|
189
|
+
printJson({
|
|
190
|
+
success: result.passed && !result.error,
|
|
191
|
+
result,
|
|
192
|
+
html: htmlPath,
|
|
193
|
+
});
|
|
194
|
+
process.exit(result.passed && !result.error ? 0 : 1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Display results
|
|
198
|
+
console.log();
|
|
199
|
+
console.log(chalk.bold(`${result.componentName} (${result.lcscCode})`));
|
|
200
|
+
console.log(chalk.dim(`Duration: ${result.durationMs}ms`));
|
|
201
|
+
console.log();
|
|
202
|
+
|
|
203
|
+
if (result.error) {
|
|
204
|
+
console.log(chalk.red(`Error: ${result.error}`));
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (result.footprint) {
|
|
209
|
+
console.log(formatComparisonResult(result.footprint));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (result.symbol) {
|
|
213
|
+
console.log(formatSymbolComparisonResult(result.symbol));
|
|
199
214
|
}
|
|
200
215
|
|
|
216
|
+
if (htmlPath) {
|
|
217
|
+
console.log(chalk.dim(`HTML report: ${htmlPath}`));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
201
221
|
process.exit(result.passed ? 0 : 1);
|
|
202
222
|
}
|
|
203
223
|
|
|
@@ -209,6 +229,11 @@ export async function validateCommand(
|
|
|
209
229
|
: [];
|
|
210
230
|
|
|
211
231
|
if (components.length === 0 && !id) {
|
|
232
|
+
if (options.json) {
|
|
233
|
+
printJsonError('missing_validation_target', 'JSON validation requires an id, --category, or --all');
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
212
237
|
// Interactive category selection
|
|
213
238
|
const categories = getCategoryNames();
|
|
214
239
|
const selected = await p.select({
|
|
@@ -232,11 +257,22 @@ export async function validateCommand(
|
|
|
232
257
|
components = components.filter((c) => !c.skip);
|
|
233
258
|
|
|
234
259
|
if (components.length === 0) {
|
|
260
|
+
if (options.json) {
|
|
261
|
+
printJson({
|
|
262
|
+
success: true,
|
|
263
|
+
summary: { total: 0, passed: 0, failed: 0, errors: 0 },
|
|
264
|
+
results: [],
|
|
265
|
+
});
|
|
266
|
+
process.exit(0);
|
|
267
|
+
}
|
|
268
|
+
|
|
235
269
|
p.log.warn('No components to validate');
|
|
236
270
|
process.exit(0);
|
|
237
271
|
}
|
|
238
272
|
|
|
239
|
-
|
|
273
|
+
if (!options.json) {
|
|
274
|
+
console.log(chalk.bold(`\nValidating ${components.length} components...\n`));
|
|
275
|
+
}
|
|
240
276
|
|
|
241
277
|
const results: ValidationResult[] = [];
|
|
242
278
|
let passed = 0;
|
|
@@ -244,7 +280,9 @@ export async function validateCommand(
|
|
|
244
280
|
let errors = 0;
|
|
245
281
|
|
|
246
282
|
for (const component of components) {
|
|
247
|
-
|
|
283
|
+
if (!options.json) {
|
|
284
|
+
process.stdout.write(` ${component.lcsc.padEnd(12)} ${component.name.slice(0, 40).padEnd(42)} `);
|
|
285
|
+
}
|
|
248
286
|
|
|
249
287
|
const result = await validateComponent(component.lcsc, {
|
|
250
288
|
footprint: validateFootprint,
|
|
@@ -255,32 +293,46 @@ export async function validateCommand(
|
|
|
255
293
|
|
|
256
294
|
if (result.error) {
|
|
257
295
|
errors++;
|
|
258
|
-
console.log(chalk.yellow('⚠ ERROR'));
|
|
296
|
+
if (!options.json) console.log(chalk.yellow('⚠ ERROR'));
|
|
259
297
|
} else if (result.passed) {
|
|
260
298
|
passed++;
|
|
261
|
-
console.log(chalk.green('✓ PASS'));
|
|
299
|
+
if (!options.json) console.log(chalk.green('✓ PASS'));
|
|
262
300
|
} else {
|
|
263
301
|
failed++;
|
|
264
|
-
console.log(chalk.red('✗ FAIL'));
|
|
302
|
+
if (!options.json) console.log(chalk.red('✗ FAIL'));
|
|
265
303
|
}
|
|
266
304
|
}
|
|
267
305
|
|
|
306
|
+
let htmlPath: string | undefined;
|
|
307
|
+
if (options.html) {
|
|
308
|
+
const html = generateBatchReport(results, 'JLC-CLI Validation Report');
|
|
309
|
+
writeFileSync(options.html, html);
|
|
310
|
+
htmlPath = options.html;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (options.json) {
|
|
314
|
+
printJson({
|
|
315
|
+
success: failed === 0 && errors === 0,
|
|
316
|
+
summary: {
|
|
317
|
+
total: results.length,
|
|
318
|
+
passed,
|
|
319
|
+
failed,
|
|
320
|
+
errors,
|
|
321
|
+
},
|
|
322
|
+
results,
|
|
323
|
+
html: htmlPath,
|
|
324
|
+
});
|
|
325
|
+
process.exit(failed > 0 || errors > 0 ? 1 : 0);
|
|
326
|
+
}
|
|
327
|
+
|
|
268
328
|
// Summary
|
|
269
329
|
console.log();
|
|
270
330
|
console.log(chalk.bold('Summary:'));
|
|
271
331
|
console.log(` ${chalk.green(`${passed} passed`)} ${chalk.red(`${failed} failed`)} ${chalk.yellow(`${errors} errors`)}`);
|
|
272
332
|
console.log();
|
|
273
333
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
console.log(JSON.stringify(results, null, 2));
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// HTML output
|
|
280
|
-
if (options.html) {
|
|
281
|
-
const html = generateBatchReport(results, 'JLC-CLI Validation Report');
|
|
282
|
-
writeFileSync(options.html, html);
|
|
283
|
-
console.log(chalk.dim(`HTML report: ${options.html}`));
|
|
334
|
+
if (htmlPath) {
|
|
335
|
+
console.log(chalk.dim(`HTML report: ${htmlPath}`));
|
|
284
336
|
}
|
|
285
337
|
|
|
286
338
|
process.exit(failed > 0 || errors > 0 ? 1 : 0);
|
package/src/index.ts
CHANGED
|
@@ -6,34 +6,49 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Command } from 'commander';
|
|
9
|
+
import { createRequire } from 'module';
|
|
9
10
|
import { searchCommand } from './commands/search.js';
|
|
10
11
|
import { infoCommand } from './commands/info.js';
|
|
11
12
|
import { installCommand } from './commands/install.js';
|
|
12
13
|
import { libraryCommand, regenerateCommand } from './commands/library.js';
|
|
13
14
|
import { easyedaSearchCommand, easyedaInstallCommand } from './commands/easyeda.js';
|
|
14
15
|
import { validateCommand } from './commands/validate.js';
|
|
16
|
+
import { getErrorMessage } from './utils/agent-output.js';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const { version } = require('../package.json');
|
|
15
20
|
|
|
16
21
|
const program = new Command();
|
|
17
22
|
|
|
23
|
+
function parsePositiveInt(value: string): number {
|
|
24
|
+
const parsed = Number.parseInt(value, 10);
|
|
25
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
26
|
+
throw new Error(`Expected a positive integer, got "${value}"`);
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
program
|
|
19
32
|
.name('jlc')
|
|
20
33
|
.description('JLC/EasyEDA component sourcing and KiCad library management')
|
|
21
|
-
.version(
|
|
34
|
+
.version(version);
|
|
22
35
|
|
|
23
36
|
program
|
|
24
37
|
.command('search <query...>')
|
|
25
38
|
.description('Search for components (basic parts sorted first)')
|
|
26
|
-
.option('-l, --limit <number>', 'Maximum results',
|
|
39
|
+
.option('-l, --limit <number>', 'Maximum results', parsePositiveInt, 20)
|
|
27
40
|
.option('--in-stock', 'Only show in-stock components')
|
|
28
41
|
.option('--basic-only', 'Only show basic parts (no extended)')
|
|
29
42
|
.option('--community', 'Search EasyEDA community library')
|
|
43
|
+
.option('--json', 'Output clean JSON and do not launch the TUI')
|
|
30
44
|
.action(async (queryParts: string[], options) => {
|
|
31
45
|
const query = queryParts.join(' ');
|
|
32
46
|
await searchCommand(query, {
|
|
33
|
-
limit:
|
|
47
|
+
limit: options.limit,
|
|
34
48
|
inStock: options.inStock,
|
|
35
49
|
basicOnly: options.basicOnly || false,
|
|
36
50
|
source: options.community ? 'easyeda-community' : 'lcsc',
|
|
51
|
+
json: options.json,
|
|
37
52
|
});
|
|
38
53
|
});
|
|
39
54
|
|
|
@@ -50,11 +65,15 @@ program
|
|
|
50
65
|
.description('Install component to KiCad libraries')
|
|
51
66
|
.option('-p, --project <path>', 'Install to project-local library')
|
|
52
67
|
.option('--with-3d', 'Include 3D model')
|
|
68
|
+
.option('-y, --yes', 'Install directly without launching the TUI')
|
|
69
|
+
.option('--json', 'Output clean JSON and install directly')
|
|
53
70
|
.option('-f, --force', 'Force reinstall (regenerate symbol and footprint)')
|
|
54
71
|
.action(async (id, options) => {
|
|
55
72
|
await installCommand(id, {
|
|
56
73
|
projectPath: options.project,
|
|
57
74
|
include3d: options.with3d,
|
|
75
|
+
yes: options.yes,
|
|
76
|
+
json: options.json,
|
|
58
77
|
force: options.force,
|
|
59
78
|
});
|
|
60
79
|
});
|
|
@@ -114,11 +133,17 @@ const easyeda = program
|
|
|
114
133
|
easyeda
|
|
115
134
|
.command('search <query...>')
|
|
116
135
|
.description('Open browser-based component search')
|
|
117
|
-
.option('-p, --port <number>', 'HTTP server port',
|
|
136
|
+
.option('-p, --port <number>', 'HTTP server port', parsePositiveInt, 3847)
|
|
137
|
+
.option('--json', 'Output clean JSON search results and do not open a browser')
|
|
138
|
+
.option('--no-open', 'Start the browser server without opening a browser')
|
|
139
|
+
.option('--once', 'Print the browser URL and exit after the server is ready')
|
|
118
140
|
.action(async (queryParts: string[], options) => {
|
|
119
141
|
const query = queryParts.join(' ');
|
|
120
142
|
await easyedaSearchCommand(query, {
|
|
121
|
-
port: options.port
|
|
143
|
+
port: options.port,
|
|
144
|
+
json: options.json,
|
|
145
|
+
open: options.open,
|
|
146
|
+
once: options.once,
|
|
122
147
|
});
|
|
123
148
|
});
|
|
124
149
|
|
|
@@ -127,13 +152,20 @@ easyeda
|
|
|
127
152
|
.description('Install EasyEDA community component to KiCad libraries')
|
|
128
153
|
.option('-p, --project <path>', 'Install to project-local library')
|
|
129
154
|
.option('--with-3d', 'Include 3D model')
|
|
155
|
+
.option('-y, --yes', 'Install directly without launching the TUI')
|
|
156
|
+
.option('--json', 'Output clean JSON and install directly')
|
|
130
157
|
.option('-f, --force', 'Force reinstall (regenerate symbol and footprint)')
|
|
131
158
|
.action(async (uuid, options) => {
|
|
132
159
|
await easyedaInstallCommand(uuid, {
|
|
133
160
|
projectPath: options.project,
|
|
134
161
|
include3d: options.with3d,
|
|
162
|
+
yes: options.yes,
|
|
163
|
+
json: options.json,
|
|
135
164
|
force: options.force,
|
|
136
165
|
});
|
|
137
166
|
});
|
|
138
167
|
|
|
139
|
-
program.
|
|
168
|
+
program.parseAsync().catch((error) => {
|
|
169
|
+
console.error(`Error: ${getErrorMessage(error)}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
|
|
5
|
+
type PackageJson = {
|
|
6
|
+
dependencies?: Record<string, string>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
async function readPackageJson(): Promise<PackageJson> {
|
|
10
|
+
const packageJsonPath = new URL("../package.json", import.meta.url);
|
|
11
|
+
const packageJson = await readFile(packageJsonPath, "utf8");
|
|
12
|
+
return JSON.parse(packageJson) as PackageJson;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("package metadata", () => {
|
|
16
|
+
it("does not publish workspace protocol runtime dependencies", async () => {
|
|
17
|
+
const packageJson = await readPackageJson();
|
|
18
|
+
const workspaceDependencies = Object.entries(packageJson.dependencies ?? {})
|
|
19
|
+
.filter(([, version]) => version.startsWith("workspace:"))
|
|
20
|
+
.map(([name]) => name);
|
|
21
|
+
|
|
22
|
+
expect(workspaceDependencies).toEqual([]);
|
|
23
|
+
expect(packageJson.dependencies?.["@jlcpcb/core"]).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface JsonError {
|
|
2
|
+
success: false;
|
|
3
|
+
error: {
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
retryable: boolean;
|
|
7
|
+
details?: unknown;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function printJson(value: unknown): void {
|
|
12
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function printJsonError(
|
|
16
|
+
code: string,
|
|
17
|
+
message: string,
|
|
18
|
+
options: { retryable?: boolean; details?: unknown } = {}
|
|
19
|
+
): void {
|
|
20
|
+
const payload: JsonError = {
|
|
21
|
+
success: false,
|
|
22
|
+
error: {
|
|
23
|
+
code,
|
|
24
|
+
message,
|
|
25
|
+
retryable: options.retryable ?? false,
|
|
26
|
+
details: options.details,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
printJson(payload);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getErrorMessage(error: unknown): string {
|
|
34
|
+
return error instanceof Error ? error.message : String(error);
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ComponentSearchResult } from '@jlcpcb/core';
|
|
2
|
+
|
|
3
|
+
export function formatSearchResultForJson(result: ComponentSearchResult) {
|
|
4
|
+
return {
|
|
5
|
+
id: result.id,
|
|
6
|
+
id_type: result.idType,
|
|
7
|
+
lcsc_id: result.idType === 'lcsc' ? result.lcscId : undefined,
|
|
8
|
+
easyeda_uuid: result.idType === 'easyeda_uuid' ? result.easyedaUuid ?? result.id : undefined,
|
|
9
|
+
name: result.name,
|
|
10
|
+
manufacturer: result.manufacturer,
|
|
11
|
+
description: result.description,
|
|
12
|
+
package: result.package,
|
|
13
|
+
datasheet: result.datasheetPdf,
|
|
14
|
+
stock: result.stock,
|
|
15
|
+
price: result.price,
|
|
16
|
+
library_type: result.libraryType,
|
|
17
|
+
category: result.category,
|
|
18
|
+
attributes: result.attributes,
|
|
19
|
+
};
|
|
20
|
+
}
|