dicom-synth 1.4.0 → 1.6.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/README.md +104 -4
- package/bin/dicom-synth-describe.mjs +73 -0
- package/bin/dicom-synth-generate.mjs +100 -40
- package/dist/esm/describe/describe.js +378 -0
- package/dist/esm/index.js +348 -42
- package/dist/esm/schema/parametric.js +327 -0
- package/dist/esm/schema/validate.js +120 -33
- package/dist/esm/syntheticFixtures/uid.js +4 -1
- package/dist/types/describe/describe.d.ts +13 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/schema/parametric.d.ts +2 -0
- package/dist/types/schema/types.d.ts +21 -0
- package/dist/types/schema/validate.d.ts +2 -1
- package/dist/types/syntheticFixtures/uid.d.ts +2 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -381,6 +381,71 @@ The validator throws with a human-readable message on any structural error.
|
|
|
381
381
|
|
|
382
382
|
---
|
|
383
383
|
|
|
384
|
+
## Parametric designer
|
|
385
|
+
|
|
386
|
+
A `ParametricSpec` describes a dataset by ranges instead of explicit entries. `resolveParametricSpec` resolves it — deterministically for a given seed — into a concrete `DatasetSpec` that can be inspected, saved, and re-run.
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
import { resolveParametricSpec, writeCollectionFromSpec } from 'dicom-synth'
|
|
390
|
+
|
|
391
|
+
const resolved = resolveParametricSpec({
|
|
392
|
+
seed: 42,
|
|
393
|
+
studies: {
|
|
394
|
+
count: { min: 2, max: 4 }, // ranges are inclusive
|
|
395
|
+
seriesPerStudy: { min: 1, max: 3 },
|
|
396
|
+
filesPerSeries: { min: 5, max: 20 },
|
|
397
|
+
fileSizeKb: { min: 100, max: 600 }, // sampled per file
|
|
398
|
+
modalities: ['CT', 'PT'], // one drawn per series
|
|
399
|
+
},
|
|
400
|
+
edgeCases: [{ type: 'invalid-uid-image', frequency: 0.05 }],
|
|
401
|
+
layout: 'hierarchical',
|
|
402
|
+
})
|
|
403
|
+
await writeCollectionFromSpec(resolved, './out')
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
- Every field of `studies` accepts a fixed number or an inclusive `{ min, max }` range (`Range`).
|
|
407
|
+
- `fileSizeKb` maps to `targetSizeKb`; a fixed value collapses each series into one counted entry, a range samples per file.
|
|
408
|
+
- `edgeCases` are `EntrySpec`s appended to the flat `entries` of the resolved spec. With `frequency` (0–1), the count is drawn per file across the whole dataset — the resolved spec records the actual draw. Without it, the entry is appended as-is (use `count` for absolute numbers). At small dataset sizes a low frequency may draw zero occurrences.
|
|
409
|
+
- The seed (drawn randomly when omitted) is embedded in the resolved spec, so resolution is always reproducible.
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
import type { ParametricSpec, ParametricStudies, ParametricEdgeCase, Range } from 'dicom-synth'
|
|
413
|
+
import { validateParametricSpec } from 'dicom-synth'
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
See `examples/parametric.json` for a JSON example and `examples/convert-data-shape.mjs` for converting an exported tree-shape JSON into a `DatasetSpec` (skips non-DICOM files).
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Describe tool
|
|
421
|
+
|
|
422
|
+
`describeDirectory` scans an existing DICOM tree and emits a concrete `DatasetSpec` reproducing its shape — studies/series grouped by their UIDs, per-file size as `targetSizeKb`, and modality. This closes the loop: real tree → spec → regenerate.
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
import { describeDirectory, writeCollectionFromSpec } from 'dicom-synth'
|
|
426
|
+
|
|
427
|
+
const { spec, stats } = describeDirectory('./real-dataset')
|
|
428
|
+
// stats: { dicomFiles, skipped, unknownModality }
|
|
429
|
+
await writeCollectionFromSpec(spec, './reproduced')
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
- **Shape only by default** — no tags are copied; the regenerated dataset carries fresh UIDs and synthetic identifiers, so it cannot leak PHI from the source.
|
|
433
|
+
- **Opt-in tag preservation** — `keepTags` copies the named tags verbatim (some datasets are valuable for testing *precisely because* of their problematic tags):
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
describeDirectory('./real-dataset', { keepTags: ['Manufacturer', '00080060'] })
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
⚠️ Tags are copied exactly as found. If a kept tag contains PHI, handling that PHI is the **caller's responsibility** — the tool extracts only what you name.
|
|
440
|
+
- Files in a series collapse into one counted entry only when modality, size, and kept tags all match, so tag/size variation is preserved rather than flattened.
|
|
441
|
+
- Non-DICOM, unparseable, or ungroupable files (missing study/series UIDs) are skipped and counted. An unsupported `Modality` is emitted as the default (CT) and counted.
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import type { DescribeOptions, DescribeResult } from 'dicom-synth'
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
384
449
|
## CLI
|
|
385
450
|
|
|
386
451
|
```bash
|
|
@@ -392,8 +457,21 @@ dicom-synth-generate --schema-inline '{"entries":[{"type":"valid-image","count":
|
|
|
392
457
|
|
|
393
458
|
# Default output directory is ./fixtures/generated
|
|
394
459
|
dicom-synth-generate --schema dataset.json
|
|
460
|
+
|
|
461
|
+
# Resolve and generate from a parametric spec; save the resolved DatasetSpec
|
|
462
|
+
dicom-synth-generate --parametric examples/parametric.json --out ./out --emit-spec resolved.json
|
|
463
|
+
|
|
464
|
+
# Preview the manifest (paths, types, sizes) without writing anything
|
|
465
|
+
dicom-synth-generate --parametric examples/parametric.json --dry-run
|
|
395
466
|
```
|
|
396
467
|
|
|
468
|
+
| Flag | Effect |
|
|
469
|
+
|---|---|
|
|
470
|
+
| `--schema <path>` / `--schema-inline <json>` / `--parametric <path>` | Input spec (mutually exclusive) |
|
|
471
|
+
| `--out <dir>` | Output directory (default `./fixtures/generated`) |
|
|
472
|
+
| `--emit-spec <path>` | Write the resolved `DatasetSpec` JSON (requires `--parametric`) |
|
|
473
|
+
| `--dry-run` | Generate in memory and print the manifest; nothing is written |
|
|
474
|
+
|
|
397
475
|
The default schema (`examples/default.json`) generates one each of `valid-image`, `invalid-uid-image`, and `vendor-warnings-image`:
|
|
398
476
|
|
|
399
477
|
```bash
|
|
@@ -406,6 +484,24 @@ dicom-synth-generate --schema examples/default.json --out /tmp/out
|
|
|
406
484
|
|
|
407
485
|
Schema validation errors exit with code 1 and a descriptive message.
|
|
408
486
|
|
|
487
|
+
### Describe an existing tree
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
# Shape only (PHI-safe) — prints the DatasetSpec to stdout
|
|
491
|
+
dicom-synth-describe ./real-dataset
|
|
492
|
+
|
|
493
|
+
# Save to a file, and keep selected tags (caller owns any PHI in them)
|
|
494
|
+
dicom-synth-describe ./real-dataset --out spec.json --keep-tags Manufacturer,00080060
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
| Flag | Effect |
|
|
498
|
+
|---|---|
|
|
499
|
+
| `<dir>` | DICOM tree to scan (required) |
|
|
500
|
+
| `--out <path>` | Write the `DatasetSpec` JSON (default: stdout) |
|
|
501
|
+
| `--keep-tags <a,b,...>` | Copy the named tags (keywords or 8-hex) verbatim; omit for shape only |
|
|
502
|
+
|
|
503
|
+
Scan stats (DICOM/skipped/unsupported-modality counts) are written to stderr.
|
|
504
|
+
|
|
409
505
|
---
|
|
410
506
|
|
|
411
507
|
## Public fixtures
|
|
@@ -471,7 +567,9 @@ pnpm fetch:public-case -- pydicom-CT-small
|
|
|
471
567
|
| Path | Responsibility |
|
|
472
568
|
|---|---|
|
|
473
569
|
| `src/schema/types.ts` | All public TypeScript types (`FileSpec`, `DatasetSpec`, `ViolationClass`, etc.) |
|
|
474
|
-
| `src/schema/validate.ts` | `validateDatasetSpec` —
|
|
570
|
+
| `src/schema/validate.ts` | `validateDatasetSpec` / `validateParametricSpec` — validate raw JSON values against the schema |
|
|
571
|
+
| `src/schema/parametric.ts` | `resolveParametricSpec` — seeded `ParametricSpec` → `DatasetSpec` resolution |
|
|
572
|
+
| `src/describe/describe.ts` | `describeDirectory` — scan a DICOM tree → `DatasetSpec` |
|
|
475
573
|
| `src/syntheticFixtures/generator.ts` | Internal buffer builders keyed on `FileSpec` type |
|
|
476
574
|
| `src/syntheticFixtures/uid.ts` | Seeded and random UID generation |
|
|
477
575
|
| `src/syntheticFixtures/violations.ts` | Post-processing violation injection |
|
|
@@ -480,7 +578,10 @@ pnpm fetch:public-case -- pydicom-CT-small
|
|
|
480
578
|
| `src/public-fixtures/fetch.ts` | Fetch, SHA-256 verify, content-addressed cache |
|
|
481
579
|
| `bin/dicom-synth-generate.mjs` | Published generate CLI (requires `pnpm build`) |
|
|
482
580
|
| `bin/dicom-synth-fetch.mjs` | Published fetch CLI (requires `pnpm build`) |
|
|
581
|
+
| `bin/dicom-synth-describe.mjs` | Published describe CLI (requires `pnpm build`) |
|
|
483
582
|
| `examples/default.json` | Default `DatasetSpec` — one each of `valid-image`, `invalid-uid-image`, `vendor-warnings-image` |
|
|
583
|
+
| `examples/parametric.json` | Example `ParametricSpec` for the `--parametric` CLI path |
|
|
584
|
+
| `examples/convert-data-shape.mjs` | Example converter: tree-shape JSON → `DatasetSpec` |
|
|
484
585
|
|
|
485
586
|
---
|
|
486
587
|
|
|
@@ -506,9 +607,8 @@ Git hooks: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
506
607
|
|
|
507
608
|
## Future development
|
|
508
609
|
|
|
509
|
-
- **
|
|
510
|
-
- **
|
|
511
|
-
- **Dry-run mode** — emit the manifest (paths, types, grouping) without writing files
|
|
610
|
+
- **Dimension edge-case recipes** — documented presets for geometry pathologies (1×65535 strips, high frame counts)
|
|
611
|
+
- **Per-file tag variance templating** — patterned overrides like `"PatientID": "P{index}"` for shape replication
|
|
512
612
|
- **Compressed transfer syntaxes** — JPEG-LS, JPEG 2000, RLE
|
|
513
613
|
- **Private fixture catalogues** — same SHA-256 fetch/cache pattern for credentials-backed sources (e.g. S3)
|
|
514
614
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { describeDirectory } from '../dist/esm/index.js'
|
|
5
|
+
|
|
6
|
+
function usage() {
|
|
7
|
+
console.error(
|
|
8
|
+
'Usage: dicom-synth-describe <dir> [--out <spec.json>] [--keep-tags <a,b,...>]\n' +
|
|
9
|
+
'\n' +
|
|
10
|
+
'Scans a DICOM tree and emits a DatasetSpec describing its shape\n' +
|
|
11
|
+
'(studies/series, per-file size, modality).\n' +
|
|
12
|
+
'\n' +
|
|
13
|
+
'By default only the shape is reproduced — no tags are copied.\n' +
|
|
14
|
+
'--keep-tags copies the named tags (DICOM keywords or 8-hex tags)\n' +
|
|
15
|
+
"verbatim; if a kept tag holds PHI, handling it is the caller's\n" +
|
|
16
|
+
"responsibility, not this tool's.",
|
|
17
|
+
)
|
|
18
|
+
process.exit(1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2)
|
|
22
|
+
if (args.length === 0) usage()
|
|
23
|
+
|
|
24
|
+
let dir
|
|
25
|
+
let outPath
|
|
26
|
+
let keepTags
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
if (args[i] === '--out' && args[i + 1]) {
|
|
30
|
+
outPath = resolve(args[++i])
|
|
31
|
+
} else if (args[i] === '--keep-tags' && args[i + 1]) {
|
|
32
|
+
keepTags = args[++i]
|
|
33
|
+
.split(',')
|
|
34
|
+
.map((t) => t.trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
} else if (args[i].startsWith('--')) {
|
|
37
|
+
console.error(`Unknown argument: ${args[i]}`)
|
|
38
|
+
usage()
|
|
39
|
+
} else if (dir === undefined) {
|
|
40
|
+
dir = resolve(args[i])
|
|
41
|
+
} else {
|
|
42
|
+
console.error(`Unexpected argument: ${args[i]}`)
|
|
43
|
+
usage()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (dir === undefined) usage()
|
|
48
|
+
|
|
49
|
+
let result
|
|
50
|
+
try {
|
|
51
|
+
result = describeDirectory(dir, keepTags ? { keepTags } : {})
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(`Error: ${err.message}`)
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const json = `${JSON.stringify(result.spec, null, 2)}\n`
|
|
58
|
+
if (outPath) {
|
|
59
|
+
writeFileSync(outPath, json)
|
|
60
|
+
console.error(`Wrote DatasetSpec to ${outPath}`)
|
|
61
|
+
} else {
|
|
62
|
+
process.stdout.write(json)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { dicomFiles, skipped, unknownModality } = result.stats
|
|
66
|
+
console.error(
|
|
67
|
+
`Described ${dicomFiles} DICOM file(s); skipped ${skipped} non-DICOM/ungroupable file(s)`,
|
|
68
|
+
)
|
|
69
|
+
if (unknownModality > 0) {
|
|
70
|
+
console.error(
|
|
71
|
+
` ${unknownModality} file(s) had an unsupported Modality — emitted as default (CT)`,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { resolve } from 'node:path'
|
|
4
4
|
import {
|
|
5
|
+
generateCollectionFromSpec,
|
|
6
|
+
resolveParametricSpec,
|
|
5
7
|
validateDatasetSpec,
|
|
6
8
|
writeCollectionFromSpec,
|
|
7
9
|
} from '../dist/esm/index.js'
|
|
8
10
|
|
|
9
11
|
function usage() {
|
|
10
12
|
console.error(
|
|
11
|
-
'Usage: dicom-synth-generate --schema <path> [--out <dir>]\n' +
|
|
12
|
-
' dicom-synth-generate --schema-inline <json> [--out <dir>]'
|
|
13
|
+
'Usage: dicom-synth-generate --schema <path> [--out <dir>] [--dry-run]\n' +
|
|
14
|
+
' dicom-synth-generate --schema-inline <json> [--out <dir>] [--dry-run]\n' +
|
|
15
|
+
' dicom-synth-generate --parametric <path> [--out <dir>] [--emit-spec <path>] [--dry-run]',
|
|
13
16
|
)
|
|
14
17
|
process.exit(1)
|
|
15
18
|
}
|
|
@@ -19,36 +22,50 @@ const args = process.argv.slice(2)
|
|
|
19
22
|
if (args.length === 0) usage()
|
|
20
23
|
|
|
21
24
|
let rawSpec
|
|
25
|
+
let specSource
|
|
22
26
|
let outDir = resolve('./fixtures/generated')
|
|
27
|
+
let emitSpecPath
|
|
28
|
+
let dryRun = false
|
|
29
|
+
|
|
30
|
+
function readSpecArg(flag, value) {
|
|
31
|
+
if (rawSpec !== undefined) {
|
|
32
|
+
console.error(
|
|
33
|
+
'Error: --schema, --schema-inline and --parametric are mutually exclusive',
|
|
34
|
+
)
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
specSource = flag
|
|
38
|
+
if (flag === '--schema-inline') {
|
|
39
|
+
try {
|
|
40
|
+
rawSpec = JSON.parse(value)
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(`Error parsing inline schema: ${err.message}`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
try {
|
|
47
|
+
rawSpec = JSON.parse(readFileSync(resolve(value), 'utf8'))
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(`Error reading ${flag.slice(2)} file: ${err.message}`)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
23
54
|
|
|
24
55
|
for (let i = 0; i < args.length; i++) {
|
|
25
56
|
if (
|
|
26
|
-
(args[i] === '--schema' ||
|
|
57
|
+
(args[i] === '--schema' ||
|
|
58
|
+
args[i] === '--schema-inline' ||
|
|
59
|
+
args[i] === '--parametric') &&
|
|
27
60
|
args[i + 1]
|
|
28
61
|
) {
|
|
29
|
-
|
|
30
|
-
console.error(
|
|
31
|
-
'Error: --schema and --schema-inline are mutually exclusive',
|
|
32
|
-
)
|
|
33
|
-
process.exit(1)
|
|
34
|
-
}
|
|
35
|
-
if (args[i] === '--schema') {
|
|
36
|
-
try {
|
|
37
|
-
rawSpec = JSON.parse(readFileSync(resolve(args[++i]), 'utf8'))
|
|
38
|
-
} catch (err) {
|
|
39
|
-
console.error(`Error reading schema file: ${err.message}`)
|
|
40
|
-
process.exit(1)
|
|
41
|
-
}
|
|
42
|
-
} else {
|
|
43
|
-
try {
|
|
44
|
-
rawSpec = JSON.parse(args[++i])
|
|
45
|
-
} catch (err) {
|
|
46
|
-
console.error(`Error parsing inline schema: ${err.message}`)
|
|
47
|
-
process.exit(1)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
62
|
+
readSpecArg(args[i], args[++i])
|
|
50
63
|
} else if (args[i] === '--out' && args[i + 1]) {
|
|
51
64
|
outDir = resolve(args[++i])
|
|
65
|
+
} else if (args[i] === '--emit-spec' && args[i + 1]) {
|
|
66
|
+
emitSpecPath = resolve(args[++i])
|
|
67
|
+
} else if (args[i] === '--dry-run') {
|
|
68
|
+
dryRun = true
|
|
52
69
|
} else {
|
|
53
70
|
console.error(`Unknown argument: ${args[i]}`)
|
|
54
71
|
usage()
|
|
@@ -57,29 +74,72 @@ for (let i = 0; i < args.length; i++) {
|
|
|
57
74
|
|
|
58
75
|
if (rawSpec === undefined) usage()
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
77
|
+
function printTypeCounts(counts) {
|
|
78
|
+
for (const [type, count] of Object.entries(counts)) {
|
|
79
|
+
console.log(` ${type}: ${count}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (emitSpecPath !== undefined && specSource !== '--parametric') {
|
|
84
|
+
console.error('Error: --emit-spec requires --parametric')
|
|
65
85
|
process.exit(1)
|
|
66
86
|
}
|
|
67
87
|
|
|
68
|
-
let
|
|
88
|
+
let spec
|
|
69
89
|
try {
|
|
70
|
-
|
|
90
|
+
spec =
|
|
91
|
+
specSource === '--parametric'
|
|
92
|
+
? resolveParametricSpec(rawSpec)
|
|
93
|
+
: validateDatasetSpec(rawSpec)
|
|
71
94
|
} catch (err) {
|
|
72
|
-
console.error(`
|
|
95
|
+
console.error(`Schema validation error: ${err.message}`)
|
|
73
96
|
process.exit(1)
|
|
74
97
|
}
|
|
75
98
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
if (emitSpecPath !== undefined) {
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(emitSpecPath, `${JSON.stringify(spec, null, 2)}\n`)
|
|
102
|
+
console.log(`Wrote resolved spec to ${emitSpecPath}`)
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`Error writing resolved spec: ${err.message}`)
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
80
107
|
}
|
|
81
108
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
if (dryRun) {
|
|
110
|
+
const counts = {}
|
|
111
|
+
let fileCount = 0
|
|
112
|
+
let byteCount = 0
|
|
113
|
+
try {
|
|
114
|
+
for await (const file of generateCollectionFromSpec(spec)) {
|
|
115
|
+
console.log(
|
|
116
|
+
`${file.relativePath} (${file.type}, ${(file.buffer.length / 1024).toFixed(1)} KB)`,
|
|
117
|
+
)
|
|
118
|
+
counts[file.type] = (counts[file.type] ?? 0) + 1
|
|
119
|
+
fileCount++
|
|
120
|
+
byteCount += file.buffer.length
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(`Error generating collection: ${err.message}`)
|
|
124
|
+
process.exit(1)
|
|
125
|
+
}
|
|
126
|
+
console.log(
|
|
127
|
+
`Dry run: ${fileCount} file(s), ${(byteCount / 1024).toFixed(1)} KB total — nothing written`,
|
|
128
|
+
)
|
|
129
|
+
printTypeCounts(counts)
|
|
130
|
+
} else {
|
|
131
|
+
let manifest
|
|
132
|
+
try {
|
|
133
|
+
manifest = await writeCollectionFromSpec(spec, outDir)
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`Error writing collection: ${err.message}`)
|
|
136
|
+
process.exit(1)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const counts = {}
|
|
140
|
+
for (const entry of manifest) {
|
|
141
|
+
counts[entry.type] = (counts[entry.type] ?? 0) + 1
|
|
142
|
+
}
|
|
143
|
+
console.log(`Wrote ${manifest.length} file(s) to ${outDir}`)
|
|
144
|
+
printTypeCounts(counts)
|
|
85
145
|
}
|