@invisra/printspec 0.1.0 → 0.1.2

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  TypeScript package for printspec JSON Schemas, offline validation, BOM helpers, browser form metadata helpers, CLI commands, deterministic bundle export, and starter OpenSCAD/CadQuery source generation.
4
4
 
5
- **Status: v0.1.0 experimental.** The package is prepared for npm publication. If it is not yet published, install from the repository workspace instead of assuming registry availability.
5
+ **Status: npm package v0.1.1 / schema v0.1.0 experimental.** Version 0.1.1 is a packaging-only patch that includes built `dist/` artifacts; schemas and `printspecVersion` remain 0.1.0.
6
6
 
7
7
  ## Installation
8
8
 
@@ -63,7 +63,7 @@ writeBundleToDirectory(bundle, "bundle", { overwrite: true });
63
63
 
64
64
  ## Package exports and bundled schemas
65
65
 
66
- The package exports the main ESM API from `@invisra/printspec` and package schema files through `@invisra/printspec/schemas/*`. Schema files are bundled in the npm package under `schemas/`.
66
+ The package exports the main ESM API from `@invisra/printspec` and package schema files through `@invisra/printspec/schemas/*`. Published npm packages must include built TypeScript artifacts under `dist/` (`dist/index.js`, `dist/index.d.ts`, and `dist/cli.js`) and schema files under `schemas/`.
67
67
 
68
68
  Validation resolves bundled schemas offline. It does not fetch hosted schema URLs during normal validation.
69
69
 
package/dist/bom.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { BomItem, PrintSpec } from './types.js';
2
+ export declare function extractBom(spec: PrintSpec): BomItem[];
3
+ export declare function bomToMarkdown(bom: BomItem[]): string;
4
+ export declare function bomToCsv(bom: BomItem[]): string;
5
+ export declare function bomToSupplierOrderList(bom: BomItem[]): string;
package/dist/bom.js ADDED
@@ -0,0 +1,30 @@
1
+ function supplier(s) { const x = (s ?? '').toLowerCase().replace(/\s+/g, ''); return ['mcmaster', 'mcmaster-carr'].includes(x) ? 'mcmaster' : (s ?? '').toLowerCase(); }
2
+ function norm(item, mult = 1) { return { ...item, quantity: Math.max(1, Math.trunc((item.quantity || 1) * mult)), supplierReferences: (item.supplierReferences ?? []).map(r => ({ ...r, supplier: supplier(r.supplier) })) }; }
3
+ function key(i) { const r = i.supplierReferences?.[0]; return [i.kind, i.standard ?? '', i.size ?? '', r?.supplier ?? '', r?.partNumber ?? '', i.role ?? ''].join('|'); }
4
+ function collect(spec, mult = 1, out = []) { for (const h of spec.hardware ?? [])
5
+ out.push(norm(h, mult)); if (spec.part)
6
+ for (const h of spec.part.hardware ?? [])
7
+ out.push(norm(h, mult)); if (spec.project) {
8
+ for (const h of spec.project.hardware ?? [])
9
+ out.push(norm(h, mult));
10
+ for (const p of spec.project.parts ?? [])
11
+ if (p.spec)
12
+ collect(p.spec, mult * (p.quantity ?? 1), out);
13
+ } return out; }
14
+ export function extractBom(spec) { const map = new Map(); for (const i of collect(spec)) {
15
+ const k = key(i);
16
+ const cur = map.get(k);
17
+ if (cur)
18
+ cur.quantity += i.quantity;
19
+ else
20
+ map.set(k, { ...i });
21
+ } return [...map.values()]; }
22
+ export function bomToMarkdown(bom) { return ['| ID | Kind | Size | Qty | Supplier | Part Number |', '| --- | --- | --- | ---: | --- | --- |', ...bom.map(i => `| ${i.id} | ${i.kind} | ${i.size ?? ''} | ${i.quantity} | ${i.supplierReferences?.[0]?.supplier ?? ''} | ${i.supplierReferences?.[0]?.partNumber ?? ''} |`)].join('\n'); }
23
+ function csv(v) { const s = String(v ?? ''); return /[",\n]/.test(s) ? `"${s.replaceAll('"', '""')}"` : s; }
24
+ export function bomToCsv(bom) { return ['id,kind,standard,size,quantity,role,supplier,partNumber,url,description', ...bom.map(i => { const r = i.supplierReferences?.[0] ?? {}; return [i.id, i.kind, i.standard ?? '', i.size ?? '', i.quantity, i.role ?? '', r.supplier ?? '', r.partNumber ?? '', r.url ?? '', r.description ?? ''].map(csv).join(','); })].join('\n'); }
25
+ export function bomToSupplierOrderList(bom) { const by = new Map(); for (const i of bom)
26
+ for (const r of i.supplierReferences ?? []) {
27
+ const s = supplier(r.supplier);
28
+ const line = `${r.partNumber} x ${i.quantity}${r.url ? ` (${r.url})` : ''}`;
29
+ by.set(s, [...(by.get(s) ?? []), line]);
30
+ } return [...by.entries()].map(([s, lines]) => `${s}\n${lines.map(l => `- ${l}`).join('\n')}`).join('\n\n'); }
@@ -0,0 +1,27 @@
1
+ export type BundleFile = {
2
+ path: string;
3
+ content: string;
4
+ mediaType: string;
5
+ };
6
+ export type BundleWarning = {
7
+ path?: string;
8
+ message: string;
9
+ };
10
+ export type BundleResult = {
11
+ supported: boolean;
12
+ files: BundleFile[];
13
+ warnings: BundleWarning[];
14
+ message?: string;
15
+ };
16
+ export type BundleOptions = {
17
+ includeOpenScad?: boolean;
18
+ includeCadQuery?: boolean;
19
+ includeBom?: boolean;
20
+ includePartCad?: boolean;
21
+ prettyJson?: boolean;
22
+ };
23
+ export type WriteBundleOptions = {
24
+ overwrite?: boolean;
25
+ };
26
+ export declare function createBundle(input: unknown, options?: BundleOptions): BundleResult;
27
+ export declare function writeBundleToDirectory(bundle: BundleResult, outputDir: string, options?: WriteBundleOptions): void;
package/dist/bundle.js ADDED
@@ -0,0 +1,99 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { validatePrintSpec } from './validate.js';
4
+ import { normalizePrintSpec } from './normalize.js';
5
+ import { generateOpenScad } from './generators/openscad.js';
6
+ import { generateCadQuery } from './generators/cadquery.js';
7
+ import { extractBom, bomToMarkdown, bomToCsv, bomToSupplierOrderList } from './bom.js';
8
+ const VERSION = '0.1.0';
9
+ const jsonMedia = 'application/json';
10
+ function j(spec, pretty) { return JSON.stringify(spec, null, pretty ? 2 : 0) + '\n'; }
11
+ function add(files, path, content, mediaType, role) { files.push({ path, content, mediaType, role }); }
12
+ function warn(warnings, message, path) { warnings.push(path ? { path, message } : { message }); }
13
+ function safePartId(id) { return id.replace(/[^A-Za-z0-9._-]/g, '-'); }
14
+ function bomFiles(files, base, bom) { if (!bom.length)
15
+ return; add(files, `${base}bom.md`, bomToMarkdown(bom) + '\n', 'text/markdown', 'bom-markdown'); add(files, `${base}bom.csv`, bomToCsv(bom) + '\n', 'text/csv', 'bom-csv'); add(files, `${base}supplier-order-list.txt`, bomToSupplierOrderList(bom) + '\n', 'text/plain', 'supplier-order-list'); }
16
+ function cadForPart(spec, base, files, warnings, opts) { if (opts.includeOpenScad) {
17
+ const g = generateOpenScad(spec);
18
+ if (g.supported) {
19
+ add(files, `${base}cad/model.scad`, g.code, 'text/plain', 'openscad-source');
20
+ for (const m of g.warnings ?? [])
21
+ warn(warnings, m, `${base}cad/model.scad`);
22
+ }
23
+ else
24
+ warn(warnings, g.message ?? 'OpenSCAD generator unsupported', `${base}cad/model.scad`);
25
+ } if (opts.includeCadQuery) {
26
+ const g = generateCadQuery(spec);
27
+ if (g.supported) {
28
+ add(files, `${base}cad/model.py`, g.code, 'text/x-python', 'cadquery-source');
29
+ for (const m of g.warnings ?? [])
30
+ warn(warnings, m, `${base}cad/model.py`);
31
+ }
32
+ else
33
+ warn(warnings, g.message ?? 'CadQuery generator unsupported', `${base}cad/model.py`);
34
+ } }
35
+ function readme(kind, spec, files, warnings, bomCount) { const lines = []; lines.push(`# ${kind === 'project' ? (spec.project?.label ?? 'printspec project') : (spec.part?.label ?? 'printspec part')}`, ''); if (kind === 'part')
36
+ lines.push(`Part type: ${spec.part?.type ?? 'unknown'}`);
37
+ else {
38
+ if (spec.project?.description)
39
+ lines.push(spec.project.description, '');
40
+ lines.push('## Parts', ...(spec.project?.parts ?? []).map((p) => `- ${p.id}: ${p.label ?? p.id} (quantity ${p.quantity ?? 1})`), '');
41
+ } lines.push(`printspec version: ${spec.printspecVersion ?? VERSION}`, '', '## Generated files', ...files.map(f => `- ${f.path} (${f.role})`), ''); lines.push('## BOM', bomCount ? `${bomCount} BOM item(s) included.` : 'No hardware/BOM items were found.', ''); lines.push('## Warnings', ...(warnings.length ? warnings.map(w => `- ${w.path ? `${w.path}: ` : ''}${w.message}`) : ['- None']), ''); lines.push('Generated CAD source should be reviewed before manufacturing.'); return lines.join('\n') + '\n'; }
42
+ function partcad(spec, files) { const cq = files.filter(f => f.role === 'cadquery-source').sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0); const name = spec.project?.partcad?.packageName ?? 'printspec-project'; const lines = ['package:', ` name: ${name}`, ` version: ${spec.project?.partcad?.packageVersion ?? VERSION}`, ' description: Experimental compatibility stub generated from printspec.', '', 'parts:']; for (const f of cq) {
43
+ const m = f.path.match(/^parts\/([^/]+)\//);
44
+ lines.push(` - name: ${m?.[1] ?? 'model'}`, ' type: cadquery', ` path: ${f.path}`);
45
+ } if (!cq.length)
46
+ lines.push(' []'); return lines.join('\n') + '\n'; }
47
+ function manifest(kind, files, warnings, spec) { const entries = files.map(f => ({ path: f.path, mediaType: f.mediaType, role: f.role })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0); entries.push({ path: 'bundle-manifest.json', mediaType: jsonMedia, role: 'bundle-manifest' }); entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0); return j({ bundleVersion: VERSION, createdBy: 'printspec', printspecVersion: spec.printspecVersion ?? VERSION, kind, files: entries, warnings }, true); }
48
+ export function createBundle(input, options = {}) {
49
+ const r = validatePrintSpec(input);
50
+ if (!r.valid)
51
+ return { supported: false, files: [], warnings: [], message: `Validation failed: ${r.errors.slice(0, 3).join('; ')}` };
52
+ const spec = normalizePrintSpec(input);
53
+ const opts = { includeOpenScad: options.includeOpenScad !== false, includeCadQuery: options.includeCadQuery !== false, includeBom: options.includeBom !== false, includePartCad: options.includePartCad === true, prettyJson: options.prettyJson !== false };
54
+ const files = [];
55
+ const warnings = [];
56
+ const kind = spec.project ? 'project' : 'part';
57
+ add(files, 'printspec.json', j(spec, opts.prettyJson), jsonMedia, 'source-spec');
58
+ if (kind === 'part') {
59
+ cadForPart(spec, '', files, warnings, opts);
60
+ if (opts.includeBom)
61
+ bomFiles(files, 'bom/', extractBom(spec));
62
+ }
63
+ else {
64
+ for (const p of spec.project.parts ?? []) {
65
+ const id = safePartId(p.id);
66
+ if (p.spec) {
67
+ const ps = normalizePrintSpec(p.spec);
68
+ add(files, `parts/${id}/printspec.json`, j(ps, opts.prettyJson), jsonMedia, 'part-source-spec');
69
+ cadForPart(ps, `parts/${id}/`, files, warnings, opts);
70
+ }
71
+ else if (p.specPath)
72
+ warn(warnings, `External specPath references are not bundled yet: ${p.specPath}`, `parts/${id}/printspec.json`);
73
+ }
74
+ if (opts.includeBom)
75
+ bomFiles(files, 'bom/', extractBom(spec));
76
+ if (opts.includePartCad)
77
+ add(files, 'partcad.yaml', partcad(spec, files), 'text/yaml', 'partcad-stub');
78
+ }
79
+ const bomCount = extractBom(spec).length;
80
+ add(files, 'README.md', readme(kind, spec, files, warnings, bomCount), 'text/markdown', 'readme');
81
+ add(files, 'bundle-manifest.json', manifest(kind, files, warnings, spec), jsonMedia, 'bundle-manifest');
82
+ files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
83
+ return { supported: true, files: files.map(({ role, ...f }) => f), warnings };
84
+ }
85
+ function assertSafe(p) { if (!p || path.isAbsolute(p) || p.split(/[\\/]+/).includes('..'))
86
+ throw new Error(`Unsafe bundle path: ${p}`); }
87
+ export function writeBundleToDirectory(bundle, outputDir, options = {}) { if (!bundle.supported)
88
+ throw new Error(bundle.message ?? 'Unsupported bundle'); if (fs.existsSync(outputDir) && !options.overwrite)
89
+ throw new Error(`Output directory already exists: ${outputDir}`); fs.mkdirSync(outputDir, { recursive: true }); for (const f of bundle.files) {
90
+ assertSafe(f.path);
91
+ const dest = path.resolve(outputDir, f.path);
92
+ const root = path.resolve(outputDir);
93
+ if (dest !== root && !dest.startsWith(root + path.sep))
94
+ throw new Error(`Unsafe bundle path: ${f.path}`);
95
+ if (fs.existsSync(dest) && !options.overwrite)
96
+ throw new Error(`Output file already exists: ${dest}`);
97
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
98
+ fs.writeFileSync(dest, f.content, 'utf8');
99
+ } }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { validatePrintSpec, generateOpenScad, generateCadQuery, extractBom, bomToMarkdown, bomToCsv, bomToSupplierOrderList, getPartFamilyFormMetadata, listPartFamilies, createBundle, writeBundleToDirectory } from './index.js';
6
+ function fail(msg) { console.error(msg); return 1; }
7
+ function packageVersion() {
8
+ const here = path.dirname(fileURLToPath(import.meta.url));
9
+ for (const candidate of [path.join(here, '..', 'package.json'), path.join(here, '..', '..', 'package.json'), path.join(here, '..', '..', '..', 'package.json')]) {
10
+ if (fs.existsSync(candidate))
11
+ return JSON.parse(fs.readFileSync(candidate, 'utf8')).version;
12
+ }
13
+ return '0.1.0';
14
+ }
15
+ function usage() { return 'usage: printspec <validate|to-openscad|to-cadquery|bom|bundle|form-metadata|list-part-families|version> [args] [--output file]\ncommands: validate, to-openscad, to-cadquery, bom, bundle, form-metadata, list-part-families, version'; }
16
+ function load(file) {
17
+ try {
18
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
19
+ }
20
+ catch (e) {
21
+ if (e?.code && e.code !== 'SyntaxError')
22
+ throw new Error(`${file}: read error: ${e.message}`);
23
+ if (e instanceof SyntaxError)
24
+ throw new Error(`${file}: parse error: ${e.message}`);
25
+ throw new Error(`${file}: parse error: ${e.message}`);
26
+ }
27
+ }
28
+ function opt(args, name) { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; }
29
+ function write(text, out) { if (out)
30
+ fs.writeFileSync(out, text);
31
+ else
32
+ console.log(text); }
33
+ function main(argv = process.argv.slice(2)) {
34
+ const [cmd, file, ...rest] = argv;
35
+ if (cmd === '--version' || cmd === 'version') {
36
+ console.log(`printspec ${packageVersion()}`);
37
+ return 0;
38
+ }
39
+ if (cmd === '--help' || cmd === '-h') {
40
+ console.log(usage());
41
+ return 0;
42
+ }
43
+ if (!cmd)
44
+ return fail(usage());
45
+ if (cmd === 'list-part-families') {
46
+ write(JSON.stringify(listPartFamilies(), null, rest.includes('--pretty') ? 2 : 0));
47
+ return 0;
48
+ }
49
+ if (cmd === 'form-metadata') {
50
+ if (!file)
51
+ return fail(usage());
52
+ try {
53
+ write(JSON.stringify(getPartFamilyFormMetadata(file), null, rest.includes('--pretty') ? 2 : 0));
54
+ return 0;
55
+ }
56
+ catch (e) {
57
+ return fail(`error: ${e.message}`);
58
+ }
59
+ }
60
+ if (!['validate', 'to-openscad', 'to-cadquery', 'bom', 'bundle'].includes(cmd))
61
+ return fail(`error: unknown command ${cmd}`);
62
+ if (!file)
63
+ return fail(usage());
64
+ let spec;
65
+ try {
66
+ spec = load(file);
67
+ }
68
+ catch (e) {
69
+ return fail(`error: unable to read JSON: ${e.message}`);
70
+ }
71
+ if (cmd === 'validate') {
72
+ const r = validatePrintSpec(spec);
73
+ if (r.valid) {
74
+ console.log('valid');
75
+ return 0;
76
+ }
77
+ console.error('invalid');
78
+ for (const e of r.errors)
79
+ console.error(`- ${e}`);
80
+ return 1;
81
+ }
82
+ if (cmd === 'to-openscad' || cmd === 'to-cadquery') {
83
+ const r = validatePrintSpec(spec);
84
+ if (!r.valid)
85
+ return fail(`error: ${r.errors.slice(0, 3).join('; ')}`);
86
+ const g = cmd === 'to-openscad' ? generateOpenScad(spec) : generateCadQuery(spec);
87
+ if (!g.supported)
88
+ return fail(`error: ${g.message ?? 'unsupported'}`);
89
+ for (const w of g.warnings ?? [])
90
+ console.error(`warning: ${w}`);
91
+ write(g.code, opt(rest, '--output'));
92
+ return 0;
93
+ }
94
+ if (cmd === 'bom') {
95
+ const fmt = opt(rest, '--format') ?? 'markdown';
96
+ const bom = extractBom(spec);
97
+ const fn = { markdown: bomToMarkdown, csv: bomToCsv, 'supplier-list': bomToSupplierOrderList }[fmt];
98
+ if (!fn)
99
+ return fail('error: unsupported BOM format');
100
+ write(fn(bom), opt(rest, '--output'));
101
+ return 0;
102
+ }
103
+ if (cmd === 'bundle') {
104
+ const out = opt(rest, '--output');
105
+ if (!out)
106
+ return fail('error: bundle requires --output <directory>');
107
+ const b = createBundle(spec, { includeOpenScad: !rest.includes('--no-openscad'), includeCadQuery: !rest.includes('--no-cadquery'), includeBom: !rest.includes('--no-bom'), includePartCad: rest.includes('--partcad') });
108
+ if (!b.supported)
109
+ return fail(`error: ${b.message ?? 'unsupported bundle'}`);
110
+ try {
111
+ writeBundleToDirectory(b, out, { overwrite: rest.includes('--overwrite') });
112
+ }
113
+ catch (e) {
114
+ return fail(`error: ${e.message}`);
115
+ }
116
+ console.log(`wrote ${b.files.length} files to ${out}`);
117
+ console.log(`warnings: ${b.warnings.length}`);
118
+ return 0;
119
+ }
120
+ return fail(`error: unknown command ${cmd}`);
121
+ }
122
+ process.exitCode = main();
@@ -0,0 +1,37 @@
1
+ export type FormGroup = {
2
+ id: string;
3
+ title: string;
4
+ fields: string[];
5
+ };
6
+ export type FormField = {
7
+ name: string;
8
+ title: string;
9
+ description?: string;
10
+ required: boolean;
11
+ type: string;
12
+ control?: string;
13
+ unit?: string;
14
+ step?: number;
15
+ minimum?: number;
16
+ exclusiveMinimum?: number;
17
+ maximum?: number;
18
+ default?: unknown;
19
+ examples?: unknown[];
20
+ priority?: 'primary' | 'advanced';
21
+ };
22
+ export type FormMetadata = {
23
+ partType: string;
24
+ title: string;
25
+ description?: string;
26
+ fields: FormField[];
27
+ groups: FormGroup[];
28
+ };
29
+ export type PartFamilySummary = {
30
+ type: string;
31
+ title: string;
32
+ description?: string;
33
+ schemaFilename: string;
34
+ generatorSupported?: boolean;
35
+ };
36
+ export declare function listPartFamilies(): PartFamilySummary[];
37
+ export declare function getPartFamilyFormMetadata(partType: string): FormMetadata;
package/dist/forms.js ADDED
@@ -0,0 +1,68 @@
1
+ import { schemas } from './schemas.js';
2
+ const generatorSupported = new Set(['rounded_rectangular_plate', 'spacer_block', 'round_spacer', 'electronics_standoff']);
3
+ function partFamilyEntries() {
4
+ return Object.entries(schemas)
5
+ .map(([filename, schema]) => ({ filename, schema, type: schema?.properties?.type?.const }))
6
+ .filter((entry) => typeof entry.type === 'string' && !!entry.schema?.properties?.parameters)
7
+ .sort((a, b) => a.type.localeCompare(b.type));
8
+ }
9
+ function schemaType(field) {
10
+ if (typeof field?.type === 'string')
11
+ return field.type;
12
+ if (field?.$ref)
13
+ return 'object';
14
+ if (Array.isArray(field?.type))
15
+ return field.type.join('|');
16
+ return 'unknown';
17
+ }
18
+ export function listPartFamilies() {
19
+ return partFamilyEntries().map(({ type, filename, schema }) => ({
20
+ type,
21
+ title: schema.title ?? type,
22
+ description: schema.description,
23
+ schemaFilename: filename,
24
+ generatorSupported: generatorSupported.has(type),
25
+ }));
26
+ }
27
+ export function getPartFamilyFormMetadata(partType) {
28
+ const entry = partFamilyEntries().find((candidate) => candidate.type === partType);
29
+ if (!entry)
30
+ throw new Error(`Unsupported printspec part family: ${partType}`);
31
+ const parameters = entry.schema?.properties?.parameters;
32
+ const properties = parameters?.properties ?? {};
33
+ const required = new Set(parameters?.required ?? []);
34
+ const metadata = parameters?.['x-printspec-ui'] ?? {};
35
+ const known = new Set(Object.keys(properties));
36
+ const ordered = Array.isArray(metadata.order) ? metadata.order.filter((name) => known.has(name)) : Object.keys(properties).sort();
37
+ for (const name of Object.keys(properties).sort())
38
+ if (!ordered.includes(name))
39
+ ordered.push(name);
40
+ const groups = Array.isArray(metadata.groups) && metadata.groups.length
41
+ ? metadata.groups.map((group) => ({ id: String(group.id), title: String(group.title ?? group.id), fields: (group.fields ?? []).filter((name) => known.has(name)) }))
42
+ : [{ id: 'parameters', title: parameters?.title ?? 'Parameters', fields: ordered }];
43
+ return {
44
+ partType,
45
+ title: entry.schema.title ?? partType,
46
+ description: entry.schema.description,
47
+ fields: ordered.map((name) => {
48
+ const field = properties[name] ?? {};
49
+ return {
50
+ name,
51
+ title: field.title ?? name,
52
+ description: field.description,
53
+ required: required.has(name),
54
+ type: schemaType(field),
55
+ control: field['x-printspec-control'],
56
+ unit: field['x-printspec-unit'],
57
+ step: field['x-printspec-step'],
58
+ minimum: field.minimum,
59
+ exclusiveMinimum: field.exclusiveMinimum,
60
+ maximum: field.maximum,
61
+ default: field.default,
62
+ examples: field.examples,
63
+ priority: field['x-printspec-priority'],
64
+ };
65
+ }),
66
+ groups,
67
+ };
68
+ }
@@ -0,0 +1,2 @@
1
+ import type { GeneratorResult, PrintSpec } from '../types.js';
2
+ export declare function generateCadQuery(spec: PrintSpec): GeneratorResult;
@@ -0,0 +1,29 @@
1
+ import { validatePrintSpec } from '../validate.js';
2
+ function invalid(spec) { const r = validatePrintSpec(spec); return r.valid ? null : { supported: false, code: '', message: `Validation failed: ${r.errors.slice(0, 3).join('; ')}`, warnings: [] }; }
3
+ const header = '# Generated by printspec 0.1.0\n# Review generated CAD before manufacturing.\nimport cadquery as cq\n\n';
4
+ function warnings(a) { const w = []; if (a.chamfer != null)
5
+ w.push('chamfer requested but not implemented'); if (a.fillet != null)
6
+ w.push('fillet requested but not implemented'); return w; }
7
+ function holeLines(hs = []) { return hs.map(h => `part = part.faces(">Z").workplane().pushPoints([(${h.x}, ${h.y})]).hole(${h.diameter})`).join('\n'); }
8
+ export function generateCadQuery(spec) {
9
+ const bad = invalid(spec);
10
+ if (bad)
11
+ return bad;
12
+ const p = spec.part;
13
+ if (!p)
14
+ return { supported: false, code: '', message: 'Only part specs are supported.', warnings: [] };
15
+ const a = p.parameters;
16
+ if (p.type === 'rounded_rectangular_plate')
17
+ return { supported: true, warnings: [], code: `${header}length = ${a.length}\nwidth = ${a.width}\nheight = ${a.thickness}\ncorner_radius = ${a.cornerRadius}\n\npart = cq.Workplane("XY").rounded_rect(length, width, corner_radius).extrude(height)\n${holeLines(a.holes)}\n` };
18
+ if (p.type === 'spacer_block')
19
+ return { supported: true, warnings: [], code: `${header}length = ${a.length}\nwidth = ${a.width}\nheight = ${a.height}\n\npart = cq.Workplane("XY").box(length, width, height, centered=(True, True, False))\n${holeLines(a.holes)}\n` };
20
+ if (p.type === 'round_spacer') {
21
+ const w = warnings(a);
22
+ return { supported: true, warnings: w, code: `${header}outer_diameter = ${a.outerDiameter}\nheight = ${a.height}${a.innerDiameter != null ? `\ninner_diameter = ${a.innerDiameter}` : ''}\n\npart = cq.Workplane("XY").circle(outer_diameter / 2).extrude(height)${a.innerDiameter != null ? '\npart = part.faces(">Z").workplane().hole(inner_diameter)' : ''}\n` };
23
+ }
24
+ if (p.type === 'electronics_standoff') {
25
+ const hasBase = a.baseDiameter != null && a.baseHeight != null;
26
+ return { supported: true, warnings: [], code: `${header}outer_diameter = ${a.outerDiameter}\nheight = ${a.height}\nhole_diameter = ${a.holeDiameter}${hasBase ? `\nbase_diameter = ${a.baseDiameter}\nbase_height = ${a.baseHeight}` : ''}\n\n${hasBase ? `base = cq.Workplane("XY").circle(base_diameter / 2).extrude(base_height)\nstandoff = cq.Workplane("XY").workplane(offset=base_height).circle(outer_diameter / 2).extrude(height)\npart = base.union(standoff)` : `part = cq.Workplane("XY").circle(outer_diameter / 2).extrude(height)`}\npart = part.faces(">Z").workplane().hole(hole_diameter)\n` };
27
+ }
28
+ return { supported: false, code: '', message: `Unsupported part family: ${p.type}`, warnings: [] };
29
+ }
@@ -0,0 +1,2 @@
1
+ import type { GeneratorResult, PrintSpec } from '../types.js';
2
+ export declare function generateOpenScad(spec: PrintSpec): GeneratorResult;
@@ -0,0 +1,30 @@
1
+ import { validatePrintSpec } from '../validate.js';
2
+ function invalid(spec) { const r = validatePrintSpec(spec); return r.valid ? null : { supported: false, code: '', message: `Validation failed: ${r.errors.slice(0, 3).join('; ')}`, warnings: [] }; }
3
+ const header = '// Generated by printspec 0.1.0\n// Review generated CAD before manufacturing.\n';
4
+ function warnings(a) { const w = []; if (a.chamfer != null)
5
+ w.push('chamfer requested but not implemented'); if (a.fillet != null)
6
+ w.push('fillet requested but not implemented'); return w; }
7
+ function holes(hs = []) { return hs.map(h => ` translate([${h.x}, ${h.y}, -0.1]) cylinder(h = height + 0.2, d = ${h.diameter}, $fn = 48);`).join('\n'); }
8
+ export function generateOpenScad(spec) {
9
+ const bad = invalid(spec);
10
+ if (bad)
11
+ return bad;
12
+ const p = spec.part;
13
+ if (!p)
14
+ return { supported: false, code: '', message: 'Only part specs are supported.', warnings: [] };
15
+ const a = p.parameters;
16
+ if (p.type === 'rounded_rectangular_plate')
17
+ return { supported: true, warnings: [], code: `${header}length = ${a.length};\nwidth = ${a.width};\nheight = ${a.thickness};\ncorner_radius = ${a.cornerRadius};\n\nmodule rounded_plate() {\n hull() {\n for (x = [-length/2 + corner_radius, length/2 - corner_radius])\n for (y = [-width/2 + corner_radius, width/2 - corner_radius])\n translate([x, y, 0]) cylinder(h = height, r = corner_radius, $fn = 32);\n }\n}\n\ndifference() {\n rounded_plate();\n${holes(a.holes)}\n}\n` };
18
+ if (p.type === 'spacer_block')
19
+ return { supported: true, warnings: [], code: `${header}length = ${a.length};\nwidth = ${a.width};\nheight = ${a.height};\n\ndifference() {\n translate([-length/2, -width/2, 0]) cube([length, width, height]);\n${holes(a.holes)}\n}\n` };
20
+ if (p.type === 'round_spacer') {
21
+ const w = warnings(a);
22
+ const inner = a.innerDiameter != null ? `\n translate([0, 0, -0.1]) cylinder(h = height + 0.2, d = inner_diameter, $fn = 64);` : '';
23
+ return { supported: true, warnings: w, code: `${header}outer_diameter = ${a.outerDiameter};\nheight = ${a.height};${a.innerDiameter != null ? `\ninner_diameter = ${a.innerDiameter};` : ''}\n\ndifference() {\n cylinder(h = height, d = outer_diameter, $fn = 64);${inner}\n}\n` };
24
+ }
25
+ if (p.type === 'electronics_standoff') {
26
+ const hasBase = a.baseDiameter != null && a.baseHeight != null;
27
+ return { supported: true, warnings: [], code: `${header}outer_diameter = ${a.outerDiameter};\nheight = ${a.height};\nhole_diameter = ${a.holeDiameter};${hasBase ? `\nbase_diameter = ${a.baseDiameter};\nbase_height = ${a.baseHeight};` : ''}\n\ndifference() {\n ${hasBase ? `union() {\n cylinder(h = base_height, d = base_diameter, $fn = 64);\n translate([0, 0, base_height]) cylinder(h = height, d = outer_diameter, $fn = 64);\n }` : `cylinder(h = height, d = outer_diameter, $fn = 64);`}\n translate([0, 0, -0.1]) cylinder(h = ${hasBase ? 'base_height + height' : 'height'} + 0.2, d = hole_diameter, $fn = 64);\n}\n` };
28
+ }
29
+ return { supported: false, code: '', message: `Unsupported part family: ${p.type}`, warnings: [] };
30
+ }
@@ -0,0 +1,9 @@
1
+ export * from './types.js';
2
+ export * from './validate.js';
3
+ export * from './normalize.js';
4
+ export * from './bom.js';
5
+ export * from './safety.js';
6
+ export * from './generators/openscad.js';
7
+ export * from './generators/cadquery.js';
8
+ export * from './forms.js';
9
+ export * from './bundle.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from './types.js';
2
+ export * from './validate.js';
3
+ export * from './normalize.js';
4
+ export * from './bom.js';
5
+ export * from './safety.js';
6
+ export * from './generators/openscad.js';
7
+ export * from './generators/cadquery.js';
8
+ export * from './forms.js';
9
+ export * from './bundle.js';
@@ -0,0 +1,2 @@
1
+ import type { PrintSpec } from './types.js';
2
+ export declare function normalizePrintSpec(spec: PrintSpec): PrintSpec;
@@ -0,0 +1,15 @@
1
+ function normSupplier(s) { const x = s.toLowerCase().replace(/\s+/g, ''); return ['mcmaster', 'mcmaster-carr'].includes(x) ? 'mcmaster' : s.toLowerCase(); }
2
+ function walk(v) { if (Array.isArray(v))
3
+ return v.map(walk); if (v && typeof v === 'object') {
4
+ const o = {};
5
+ for (const [k, val] of Object.entries(v))
6
+ o[k] = walk(val);
7
+ if ('diameter' in o && 'x' in o && 'y' in o) {
8
+ o.axis ??= 'z';
9
+ o.depth ??= 'through';
10
+ }
11
+ if (o.supplier)
12
+ o.supplier = normSupplier(o.supplier);
13
+ return o;
14
+ } return v; }
15
+ export function normalizePrintSpec(spec) { const out = walk(JSON.parse(JSON.stringify(spec))); out.units ??= 'mm'; return out; }
@@ -0,0 +1,6 @@
1
+ export declare const isPotentiallyUnsafeLabel: (text: string) => boolean;
2
+ export declare const hasDisallowedSupplierRole: (text: string) => boolean;
3
+ export declare function validateSafeMetadata(spec: any): {
4
+ valid: boolean;
5
+ errors: string[];
6
+ };
package/dist/safety.js ADDED
@@ -0,0 +1,4 @@
1
+ const bad = ['weapon', 'firearm', 'ammunition', 'explosive', 'suppressor', 'silencer', 'lockpick', 'bypass', 'implant', 'pressure vessel', 'flight-critical', 'high-voltage'];
2
+ export const isPotentiallyUnsafeLabel = (text) => bad.some(w => text.toLowerCase().includes(w));
3
+ export const hasDisallowedSupplierRole = isPotentiallyUnsafeLabel;
4
+ export function validateSafeMetadata(spec) { const text = JSON.stringify(spec.metadata ?? {}) + ' ' + (spec.part?.label ?? spec.project?.label ?? ''); return { valid: !isPotentiallyUnsafeLabel(text), errors: isPotentiallyUnsafeLabel(text) ? ['Metadata or label may describe a disallowed/safety-critical use.'] : [] }; }
@@ -0,0 +1,6 @@
1
+ import { Ajv2020 } from 'ajv/dist/2020.js';
2
+ export declare const schemaDir: string;
3
+ export declare const schemaFiles: any;
4
+ export declare function loadSchemas(): Record<string, any>;
5
+ export declare const schemas: Record<string, any>;
6
+ export declare function createAjv(): Ajv2020;
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { Ajv2020 } from 'ajv/dist/2020.js';
5
+ import addFormats from 'ajv-formats';
6
+ const schemaBaseUri = 'https://schemas.invisra.ai/printspec/0.1.0/';
7
+ function isDirectory(candidate) {
8
+ return fs.existsSync(candidate) && fs.statSync(candidate).isDirectory();
9
+ }
10
+ function findSchemaDir() {
11
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
12
+ const packageLocalCandidate = path.resolve(moduleDir, '..', 'schemas');
13
+ if (isDirectory(packageLocalCandidate))
14
+ return packageLocalCandidate;
15
+ // Source checkout fallback: walk upward to the repository-level schemas
16
+ // directory so development builds can still validate fully offline.
17
+ for (let dir = moduleDir;; dir = path.dirname(dir)) {
18
+ const candidate = path.join(dir, 'schemas');
19
+ if (isDirectory(candidate))
20
+ return candidate;
21
+ const parent = path.dirname(dir);
22
+ if (parent === dir)
23
+ break;
24
+ }
25
+ throw new Error('Unable to locate bundled printspec schemas. Run npm run sync:schemas before building, or reinstall the package.');
26
+ }
27
+ export const schemaDir = findSchemaDir();
28
+ export const schemaFiles = fs.readdirSync(schemaDir).filter((f) => f.endsWith('.schema.json')).sort();
29
+ export function loadSchemas() {
30
+ const files = fs.readdirSync(schemaDir).filter((f) => f.endsWith('.schema.json')).sort();
31
+ return Object.fromEntries(files.map((file) => [file, JSON.parse(fs.readFileSync(path.join(schemaDir, file), 'utf8'))]));
32
+ }
33
+ export const schemas = loadSchemas();
34
+ export function createAjv() {
35
+ const ajv = new Ajv2020({ allErrors: true, strict: false, loadSchema: undefined });
36
+ addFormats(ajv);
37
+ const registered = new Set();
38
+ for (const [filename, schema] of Object.entries(schemas)) {
39
+ const primaryId = schema.$id ?? `${schemaBaseUri}${filename}`;
40
+ ajv.addSchema(schema, primaryId);
41
+ registered.add(primaryId);
42
+ }
43
+ for (const [filename, schema] of Object.entries(schemas)) {
44
+ const aliasSchema = { ...schema };
45
+ delete aliasSchema.$id;
46
+ for (const alias of [filename, `${schemaBaseUri}${filename}`]) {
47
+ if (!registered.has(alias)) {
48
+ ajv.addSchema(aliasSchema, alias);
49
+ registered.add(alias);
50
+ }
51
+ }
52
+ }
53
+ return ajv;
54
+ }
@@ -0,0 +1 @@
1
+ export declare function validateSemantic(spec: any): string[];
@@ -0,0 +1,80 @@
1
+ function pathOf(p) { return p || '$'; }
2
+ function dup(values, label) { const seen = new Set(); const e = []; for (const v of values) {
3
+ if (seen.has(v))
4
+ e.push(`duplicate ${label} id: ${v}`);
5
+ seen.add(v);
6
+ } return e; }
7
+ function params(part) { return part?.parameters ?? {}; }
8
+ function checkPart(part, prefix = 'part') { const e = []; const p = params(part); if (part?.type === 'rounded_rectangular_plate' && p.cornerRadius > Math.min(p.length, p.width) / 2)
9
+ e.push(`${prefix}.parameters.cornerRadius exceeds half of min(length,width)`); if (part?.type === 'simple_box' && p.wallThickness >= Math.min(p.outerLength, p.outerWidth) / 2)
10
+ e.push(`${prefix}.parameters.wallThickness must be less than half of outer dimensions`); if (part?.type === 'round_spacer' && p.innerDiameter != null && p.innerDiameter >= p.outerDiameter)
11
+ e.push(`${prefix}.parameters.innerDiameter must be less than outerDiameter`); if (part?.type === 'electronics_standoff') {
12
+ if (p.holeDiameter >= p.outerDiameter)
13
+ e.push(`${prefix}.parameters.holeDiameter must be less than outerDiameter`);
14
+ if ((p.baseDiameter == null) !== (p.baseHeight == null))
15
+ e.push(`${prefix}.parameters.baseDiameter and baseHeight must be provided together`);
16
+ if (p.baseDiameter != null && p.baseDiameter < p.outerDiameter)
17
+ e.push(`${prefix}.parameters.baseDiameter must be greater than or equal to outerDiameter`);
18
+ } const maxW = p.width ?? p.outerWidth ?? p.outerDiameter; for (const h of p.holes ?? []) {
19
+ if (h.diameter > maxW)
20
+ e.push(`${prefix}.parameters.holes diameter exceeds target width`);
21
+ } return e; }
22
+ export function validateSemantic(spec) {
23
+ const e = [];
24
+ const checkHw = (items, label) => { e.push(...dup((items ?? []).map(h => h.id).filter(Boolean), `${label} hardware`)); for (const h of items ?? []) {
25
+ if (!Number.isInteger(h.quantity) || h.quantity < 1)
26
+ e.push(`${label}.hardware quantity must be integer >= 1`);
27
+ for (const r of h.supplierReferences ?? []) {
28
+ if (!r.partNumber)
29
+ e.push(`${label}.supplierReference partNumber is required`);
30
+ if (r.url) {
31
+ try {
32
+ new URL(r.url);
33
+ }
34
+ catch {
35
+ e.push(`${label}.supplierReference url is invalid`);
36
+ }
37
+ }
38
+ }
39
+ } };
40
+ checkHw(spec?.hardware, 'top-level');
41
+ if (spec?.part) {
42
+ if (spec.part.type === 'composable_part') {
43
+ const ids = (spec.part.components ?? []).map((c) => c.id);
44
+ e.push(...dup(ids, 'component'));
45
+ const set = new Set(ids);
46
+ e.push(...dup((spec.part.features ?? []).map((f) => f.id).filter(Boolean), 'feature'));
47
+ for (const f of spec.part.features ?? [])
48
+ if (f.target && !set.has(f.target))
49
+ e.push(`feature ${f.id ?? ''} target does not exist: ${f.target}`);
50
+ for (const c of spec.part.components ?? []) {
51
+ const t = c.relation?.target;
52
+ if (['mirrored_from', 'attached_to_face', 'on_top_of'].includes(c.relation?.type) && t && !set.has(t))
53
+ e.push(`component ${c.id} relation target does not exist: ${t}`);
54
+ }
55
+ }
56
+ else
57
+ e.push(...checkPart(spec.part));
58
+ checkHw(spec.part.hardware, 'part');
59
+ }
60
+ if (spec?.project) {
61
+ const partIds = (spec.project.parts ?? []).map((p) => p.id);
62
+ const partSet = new Set(partIds);
63
+ const hwIds = (spec.project.hardware ?? []).map((h) => h.id);
64
+ const hwSet = new Set(hwIds);
65
+ e.push(...dup(partIds, 'project part'), ...dup(hwIds, 'project hardware'));
66
+ for (const r of spec.project.relationships ?? []) {
67
+ if (r.partA && !partSet.has(r.partA))
68
+ e.push(`relationship partA missing: ${r.partA}`);
69
+ if (r.partB && !partSet.has(r.partB))
70
+ e.push(`relationship partB missing: ${r.partB}`);
71
+ if (r.hardware && !hwSet.has(r.hardware))
72
+ e.push(`relationship hardware missing: ${r.hardware}`);
73
+ }
74
+ for (const p of spec.project.parts ?? [])
75
+ if (p.spec)
76
+ e.push(...validateSemantic(p.spec).map(x => `project.parts.${p.id}: ${x}`));
77
+ checkHw(spec.project.hardware, 'project');
78
+ }
79
+ return e;
80
+ }
@@ -0,0 +1,71 @@
1
+ export type SupplierReference = {
2
+ supplier: string;
3
+ partNumber: string;
4
+ url?: string;
5
+ description?: string;
6
+ };
7
+ export type HardwareItem = {
8
+ id: string;
9
+ kind: string;
10
+ standard?: string;
11
+ size?: string;
12
+ quantity: number;
13
+ role?: string;
14
+ supplierReferences?: SupplierReference[];
15
+ };
16
+ export type BomItem = HardwareItem;
17
+ export type ValidationResult = {
18
+ valid: boolean;
19
+ errors: string[];
20
+ };
21
+ export type PartFamilySpec = {
22
+ type: string;
23
+ label: string;
24
+ parameters: Record<string, unknown>;
25
+ hardware?: HardwareItem[];
26
+ };
27
+ export type Component = {
28
+ id: string;
29
+ kind: string;
30
+ operation: 'add' | 'subtract';
31
+ dimensions: Record<string, number>;
32
+ };
33
+ export type Feature = {
34
+ id: string;
35
+ kind: string;
36
+ target: string;
37
+ parameters?: Record<string, unknown>;
38
+ };
39
+ export type ComposablePartSpec = {
40
+ type: 'composable_part';
41
+ label: string;
42
+ components: Component[];
43
+ features?: Feature[];
44
+ hardware?: HardwareItem[];
45
+ };
46
+ export type ProjectSpec = {
47
+ type: 'project';
48
+ label: string;
49
+ parts: {
50
+ id: string;
51
+ label: string;
52
+ spec?: PrintSpec;
53
+ specPath?: string;
54
+ quantity?: number;
55
+ }[];
56
+ hardware?: HardwareItem[];
57
+ };
58
+ export type PrintSpec = {
59
+ printspecVersion: string;
60
+ units: 'mm';
61
+ part?: PartFamilySpec | ComposablePartSpec;
62
+ project?: ProjectSpec;
63
+ hardware?: HardwareItem[];
64
+ metadata?: Record<string, unknown>;
65
+ };
66
+ export type GeneratorResult = {
67
+ supported: boolean;
68
+ code: string;
69
+ message?: string;
70
+ warnings?: string[];
71
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { ValidationResult } from './types.js';
2
+ type ValidationOptions = {
3
+ semantic?: boolean;
4
+ };
5
+ export declare function validatePartFamilySpec(part: unknown, _options?: ValidationOptions): ValidationResult;
6
+ export declare function validateComposablePartSpec(part: unknown, _options?: ValidationOptions): ValidationResult;
7
+ export declare function validateProjectSpec(project: unknown, _options?: ValidationOptions): ValidationResult;
8
+ export declare function validatePrintSpec(spec: unknown, options?: ValidationOptions): ValidationResult;
9
+ export {};
@@ -0,0 +1,45 @@
1
+ import { validateSemantic } from './semantic.js';
2
+ import { createAjv, schemas } from './schemas.js';
3
+ const ajv = createAjv();
4
+ const validators = new Map();
5
+ function validatorFor(schemaName) {
6
+ const existing = validators.get(schemaName);
7
+ if (existing)
8
+ return existing;
9
+ const schema = schemas[schemaName];
10
+ if (!schema)
11
+ throw new Error(`Missing local schema: ${schemaName}`);
12
+ const compiled = ajv.compile(schema);
13
+ validators.set(schemaName, compiled);
14
+ return compiled;
15
+ }
16
+ function formatPath(instancePath) {
17
+ return instancePath && instancePath.length > 0 ? instancePath : '/';
18
+ }
19
+ function formatAjvErrors(validate) {
20
+ return (validate.errors ?? []).map((error) => {
21
+ const keyword = error.keyword ? ` [${error.keyword}]` : '';
22
+ const message = error.message ?? 'failed schema validation';
23
+ return `${formatPath(error.instancePath)}: ${message}${keyword}`;
24
+ });
25
+ }
26
+ function validateWithSchema(schemaName, value, semantic = false) {
27
+ const validate = validatorFor(schemaName);
28
+ const ok = validate(value);
29
+ const errors = ok ? [] : formatAjvErrors(validate);
30
+ if (errors.length === 0 && semantic)
31
+ errors.push(...validateSemantic(value));
32
+ return { valid: errors.length === 0, errors };
33
+ }
34
+ export function validatePartFamilySpec(part, _options = {}) {
35
+ return validateWithSchema('part-family.schema.json', part, false);
36
+ }
37
+ export function validateComposablePartSpec(part, _options = {}) {
38
+ return validateWithSchema('composable-part.schema.json', part, false);
39
+ }
40
+ export function validateProjectSpec(project, _options = {}) {
41
+ return validateWithSchema('project.schema.json', project, false);
42
+ }
43
+ export function validatePrintSpec(spec, options = {}) {
44
+ return validateWithSchema('printspec.schema.json', spec, options.semantic !== false);
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invisra/printspec",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "JSON schemas, validators, BOM helpers, CLI commands, and starter generators for practical parametric 3D-printable parts.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Invisra",
@@ -45,7 +45,9 @@
45
45
  "scripts": {
46
46
  "build": "tsc",
47
47
  "test": "npm run build && node --test ../../tests/typescript/*.test.js",
48
- "clean": "rm -rf dist"
48
+ "clean": "rm -rf dist",
49
+ "prepack": "npm run build",
50
+ "prepublishOnly": "npm run build && test -f dist/index.js && test -f dist/index.d.ts && test -f dist/cli.js"
49
51
  },
50
52
  "dependencies": {
51
53
  "ajv": "^8.17.1",