@shayc/open-board-format 0.4.1 → 0.5.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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # @shayc/open-board-format
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 24535c7: Throw a typed `OBFError` instead of plain `Error`. Every failure now carries a discriminated `error.info`, so consumers branch on `error.info.code` (e.g. `"missing-resource"`, `"invalid-board"`, `"not-zip"`) and read structured fields off each variant — no message parsing.
8
+
9
+ - New exports: `OBFError` (class), and the `OBFErrorInfo`, `OBFErrorCode`, and `OBFIssue` types.
10
+ - Validation failures (`invalid-board`, `invalid-manifest`) expose the Zod `issues` list on `error.info`. The underlying error — the `ZodError`, or the `JSON.parse`/fflate failure for `not-json`/`unreadable-zip`/`zip-failed` — is always on the standard `error.cause` and never duplicated on `info`.
11
+ - An `internal` code marks a library-invariant violation (a bug here), not something callers can recover from.
12
+ - **Breaking:** thrown errors are now `OBFError` (`error.name` is `"OBFError"`) and message strings changed. Branch on `error.info.code` rather than matching `error.message`. The internal `buildJsonParseErrorMessage` helper was removed.
13
+
14
+ ## 0.4.2
15
+
16
+ ### Patch Changes
17
+
18
+ - d2beda3: docs: ship the OBF spec mirror (`docs/external/open-board-format.md`) in the npm package
19
+
20
+ The README already points at this file; including it makes that link resolve
21
+ inside `node_modules` and gives offline tooling and coding agents the full
22
+ format semantics (specialty actions, string-list fallback, ID uniqueness)
23
+ that type declarations can't carry.
24
+
3
25
  ## 0.4.1
4
26
 
5
27
  ### Patch Changes
package/README.md CHANGED
@@ -175,16 +175,52 @@ import { OBFButtonSchema, OBFManifestSchema } from "@shayc/open-board-format";
175
175
 
176
176
  ## Errors
177
177
 
178
- All failures throw plain `Error`. The message identifies what failed, typically with one of these prefixes:
178
+ Every failure throws an `OBFError`. Branch on `error.info.code` — a discriminated union where each `code` carries exactly the fields relevant to that failure. Don't match on `error.message`; the message is human-readable and may change between releases.
179
179
 
180
- - `Invalid OBF:` — schema validation rejected an OBF board.
181
- - `Invalid OBZ:` the package was rejected.
182
- - On read: not a ZIP, missing manifest, the manifest references a board file not in the archive, or a board's `id` differs from the ID the manifest declares for it.
183
- - On write (`createOBZ`): `rootBoardId` matches no board, two boards share the same `id`, a board fails validation, two boards map the same media `id` to conflicting paths, a declared image/sound `path` has no matching resource, or a resource would overwrite a generated entry.
184
- - `Invalid manifest:` — `manifest.json` failed to parse or validate, including a `root` that is not listed in `paths.boards`.
185
- - `Failed to unzip:` / `Failed to zip:` — the archive could not be decompressed (truncated or corrupt despite a valid ZIP signature) or compressed.
180
+ ```ts
181
+ import { loadBoard, OBFError } from "@shayc/open-board-format";
182
+
183
+ try {
184
+ await loadBoard(file);
185
+ } catch (error) {
186
+ if (!(error instanceof OBFError)) throw error;
187
+
188
+ switch (error.info.code) {
189
+ case "missing-resource":
190
+ // `kind`, `mediaId`, and `path` are all typed and present here
191
+ console.warn(`Missing ${error.info.kind} at ${error.info.path}`);
192
+ break;
193
+ case "invalid-board":
194
+ // `issues` is the Zod issue list — which field failed and why
195
+ console.error(error.info.issues);
196
+ break;
197
+ default:
198
+ console.error(error.message);
199
+ }
200
+ }
201
+ ```
186
202
 
187
- When the root cause is a `JSON.parse` failure, the original error is preserved as `error.cause`. For finer-grained validation, drop one level down and use the Zod schemas directly with `safeParse` — the `issues` array tells you exactly which field failed.
203
+ The `code` values, grouped by what they describe:
204
+
205
+ | Group | `info.code` | Key fields (on `info`) |
206
+ | ----------- | ------------------- | -------------------------------- |
207
+ | Decoding | `not-json` | `source` |
208
+ | | `not-zip` | — |
209
+ | | `unreadable-zip` | — |
210
+ | Validation | `invalid-board` | `issues`, `boardId?` |
211
+ | | `invalid-manifest` | `issues` |
212
+ | Read (OBZ) | `missing-manifest` | — |
213
+ | | `missing-board` | `boardId`, `path` |
214
+ | | `board-id-mismatch` | `path`, `declaredId`, `actualId` |
215
+ | Write (OBZ) | `unknown-root` | `rootBoardId` |
216
+ | | `duplicate-board` | `boardId` |
217
+ | | `missing-resource` | `kind`, `mediaId`, `path` |
218
+ | | `conflicting-paths` | `kind`, `mediaId`, `paths` |
219
+ | | `path-collision` | `path` |
220
+ | | `zip-failed` | — |
221
+ | Internal | `internal` | `detail` |
222
+
223
+ `OBFErrorInfo` and `OBFErrorCode` are exported for exhaustive handling. The underlying error, when there is one, is always on the standard `error.cause` — never duplicated on `info`. Validation failures (`invalid-board`, `invalid-manifest`) put the `ZodError` there, so you can call `z.treeifyError(error.cause)` for nested, UI-friendly output, while `info.issues` (Zod's issue type, re-exported as `OBFIssue`) gives you the flat list directly. For `not-json` / `*-zip` failures `error.cause` is the underlying parser or fflate error. An `internal` code signals a bug in this library that callers can't recover from — please report it.
188
224
 
189
225
  ## Security
190
226
 
package/dist/index.d.mts CHANGED
@@ -354,6 +354,89 @@ declare const OBFManifestSchema: z.ZodObject<{
354
354
  */
355
355
  type OBFManifest = z.infer<typeof OBFManifestSchema>;
356
356
  //#endregion
357
+ //#region src/errors.d.ts
358
+ /**
359
+ * A single schema validation problem — Zod's issue shape, re-exported under a
360
+ * domain name. `z.core.$ZodIssue` is the type Zod v4 designates for libraries
361
+ * built on it (the bare `z.ZodIssue` is deprecated in its favor); aliasing it
362
+ * gives consumers a stable OBF name without reaching into Zod's `core` export.
363
+ */
364
+ type OBFIssue = z.core.$ZodIssue;
365
+ /**
366
+ * Discriminated description of why an {@link OBFError} was thrown.
367
+ *
368
+ * Switch on `code`; each variant carries the fields relevant to it. When a
369
+ * failure wraps an underlying error it lives on the standard `error.cause`,
370
+ * never duplicated here. The only optional field is `invalid-board`'s
371
+ * `boardId`, absent when validation runs on a value with no known id.
372
+ */
373
+ type OBFErrorInfo = /** Input was not parseable JSON. */{
374
+ code: "not-json";
375
+ source: "board" | "manifest";
376
+ } /** An OBZ archive was expected, but the bytes are not a ZIP. */ | {
377
+ code: "not-zip";
378
+ } /** A ZIP archive could not be decompressed. */ | {
379
+ code: "unreadable-zip";
380
+ } /** A board failed schema validation. `boardId` is set when known. */ | {
381
+ code: "invalid-board";
382
+ boardId?: string;
383
+ issues: readonly OBFIssue[];
384
+ } /** A manifest failed schema validation. */ | {
385
+ code: "invalid-manifest";
386
+ issues: readonly OBFIssue[];
387
+ } /** The archive has no `manifest.json`. */ | {
388
+ code: "missing-manifest";
389
+ } /** A board the manifest declares is absent from the archive. */ | {
390
+ code: "missing-board";
391
+ boardId: string;
392
+ path: string;
393
+ } /** A board's `id` disagrees with the id the manifest declares for it. */ | {
394
+ code: "board-id-mismatch";
395
+ path: string;
396
+ declaredId: string;
397
+ actualId: string;
398
+ } /** `rootBoardId` matches none of the supplied boards. */ | {
399
+ code: "unknown-root";
400
+ rootBoardId: string;
401
+ } /** Two supplied boards share the same `id`. */ | {
402
+ code: "duplicate-board";
403
+ boardId: string;
404
+ } /** A board declares a media `path` with no matching resource. */ | {
405
+ code: "missing-resource";
406
+ kind: "image" | "sound";
407
+ mediaId: string;
408
+ path: string;
409
+ } /** Two boards declare the same media id with different paths. */ | {
410
+ code: "conflicting-paths";
411
+ kind: "image" | "sound";
412
+ mediaId: string;
413
+ paths: [string, string];
414
+ } /** A supplied resource would overwrite a generated board or the manifest. */ | {
415
+ code: "path-collision";
416
+ path: string;
417
+ } /** The archive could not be compressed. */ | {
418
+ code: "zip-failed";
419
+ } /** An internal invariant was violated — a bug in this library; please report. */ | {
420
+ code: "internal";
421
+ detail: string;
422
+ };
423
+ /** Every `code` an {@link OBFError} can carry. */
424
+ type OBFErrorCode = OBFErrorInfo["code"];
425
+ /**
426
+ * The single error type thrown by `@shayc/open-board-format`.
427
+ *
428
+ * Branch on {@link OBFError.info} (a discriminated {@link OBFErrorInfo}) rather
429
+ * than parsing {@link OBFError.message}. Any underlying error — a `JSON.parse`
430
+ * failure, a `ZodError`, or an fflate error — is on the standard `error.cause`.
431
+ */
432
+ declare class OBFError extends Error {
433
+ /** Structured, discriminated description of the failure. */
434
+ readonly info: OBFErrorInfo;
435
+ constructor(info: OBFErrorInfo, options?: {
436
+ cause?: unknown;
437
+ });
438
+ }
439
+ //#endregion
357
440
  //#region src/obf.d.ts
358
441
  /**
359
442
  * Parse a JSON string into a validated OBF board.
@@ -364,7 +447,8 @@ type OBFManifest = z.infer<typeof OBFManifestSchema>;
364
447
  * @param json - The JSON string to parse.
365
448
  * @returns The validated board object.
366
449
  *
367
- * @throws {Error} If the JSON is malformed or fails schema validation.
450
+ * @throws {@link OBFError} with `info.code` `"not-json"` if the JSON is
451
+ * malformed, or `"invalid-board"` if it fails schema validation.
368
452
  */
369
453
  declare function parseOBF(json: string): OBFBoard;
370
454
  /**
@@ -376,7 +460,8 @@ declare function parseOBF(json: string): OBFBoard;
376
460
  * @param file - A `File` handle pointing to an `.obf` file.
377
461
  * @returns The validated board object.
378
462
  *
379
- * @throws {Error} If the file content is malformed or fails schema validation.
463
+ * @throws {@link OBFError} with `info.code` `"not-json"` if the file content is
464
+ * malformed, or `"invalid-board"` if it fails schema validation.
380
465
  */
381
466
  declare function loadOBF(file: File): Promise<OBFBoard>;
382
467
  /**
@@ -385,7 +470,8 @@ declare function loadOBF(file: File): Promise<OBFBoard>;
385
470
  * @param data - The value to validate.
386
471
  * @returns The validated board object.
387
472
  *
388
- * @throws {Error} If the value fails schema validation.
473
+ * @throws {@link OBFError} with `info.code` `"invalid-board"` if the value fails
474
+ * schema validation. `info.issues` holds the underlying Zod issues.
389
475
  */
390
476
  declare function validateOBF(data: unknown): OBFBoard;
391
477
  /**
@@ -426,7 +512,8 @@ interface ParsedOBZ {
426
512
  * @param file - A `File` handle pointing to an `.obz` archive.
427
513
  * @returns The parsed manifest, boards, root board, and binary resources.
428
514
  *
429
- * @throws {Error} Same failures as {@link extractOBZ}, which this delegates to.
515
+ * @throws {@link OBFError} the same failures as {@link extractOBZ}, which
516
+ * this delegates to.
430
517
  */
431
518
  declare function loadOBZ(file: File): Promise<ParsedOBZ>;
432
519
  /**
@@ -437,9 +524,10 @@ declare function loadOBZ(file: File): Promise<ParsedOBZ>;
437
524
  * the resolved root board, and a map of file paths to their
438
525
  * binary content.
439
526
  *
440
- * @throws {Error} If the archive is not a valid ZIP, the manifest is missing,
441
- * a board declared in the manifest is missing or fails validation, or a
442
- * board's `id` differs from the ID the manifest declares for it.
527
+ * @throws {@link OBFError}; branch on `info.code`: `"not-zip"`,
528
+ * `"unreadable-zip"`, `"missing-manifest"`, `"not-json"` or
529
+ * `"invalid-manifest"` (bad manifest), `"missing-board"`,
530
+ * `"board-id-mismatch"`, or `"invalid-board"` (a board fails validation).
443
531
  */
444
532
  declare function extractOBZ(archive: ArrayBuffer): Promise<ParsedOBZ>;
445
533
  /**
@@ -449,7 +537,8 @@ declare function extractOBZ(archive: ArrayBuffer): Promise<ParsedOBZ>;
449
537
  * @param json - A JSON string representing the manifest.
450
538
  * @returns The validated manifest object.
451
539
  *
452
- * @throws {Error} If the JSON is malformed or fails schema validation.
540
+ * @throws {@link OBFError} with `info.code` `"not-json"` if the JSON is
541
+ * malformed, or `"invalid-manifest"` if it fails schema validation.
453
542
  */
454
543
  declare function parseManifest(json: string): OBFManifest;
455
544
  /**
@@ -458,17 +547,19 @@ declare function parseManifest(json: string): OBFManifest;
458
547
  * A manifest is generated automatically from the supplied boards,
459
548
  * using the `rootBoardId` to designate the entry-point board.
460
549
  *
550
+ * Every failure is an {@link OBFError}; branch on `info.code`.
551
+ *
461
552
  * @param boards - The boards to include in the archive.
462
553
  * @param rootBoardId - The ID of the board that serves as the archive's entry point.
463
554
  * @param resources - Optional map of file paths to binary content (images, sounds, etc.).
464
555
  * @returns A `Blob` containing the compressed OBZ archive.
465
556
  *
466
- * @throws {Error} If `rootBoardId` does not match any of the supplied boards.
467
- * @throws {Error} If two supplied boards share the same ID.
468
- * @throws {Error} If a supplied board fails schema validation.
469
- * @throws {Error} If two boards declare the same media ID with conflicting paths.
470
- * @throws {Error} If a board declares an image or sound `path` with no matching entry in `resources`.
471
- * @throws {Error} If a `resources` entry would overwrite the generated `manifest.json` or a board file.
557
+ * @throws {@link OBFError} `"unknown-root"` if `rootBoardId` does not match any of the supplied boards.
558
+ * @throws {@link OBFError} `"duplicate-board"` if two supplied boards share the same ID.
559
+ * @throws {@link OBFError} `"invalid-board"` if a supplied board fails schema validation.
560
+ * @throws {@link OBFError} `"conflicting-paths"` if two boards declare the same media ID with conflicting paths.
561
+ * @throws {@link OBFError} `"missing-resource"` if a board declares an image or sound `path` with no matching entry in `resources`.
562
+ * @throws {@link OBFError} `"path-collision"` if a `resources` entry would overwrite the generated `manifest.json` or a board file.
472
563
  */
473
564
  declare function createOBZ(boards: OBFBoard[], rootBoardId: string, resources?: Map<string, Uint8Array | ArrayBuffer>): Promise<Blob>;
474
565
  //#endregion
@@ -508,8 +599,9 @@ type LoadedBoard = {
508
599
  * @param input - A `File` handle or `ArrayBuffer` holding `.obf` or `.obz` content.
509
600
  * @returns A discriminated union tagged by `format`.
510
601
  *
511
- * @throws {Error} If an OBZ archive is malformed or its manifest is missing,
512
- * or if an OBF board is malformed or fails schema validation.
602
+ * @throws {@link OBFError} the OBZ failures of {@link extractOBZ} when the
603
+ * input is an archive, or the OBF failures of {@link parseOBF} otherwise.
604
+ * Branch on `error.info.code`.
513
605
  */
514
606
  declare function loadBoard(input: File | ArrayBuffer): Promise<LoadedBoard>;
515
607
  //#endregion
@@ -523,7 +615,8 @@ declare function loadBoard(input: File | ArrayBuffer): Promise<LoadedBoard>;
523
615
  * @param archive - The ZIP archive as an `ArrayBuffer`.
524
616
  * @returns A map of file paths to their decompressed content.
525
617
  *
526
- * @throws {Error} If the archive is corrupt or cannot be decompressed.
618
+ * @throws {@link OBFError} with `info.code` `"unreadable-zip"` if the archive is
619
+ * corrupt or cannot be decompressed.
527
620
  */
528
621
  declare function unzip(archive: ArrayBuffer): Promise<Map<string, Uint8Array>>;
529
622
  /**
@@ -536,7 +629,8 @@ declare function unzip(archive: ArrayBuffer): Promise<Map<string, Uint8Array>>;
536
629
  * @param entries - A map of file paths to their content bytes.
537
630
  * @returns The compressed archive as a `Uint8Array`.
538
631
  *
539
- * @throws {Error} If fflate fails to compress an entry.
632
+ * @throws {@link OBFError} with `info.code` `"zip-failed"` if fflate fails to
633
+ * compress an entry.
540
634
  */
541
635
  declare function zip(entries: Map<string, Uint8Array | ArrayBuffer>): Promise<Uint8Array>;
542
636
  /**
@@ -548,5 +642,5 @@ declare function zip(entries: Map<string, Uint8Array | ArrayBuffer>): Promise<Ui
548
642
  */
549
643
  declare function isZip(archive: ArrayBuffer): boolean;
550
644
  //#endregion
551
- export { type LoadedBoard, type OBFBoard, OBFBoardSchema, type OBFButton, type OBFButtonAction, OBFButtonActionSchema, OBFButtonSchema, type OBFFormatVersion, OBFFormatVersionSchema, type OBFGrid, OBFGridSchema, type OBFID, OBFIDSchema, type OBFImage, OBFImageSchema, type OBFLicense, OBFLicenseSchema, type OBFLoadBoard, OBFLoadBoardSchema, type OBFLocaleCode, OBFLocaleCodeSchema, type OBFLocalizedStrings, OBFLocalizedStringsSchema, type OBFManifest, OBFManifestSchema, type OBFMedia, OBFMediaSchema, type OBFSound, OBFSoundSchema, type OBFSpecialtyAction, OBFSpecialtyActionSchema, type OBFSpellingAction, OBFSpellingActionSchema, type OBFStrings, OBFStringsSchema, type OBFSymbolInfo, OBFSymbolInfoSchema, type ParsedOBZ, createOBZ, extractOBZ, isZip, loadBoard, loadOBF, loadOBZ, parseManifest, parseOBF, stringifyOBF, unzip, validateOBF, zip };
645
+ export { type LoadedBoard, type OBFBoard, OBFBoardSchema, type OBFButton, type OBFButtonAction, OBFButtonActionSchema, OBFButtonSchema, OBFError, type OBFErrorCode, type OBFErrorInfo, type OBFFormatVersion, OBFFormatVersionSchema, type OBFGrid, OBFGridSchema, type OBFID, OBFIDSchema, type OBFImage, OBFImageSchema, type OBFIssue, type OBFLicense, OBFLicenseSchema, type OBFLoadBoard, OBFLoadBoardSchema, type OBFLocaleCode, OBFLocaleCodeSchema, type OBFLocalizedStrings, OBFLocalizedStringsSchema, type OBFManifest, OBFManifestSchema, type OBFMedia, OBFMediaSchema, type OBFSound, OBFSoundSchema, type OBFSpecialtyAction, OBFSpecialtyActionSchema, type OBFSpellingAction, OBFSpellingActionSchema, type OBFStrings, OBFStringsSchema, type OBFSymbolInfo, OBFSymbolInfoSchema, type ParsedOBZ, createOBZ, extractOBZ, isZip, loadBoard, loadOBF, loadOBZ, parseManifest, parseOBF, stringifyOBF, unzip, validateOBF, zip };
552
646
  //# sourceMappingURL=index.d.mts.map