@jbrowse/mobx-state-tree 5.9.2 → 5.10.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 CHANGED
@@ -7,12 +7,11 @@ Fork of mobx-state-tree v5.4.2 for use in jbrowse
7
7
  - updated typescript
8
8
  - updated to import actual filepaths instead of node module resolution
9
9
  - reduced rollup config
10
- - tried tsdown but builds were very slow for some reason
11
10
  - removed process.env.NODE_ENV from code, instead exporting a setDevMode function
12
11
  - converted tests to vitest
13
12
  - remove unused dependencies
14
13
  - updated tslint to eslint and typescript-eslint
15
- - attempted to make it a dual CJS/'pure ESM' package but did not work quite yet, using main (CJS) and module (ESM-ish) fields for now since dual build causes trouble with both being imported in vite
14
+ - dual CJS/ESM build using main and module fields (pure ESM causes trouble when both are imported in vite)
16
15
  - added `types.resilient` - wraps a type so that instantiation errors are caught
17
16
  and a fallback type is used instead of crashing the entire state tree. This is
18
17
  useful for arrays or maps that may contain unknown or invalid entries (e.g.
@@ -22,3 +21,6 @@ Fork of mobx-state-tree v5.4.2 for use in jbrowse
22
21
  meaning any value is considered a valid snapshot. Validation is deferred to
23
22
  instantiation time, where failures are caught and routed to the fallback
24
23
  - removed NonEmptyObject annotation (also at upstream in post v5 versions)
24
+ - replaced `unique symbol` phantom properties (`$emptyObject`, `$stateTreeNodeType`) with
25
+ exported string-keyed brand interfaces (`$EmptyObjectBrand`, `$__mstStateTreeNodeType__`),
26
+ fixing tsgo TS4058/TS4023 errors when MST types propagate into exported function return types
@@ -2701,16 +2701,27 @@ function typecheck(type, value) {
2701
2701
  }
2702
2702
  }
2703
2703
  const MAX_ERRORS_REPORTED = 10;
2704
- function validationErrorsToString(type, value, errors) {
2705
- if (errors.length === 0) {
2706
- return undefined;
2707
- }
2704
+ /**
2705
+ * @internal
2706
+ * @hidden
2707
+ * Format a list of validation errors as bullet-style lines, capped at
2708
+ * MAX_ERRORS_REPORTED entries with an overflow suffix. Shared by typecheck()
2709
+ * and union's `noMatchMessage` so prod-build union failures can include
2710
+ * candidate validation errors using the same formatting.
2711
+ */
2712
+ function formatValidationErrorLines(errors) {
2708
2713
  const shown = errors.slice(0, MAX_ERRORS_REPORTED).map(toErrorString);
2709
2714
  const overflow = errors.length - shown.length;
2710
2715
  if (overflow > 0) {
2711
2716
  shown.push(`(… and ${overflow} more error${overflow === 1 ? "" : "s"})`);
2712
2717
  }
2713
- return (`Error while converting ${shortenPrintValue(prettyPrintValue(value))} to \`${type.name}\`:\n\n ` + shown.join("\n "));
2718
+ return shown;
2719
+ }
2720
+ function validationErrorsToString(type, value, errors) {
2721
+ if (errors.length === 0) {
2722
+ return undefined;
2723
+ }
2724
+ return (`Error while converting ${shortenPrintValue(prettyPrintValue(value))} to \`${type.name}\`:\n\n ` + formatValidationErrorLines(errors).join("\n "));
2714
2725
  }
2715
2726
 
2716
2727
  let identifierCacheId = 0;
@@ -5391,6 +5402,25 @@ function enumeration(name, options) {
5391
5402
  return type;
5392
5403
  }
5393
5404
 
5405
+ // Drill through single-subtype wrappers — optional(), refinement(),
5406
+ // snapshotProcessor(), late() — to the underlying ModelType. Discriminated-
5407
+ // union scoping keys on a member's literal `type` property, but real-world
5408
+ // members are rarely bare models (jbrowse config schemas, for instance, are
5409
+ // always optional(model) or optional(snapshotProcessor(model))). Without this
5410
+ // the scoping never engages and every failure prints every member's full
5411
+ // structure. Wrappers expose their child as `_subtype` (optional/refinement/
5412
+ // snapshotProcessor) or via `getSubType()` (late); bounded to avoid cycles.
5413
+ function resolveModelType(type) {
5414
+ let current = type;
5415
+ for (let depth = 0; current && depth < 20; depth++) {
5416
+ if (current instanceof ModelType) {
5417
+ return current;
5418
+ }
5419
+ const wrapper = current;
5420
+ current = wrapper._subtype ?? wrapper.getSubType?.(false);
5421
+ }
5422
+ return undefined;
5423
+ }
5394
5424
  /**
5395
5425
  * @internal
5396
5426
  * @hidden
@@ -5440,10 +5470,74 @@ class Union extends BaseType {
5440
5470
  return type.reconcile(current, newValue, parent, subpath);
5441
5471
  }
5442
5472
  noMatchMessage(value) {
5443
- const discriminator = isPlainObject(value) && typeof value.type === "string"
5444
- ? ` for snapshot with type "${value.type}"`
5445
- : "";
5446
- return `No matching type for union ${this.name}${discriminator}`;
5473
+ const base = `No matching type for union ${this.name}`;
5474
+ if (!isPlainObject(value)) {
5475
+ return base;
5476
+ }
5477
+ const discriminator = value.type;
5478
+ if (typeof discriminator !== "string") {
5479
+ return base;
5480
+ }
5481
+ const baseWithDiscriminator = `${base} for snapshot with type "${discriminator}"`;
5482
+ // If exactly one union member has a literal `type` property matching the
5483
+ // snapshot's discriminator, run its validate() so we can append the
5484
+ // property-level reasons it didn't match. This converts the bare prod-build
5485
+ // "no matching type" into something diagnosable (e.g. which field was the
5486
+ // wrong type, which required field was missing) without re-bloating the
5487
+ // message back to every-member's full describe() output.
5488
+ const candidate = this._findCandidateByTypeDiscriminator(discriminator);
5489
+ if (!candidate) {
5490
+ return baseWithDiscriminator;
5491
+ }
5492
+ const errors = candidate.validate(value, [
5493
+ { path: "", type: candidate }
5494
+ ]);
5495
+ if (errors.length === 0) {
5496
+ return baseWithDiscriminator;
5497
+ }
5498
+ return (`${baseWithDiscriminator}:\n ` +
5499
+ formatValidationErrorLines(errors).join("\n "));
5500
+ }
5501
+ _findCandidateByTypeDiscriminator(discriminator) {
5502
+ let found;
5503
+ for (const t of this._types) {
5504
+ const model = resolveModelType(t);
5505
+ if (!model) {
5506
+ continue;
5507
+ }
5508
+ const typeProp = model.properties.type;
5509
+ if (!typeProp ||
5510
+ !(typeProp.flags & TypeFlags.Literal) ||
5511
+ !typeProp.is(discriminator)) {
5512
+ continue;
5513
+ }
5514
+ if (found) {
5515
+ // Ambiguous (two members declare the same `type` literal). Fall back
5516
+ // to the short message rather than picking arbitrarily.
5517
+ return undefined;
5518
+ }
5519
+ // Return the original (possibly wrapped) member, not the unwrapped
5520
+ // model, so validate()/is() still apply optional defaults and any
5521
+ // snapshotProcessor pre-processing.
5522
+ found = t;
5523
+ }
5524
+ return found;
5525
+ }
5526
+ // True when every member resolves to a model carrying a literal `type`
5527
+ // discriminator — i.e. a fully discriminated union, where a snapshot's `type`
5528
+ // uniquely identifies the intended member and no untagged catch-all member
5529
+ // could also accept it. Cached: membership is fixed at construction.
5530
+ _allMembersDiscriminated;
5531
+ allMembersDiscriminated() {
5532
+ if (this._allMembersDiscriminated === undefined) {
5533
+ this._allMembersDiscriminated = this._types.every(t => {
5534
+ const model = resolveModelType(t);
5535
+ const typeProp = model &&
5536
+ model.properties.type;
5537
+ return !!typeProp && (typeProp.flags & TypeFlags.Literal) !== 0;
5538
+ });
5539
+ }
5540
+ return this._allMembersDiscriminated;
5447
5541
  }
5448
5542
  determineType(value, reconcileCurrentType) {
5449
5543
  // try the dispatcher, if defined
@@ -5532,11 +5626,14 @@ class Union extends BaseType {
5532
5626
  }
5533
5627
  snapshotLooksLikeType(value, type) {
5534
5628
  // for model types, check if snapshot has all the required property keys
5535
- // and that any literal-typed properties match exactly
5536
- if (type instanceof ModelType) {
5537
- const props = type.properties;
5629
+ // and that any literal-typed properties match exactly. Unwrap optional() /
5630
+ // snapshotProcessor() / refinement() / late() so wrapped members (e.g.
5631
+ // jbrowse config schemas, which are always optional(model)) still match.
5632
+ const model = resolveModelType(type);
5633
+ if (model) {
5634
+ const props = model.properties;
5538
5635
  // use cached propertyNames from ModelType instead of Object.keys()
5539
- for (const key of type.propertyNames) {
5636
+ for (const key of model.propertyNames) {
5540
5637
  const propType = props[key];
5541
5638
  const isOptional = propType.flags & TypeFlags.Optional;
5542
5639
  const propValue = value[key];
@@ -5563,6 +5660,28 @@ class Union extends BaseType {
5563
5660
  if (this._dispatcher) {
5564
5661
  return this._dispatcher(value).validate(value, context);
5565
5662
  }
5663
+ // For plain-object snapshots carrying a `type` discriminator, validate only
5664
+ // the single member whose literal `type` matches.
5665
+ // - Clean validation short-circuits to success only when it is sound to skip
5666
+ // the other members: an eager union (first match wins) or a fully
5667
+ // discriminated one (no untagged catch-all could also match). A non-eager
5668
+ // union with a catch-all must still fall through so ambiguity is counted.
5669
+ // - A failure is the definitive, scoped error only when every member is
5670
+ // discriminated; otherwise a catch-all could still accept the value, so
5671
+ // fall through to full validation.
5672
+ if (isPlainObject(value) && !isStateTreeNode(value)) {
5673
+ const discriminator = value.type;
5674
+ if (typeof discriminator === "string") {
5675
+ const candidate = this._findCandidateByTypeDiscriminator(discriminator);
5676
+ if (candidate) {
5677
+ const errors = candidate.validate(value, context);
5678
+ const cleanAndUnique = errors.length === 0 && this._eager;
5679
+ if (cleanAndUnique || this.allMembersDiscriminated()) {
5680
+ return errors;
5681
+ }
5682
+ }
5683
+ }
5684
+ }
5566
5685
  // for plain-object snapshots, prefer union members whose literal-typed
5567
5686
  // discriminator properties match the value (e.g. {type: "MsaView"})
5568
5687
  // so error output is scoped to the intended branch instead of every member