@istar-ts/core 0.13.3 → 0.13.5

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
@@ -35,7 +35,14 @@ writeFileSync('model.pistar', toPistar(model));
35
35
  - Files saved by the piStar web tool round-trip **byte-identically** through
36
36
  `toPistar(parsePistar(file))` when you do not edit the model.
37
37
  - Pass `{ saveDate: new Date() }` when saving an edited model (piStar updates `saveDate` on save).
38
- Omit it to preserve the model’s existing `saveDate` on a load/save cycle.
38
+ Omit it to preserve the model’s existing `saveDate` on a load/save cycle; a file that had none
39
+ keeps none until a save passes one.
40
+ - A model's metamodel and layout are kept beside it, in registries shared by every copy of
41
+ `@istar-ts/core` in the process (ESM and CommonJS builds alike), so a model parsed by one copy
42
+ is read correctly by another.
43
+ - A parsed file is written back with its own key order at every level (top level, each element,
44
+ link, hub entry and the diagram), so files other tools wrote stay byte-identical. `inheritSourceLayout`
45
+ carries that over to an edited model; the store does it on every edit.
39
46
 
40
47
  ```ts
41
48
  toPistar(editedModel, { saveDate: new Date() });
package/dist/index.cjs CHANGED
@@ -551,6 +551,26 @@ function elementBehavesLike(metamodel, kind, ancestor) {
551
551
  return false;
552
552
  }
553
553
  //#endregion
554
+ //#region src/shared.ts
555
+ /**
556
+ * Registries the library keeps beside models (their metamodel, their file's layout) are
557
+ * shared across every copy of `@istar-ts/core` loaded in a process. A bundle may load the
558
+ * package twice, once as ESM and once as CommonJS (an application parsing with the ESM build
559
+ * while a CommonJS dependency reads the model with the CJS build): a module-level WeakMap in
560
+ * each copy would make a model parsed by one copy unknown to the other, so `metamodelOf`
561
+ * would fall back to iStar 2.0 there. The registries live on `globalThis` under one symbol
562
+ * instead, keyed by name, and every copy finds the same ones.
563
+ */
564
+ const REGISTRIES = Symbol.for("@istar-ts/core/registries");
565
+ /** The WeakMap named `name`, the same object in every copy of the library in this process. */
566
+ function sharedWeakMap(name) {
567
+ const host = globalThis;
568
+ const registries = host[REGISTRIES] ??= /* @__PURE__ */ new Map();
569
+ let map = registries.get(name);
570
+ if (!map) registries.set(name, map = /* @__PURE__ */ new WeakMap());
571
+ return map;
572
+ }
573
+ //#endregion
554
574
  //#region src/model.ts
555
575
  /** Tool identifier written by piStar 2.1.0, whose file format this library emulates. */
556
576
  const PISTAR_TOOL = "pistar.2.1.0";
@@ -581,7 +601,7 @@ const DEFAULT_DIAGRAM = {
581
601
  * so a model's shape (and `JSON.stringify`, deep equality, snapshots) is the same as before
582
602
  * metamodels existed. Operations and the store carry it to every model they derive.
583
603
  */
584
- const metamodels = /* @__PURE__ */ new WeakMap();
604
+ const metamodels = sharedWeakMap("metamodels");
585
605
  /**
586
606
  * Associates `metamodel` with `model` and returns the same model, typed for it. Use when a
587
607
  * model was built without the library's operations (e.g. with an object spread), which loses
@@ -596,7 +616,7 @@ function withMetamodel(model, metamodel) {
596
616
  * For models whose file declares part of their metamodel (see `withFileMetamodel`): the
597
617
  * metamodel the host supplied, without the file's kinds.
598
618
  */
599
- const hostMetamodels = /* @__PURE__ */ new WeakMap();
619
+ const hostMetamodels = sharedWeakMap("hostMetamodels");
600
620
  /** @internal Records the host part of a model's metamodel (used by serialization). */
601
621
  function setHostMetamodel(model, metamodel) {
602
622
  if (metamodel) hostMetamodels.set(model, metamodel);
@@ -1235,7 +1255,13 @@ var PistarParseError = class extends Error {
1235
1255
  var PistarWriteError = class extends Error {
1236
1256
  name = "PistarWriteError";
1237
1257
  };
1238
- const layouts = /* @__PURE__ */ new WeakMap();
1258
+ /** The `objects` key of a hub's entry (`{ hub, starts, end }`) in a `SourceLayout`. */
1259
+ function hubEntryLayoutKey(hubId) {
1260
+ return `${hubId}\u0000entry`;
1261
+ }
1262
+ /** The `objects` key of the diagram in a `SourceLayout`. */
1263
+ const DIAGRAM_LAYOUT_KEY = "\0diagram";
1264
+ const layouts = sharedWeakMap("layouts");
1239
1265
  /** The key order recorded when `model` was parsed, if it came from `parsePistar`. */
1240
1266
  function getSourceLayout(model) {
1241
1267
  return layouts.get(model);
@@ -1342,6 +1368,7 @@ function parsePistar(input, options = {}) {
1342
1368
  usedDisplay.add(id);
1343
1369
  return entry;
1344
1370
  };
1371
+ const objects = /* @__PURE__ */ new Map();
1345
1372
  const readElement = (raw, where, known, extras) => {
1346
1373
  if (!isRecord(raw)) throw new PistarParseError(`${where}: expected an object`);
1347
1374
  const { id, type } = raw;
@@ -1361,6 +1388,7 @@ function parsePistar(input, options = {}) {
1361
1388
  ...optional("extra", extraOf(raw, known))
1362
1389
  };
1363
1390
  elements.set(id, element);
1391
+ objects.set(id, Object.keys(raw));
1364
1392
  return element;
1365
1393
  };
1366
1394
  asArray(json.actors, "actors").forEach((raw, i) => {
@@ -1405,12 +1433,14 @@ function parsePistar(input, options = {}) {
1405
1433
  ...optional("extra", extraOf(raw, LINK_KEYS))
1406
1434
  };
1407
1435
  links.set(id, link);
1436
+ objects.set(id, Object.keys(raw));
1408
1437
  return link;
1409
1438
  };
1410
1439
  const readHubEntry = (raw, where) => {
1411
1440
  const hub = readElement(raw.hub, `${where}.hub`, HUB_KEYS, {});
1412
1441
  const hubKind = metamodel.elements.get(hub.kind);
1413
1442
  if (hubKind?.role !== "hub") throw new PistarParseError(`${where}.hub (${hub.id}): ${hub.kind} is not a hub kind`);
1443
+ objects.set(hubEntryLayoutKey(hub.id), Object.keys(raw));
1414
1444
  const entryExtra = extraOf(raw, HUB_ENTRY_KEYS);
1415
1445
  if (entryExtra) elements.set(hub.id, {
1416
1446
  ...hub,
@@ -1448,6 +1478,7 @@ function parsePistar(input, options = {}) {
1448
1478
  ...isRecord(d.customProperties) ? { customProperties: d.customProperties } : {},
1449
1479
  ...optional("extra", extraOf(d, DIAGRAM_KEYS))
1450
1480
  };
1481
+ objects.set(DIAGRAM_LAYOUT_KEY, Object.keys(d));
1451
1482
  }
1452
1483
  const model = {
1453
1484
  elements,
@@ -1461,7 +1492,8 @@ function parsePistar(input, options = {}) {
1461
1492
  };
1462
1493
  layouts.set(model, {
1463
1494
  keys: Object.keys(json),
1464
- display: Object.keys(display)
1495
+ display: Object.keys(display),
1496
+ objects
1465
1497
  });
1466
1498
  setHostMetamodel(model, host);
1467
1499
  return withMetamodel(model, metamodel);
@@ -1534,6 +1566,19 @@ function pistarTypeOf(type, kind, metamodel) {
1534
1566
  if (type === void 0) throw new PistarWriteError(`kind "${kind}" is not part of ${metamodel.name}; pass its metamodel (toPistar(model, { metamodel }) or withMetamodel)`);
1535
1567
  return type;
1536
1568
  }
1569
+ /**
1570
+ * `value` with its keys in the order `keys` gives (those present), then the rest as built (piStar's
1571
+ * order): an object read from a file is written back with the file's key order, and keys the
1572
+ * file lacked follow.
1573
+ */
1574
+ function inSourceOrder(value, keys) {
1575
+ if (!keys) return value;
1576
+ const result = {};
1577
+ const record = value;
1578
+ for (const key of keys) if (key in record) result[key] = record[key];
1579
+ for (const key of Object.keys(record)) if (!(key in result)) result[key] = record[key];
1580
+ return result;
1581
+ }
1537
1582
  function elementJson(element, metamodel) {
1538
1583
  return {
1539
1584
  id: element.id,
@@ -1554,38 +1599,40 @@ function toPistarObject(input, options = {}) {
1554
1599
  const orphans = [];
1555
1600
  const dependencies = [];
1556
1601
  const children = /* @__PURE__ */ new Map();
1602
+ const layout = layouts.get(model);
1603
+ const sourceOrder = (id, value) => inSourceOrder(value, layout?.objects?.get(id));
1557
1604
  for (const element of model.elements.values()) {
1558
1605
  if (isHub(element)) continue;
1559
1606
  if (element.parent !== void 0 && !isActorKind(element.kind)) {
1560
1607
  let list = children.get(element.parent);
1561
1608
  if (!list) children.set(element.parent, list = []);
1562
- list.push({
1609
+ list.push(sourceOrder(element.id, {
1563
1610
  ...elementJson(element, metamodel),
1564
1611
  ...element.extra
1565
- });
1612
+ }));
1566
1613
  }
1567
1614
  }
1568
1615
  for (const element of model.elements.values()) {
1569
1616
  if (isHub(element)) continue;
1570
- if (isActorKind(element.kind)) actors.push({
1617
+ if (isActorKind(element.kind)) actors.push(sourceOrder(element.id, {
1571
1618
  ...elementJson(element, metamodel),
1572
1619
  nodes: children.get(element.id) ?? [],
1573
1620
  ...element.extra
1574
- });
1621
+ }));
1575
1622
  else if (element.isDependum) {
1576
1623
  const { inbound, outbound } = dependencyLinksOf(model, element.id);
1577
- dependencies.push({
1624
+ dependencies.push(sourceOrder(element.id, {
1578
1625
  ...elementJson(element, metamodel),
1579
1626
  source: inbound?.source ?? element.dependency?.source ?? "",
1580
1627
  target: outbound?.target ?? element.dependency?.target ?? "",
1581
1628
  ...element.extra
1582
- });
1583
- } else if (element.parent === void 0 || !model.elements.has(element.parent)) orphans.push({
1629
+ }));
1630
+ } else if (element.parent === void 0 || !model.elements.has(element.parent)) orphans.push(sourceOrder(element.id, {
1584
1631
  ...elementJson(element, metamodel),
1585
1632
  ...element.extra
1586
- });
1633
+ }));
1587
1634
  }
1588
- const linkJson = (link) => ({
1635
+ const linkJson = (link) => sourceOrder(link.id, {
1589
1636
  id: link.id,
1590
1637
  type: pistarTypeOf(metamodel.links.get(link.kind)?.pistarType, link.kind, metamodel),
1591
1638
  source: link.source,
@@ -1617,19 +1664,19 @@ function toPistarObject(input, options = {}) {
1617
1664
  const hubEntry = (hub) => {
1618
1665
  const parts = hubs.get(hub.id) ?? { legs: [] };
1619
1666
  const { [HUB_ENTRY_KEY]: entryExtra, ...hubExtra } = hub.extra ?? {};
1620
- return {
1621
- hub: {
1667
+ return sourceOrder(hubEntryLayoutKey(hub.id), {
1668
+ hub: sourceOrder(hub.id, {
1622
1669
  id: hub.id,
1623
1670
  type: pistarTypeOf(metamodel.elements.get(hub.kind)?.pistarType, hub.kind, metamodel),
1624
1671
  x: hub.x,
1625
1672
  y: hub.y,
1626
1673
  ...hub.customProperties ? { customProperties: { ...hub.customProperties } } : {},
1627
1674
  ...hubExtra
1628
- },
1675
+ }),
1629
1676
  starts: parts.legs.map(linkJson),
1630
1677
  ...parts.end ? { end: linkJson(parts.end) } : {},
1631
1678
  ...isRecord(entryExtra) ? entryExtra : {}
1632
- };
1679
+ });
1633
1680
  };
1634
1681
  const links = [];
1635
1682
  const written = /* @__PURE__ */ new Set();
@@ -1643,7 +1690,6 @@ function toPistarObject(input, options = {}) {
1643
1690
  }
1644
1691
  }
1645
1692
  for (const element of model.elements.values()) if (isHub(element) && !written.has(element.id)) links.push(hubEntry(element));
1646
- const layout = layouts.get(model);
1647
1693
  const display = {};
1648
1694
  const displayOf = (id) => {
1649
1695
  const owner = model.elements.get(id) ?? model.links.get(id);
@@ -1658,15 +1704,16 @@ function toPistarObject(input, options = {}) {
1658
1704
  for (const [id, entry] of Object.entries(model.extraDisplay ?? {})) if (!(id in display)) display[id] = entry;
1659
1705
  let saveDate;
1660
1706
  if (options.saveDate instanceof Date) saveDate = formatDate(options.saveDate);
1661
- else saveDate = options.saveDate ?? model.saveDate ?? formatDate(/* @__PURE__ */ new Date());
1707
+ else if (options.saveDate !== void 0 || model.saveDate !== void 0) saveDate = options.saveDate ?? model.saveDate;
1708
+ else if (!layout || layout.keys.includes("saveDate")) saveDate = formatDate(/* @__PURE__ */ new Date());
1662
1709
  const d = model.diagram;
1663
- const diagram = d && {
1710
+ const diagram = d && sourceOrder("\0diagram", {
1664
1711
  ...d.width !== void 0 ? { width: d.width } : {},
1665
1712
  ...d.height !== void 0 ? { height: d.height } : {},
1666
1713
  ...d.name !== void 0 ? { name: d.name } : {},
1667
1714
  ...d.customProperties ? { customProperties: { ...d.customProperties } } : {},
1668
1715
  ...d.extra
1669
- };
1716
+ });
1670
1717
  const values = {
1671
1718
  ...model.extra,
1672
1719
  actors,
@@ -2851,6 +2898,7 @@ exports.DEFAULT_DIAGRAM = DEFAULT_DIAGRAM;
2851
2898
  exports.DEFAULT_ELEMENT_SIZE = DEFAULT_ELEMENT_SIZE;
2852
2899
  exports.DEPENDENCY_LINK_KINDS = DEPENDENCY_LINK_KINDS;
2853
2900
  exports.DIAGNOSTIC_SEVERITIES = DIAGNOSTIC_SEVERITIES;
2901
+ exports.DIAGRAM_LAYOUT_KEY = DIAGRAM_LAYOUT_KEY;
2854
2902
  exports.DSM3_FILE_KEYS = DSM3_FILE_KEYS;
2855
2903
  exports.DSM3_TOOL = DSM3_TOOL;
2856
2904
  exports.ELEMENT_KINDS = ELEMENT_KINDS;
@@ -2909,6 +2957,7 @@ exports.fromNodeIdDiagnostics = fromNodeIdDiagnostics;
2909
2957
  exports.getSourceLayout = getSourceLayout;
2910
2958
  exports.groupDiagnostics = groupDiagnostics;
2911
2959
  exports.hostMetamodelOf = hostMetamodelOf;
2960
+ exports.hubEntryLayoutKey = hubEntryLayoutKey;
2912
2961
  exports.hubLinkOf = hubLinkOf;
2913
2962
  exports.hubOfLink = hubOfLink;
2914
2963
  exports.inheritMetamodel = inheritMetamodel;