@wildwinter/toolkit 0.2.5 → 0.2.7

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,22 @@
1
1
  # @wildwinter/toolkit
2
2
 
3
+ ## 0.2.7
4
+
5
+ ### Added
6
+
7
+ - **Value help on a pick** (`./extensions`): a pick's `valueHelp` describes its fixed `words`, a sentence each; the
8
+ manifest check holds each key to one of them, since the project's lists and nodes are the project's to describe.
9
+
10
+ ## 0.2.6
11
+
12
+ ### Added
13
+
14
+ - **Views and reads in a manifest** (`./extensions`, tier 2): `views` declares the web components an extension's
15
+ installed package supplies, each with an `id`, a `name`, an `entry` script inside the package, the custom `element`
16
+ it defines, and optionally the `hosts` that offer it; `reads` names the other extensions whose data the views read.
17
+ The manifest check holds both to their shape. `viewsFor(manifest, host)` lists the views a host offers, and
18
+ `isViewEntry` keeps an entry inside its package.
19
+
3
20
  ## 0.2.5
4
21
 
5
22
  ### Added
@@ -571,6 +571,81 @@
571
571
  }
572
572
  ]
573
573
  },
574
+ {
575
+ "name": "a pick's value help describes its fixed words, never its lists or nodes",
576
+ "manifest": {
577
+ "schema": "wildwinter/extension@0",
578
+ "id": "probe",
579
+ "name": "Probe",
580
+ "version": "1.0.0",
581
+ "targets": {
582
+ "patter": {
583
+ "project": {
584
+ "fields": [
585
+ {
586
+ "name": "types",
587
+ "type": "list",
588
+ "of": {
589
+ "type": "text"
590
+ }
591
+ }
592
+ ]
593
+ },
594
+ "scene": {
595
+ "fields": [
596
+ {
597
+ "name": "type",
598
+ "type": "pick",
599
+ "from": [
600
+ {
601
+ "words": [
602
+ "ambient",
603
+ "cinematic"
604
+ ]
605
+ },
606
+ {
607
+ "list": "types"
608
+ },
609
+ {
610
+ "words": [
611
+ "bark"
612
+ ]
613
+ }
614
+ ],
615
+ "valueHelp": {
616
+ "ambient": "Voices from sources.",
617
+ "bark": "A one-off line.",
618
+ "duel": "One of the project's types: not the manifest's to describe."
619
+ }
620
+ },
621
+ {
622
+ "name": "to",
623
+ "type": "pick",
624
+ "from": [
625
+ {
626
+ "to": "cast"
627
+ }
628
+ ],
629
+ "valueHelp": {
630
+ "WREN": "A cast member."
631
+ }
632
+ }
633
+ ]
634
+ }
635
+ }
636
+ }
637
+ },
638
+ "problems": [
639
+ {
640
+ "path": "targets.patter.scene.fields[0].valueHelp.duel",
641
+ "code": "value-help"
642
+ },
643
+ {
644
+ "path": "targets.patter.scene.fields[1].valueHelp.WREN",
645
+ "code": "value-help"
646
+ }
647
+ ]
648
+ },
574
649
  {
575
650
  "name": "a reference names a kind its host can point at",
576
651
  "manifest": {
@@ -1438,6 +1513,238 @@
1438
1513
  "code": "default-misfit"
1439
1514
  }
1440
1515
  ]
1516
+ },
1517
+ {
1518
+ "name": "views and reads pass",
1519
+ "manifest": {
1520
+ "schema": "wildwinter/extension@0",
1521
+ "id": "probe",
1522
+ "name": "Probe",
1523
+ "version": "1.0.0",
1524
+ "targets": {
1525
+ "patter": {
1526
+ "line": {
1527
+ "fields": [
1528
+ {
1529
+ "name": "mood",
1530
+ "type": "text"
1531
+ }
1532
+ ]
1533
+ }
1534
+ }
1535
+ },
1536
+ "views": [
1537
+ {
1538
+ "id": "sheet",
1539
+ "name": "Sheet",
1540
+ "entry": "view/sheet.js",
1541
+ "element": "probe-sheet"
1542
+ },
1543
+ {
1544
+ "id": "cards",
1545
+ "name": "Cards",
1546
+ "entry": "cards.js",
1547
+ "element": "probe-cards",
1548
+ "hosts": [
1549
+ "patter"
1550
+ ]
1551
+ }
1552
+ ],
1553
+ "reads": [
1554
+ "patterstage"
1555
+ ]
1556
+ },
1557
+ "problems": []
1558
+ },
1559
+ {
1560
+ "name": "a view's shape is checked",
1561
+ "manifest": {
1562
+ "schema": "wildwinter/extension@0",
1563
+ "id": "probe",
1564
+ "name": "Probe",
1565
+ "version": "1.0.0",
1566
+ "targets": {
1567
+ "patter": {
1568
+ "line": {
1569
+ "fields": [
1570
+ {
1571
+ "name": "mood",
1572
+ "type": "text"
1573
+ }
1574
+ ]
1575
+ }
1576
+ }
1577
+ },
1578
+ "views": [
1579
+ {
1580
+ "id": "Sheet",
1581
+ "name": " ",
1582
+ "entry": "../sheet.js",
1583
+ "element": "sheet",
1584
+ "colour": "red"
1585
+ },
1586
+ {
1587
+ "id": "two",
1588
+ "name": "Two",
1589
+ "entry": "/abs/two.js",
1590
+ "element": "Probe-Two",
1591
+ "hosts": []
1592
+ },
1593
+ {
1594
+ "id": "three",
1595
+ "name": "Three",
1596
+ "entry": "view\\three.js",
1597
+ "element": "probe-three",
1598
+ "hosts": [
1599
+ "ink",
1600
+ "storylets"
1601
+ ]
1602
+ },
1603
+ {
1604
+ "id": "three",
1605
+ "name": "Again",
1606
+ "entry": "view/again.mjs",
1607
+ "element": "probe-three"
1608
+ }
1609
+ ]
1610
+ },
1611
+ "problems": [
1612
+ {
1613
+ "path": "views[0].colour",
1614
+ "code": "unknown-key",
1615
+ "severity": "warning"
1616
+ },
1617
+ {
1618
+ "path": "views[0].id",
1619
+ "code": "view-id"
1620
+ },
1621
+ {
1622
+ "path": "views[0].name",
1623
+ "code": "view-name"
1624
+ },
1625
+ {
1626
+ "path": "views[0].entry",
1627
+ "code": "view-entry"
1628
+ },
1629
+ {
1630
+ "path": "views[0].element",
1631
+ "code": "view-element"
1632
+ },
1633
+ {
1634
+ "path": "views[1].entry",
1635
+ "code": "view-entry"
1636
+ },
1637
+ {
1638
+ "path": "views[1].element",
1639
+ "code": "view-element"
1640
+ },
1641
+ {
1642
+ "path": "views[1].hosts",
1643
+ "code": "view-hosts"
1644
+ },
1645
+ {
1646
+ "path": "views[2].entry",
1647
+ "code": "view-entry"
1648
+ },
1649
+ {
1650
+ "path": "views[2].hosts[0]",
1651
+ "code": "unknown-host"
1652
+ },
1653
+ {
1654
+ "path": "views[2].hosts[1]",
1655
+ "code": "view-host-untargeted",
1656
+ "severity": "warning"
1657
+ },
1658
+ {
1659
+ "path": "views[3].id",
1660
+ "code": "view-duplicate"
1661
+ },
1662
+ {
1663
+ "path": "views[3].entry",
1664
+ "code": "view-entry"
1665
+ },
1666
+ {
1667
+ "path": "views[3].element",
1668
+ "code": "view-duplicate"
1669
+ }
1670
+ ]
1671
+ },
1672
+ {
1673
+ "name": "reads name other extensions, once each",
1674
+ "manifest": {
1675
+ "schema": "wildwinter/extension@0",
1676
+ "id": "probe",
1677
+ "name": "Probe",
1678
+ "version": "1.0.0",
1679
+ "targets": {
1680
+ "patter": {
1681
+ "line": {
1682
+ "fields": [
1683
+ {
1684
+ "name": "mood",
1685
+ "type": "text"
1686
+ }
1687
+ ]
1688
+ }
1689
+ }
1690
+ },
1691
+ "reads": [
1692
+ "patterstage",
1693
+ "probe",
1694
+ "patterstage",
1695
+ "Bad"
1696
+ ]
1697
+ },
1698
+ "problems": [
1699
+ {
1700
+ "path": "reads[1]",
1701
+ "code": "reads-self",
1702
+ "severity": "warning"
1703
+ },
1704
+ {
1705
+ "path": "reads[2]",
1706
+ "code": "reads-duplicate"
1707
+ },
1708
+ {
1709
+ "path": "reads[3]",
1710
+ "code": "reads-id"
1711
+ }
1712
+ ]
1713
+ },
1714
+ {
1715
+ "name": "views and reads must be lists",
1716
+ "manifest": {
1717
+ "schema": "wildwinter/extension@0",
1718
+ "id": "probe",
1719
+ "name": "Probe",
1720
+ "version": "1.0.0",
1721
+ "targets": {
1722
+ "patter": {
1723
+ "line": {
1724
+ "fields": [
1725
+ {
1726
+ "name": "mood",
1727
+ "type": "text"
1728
+ }
1729
+ ]
1730
+ }
1731
+ }
1732
+ },
1733
+ "views": {
1734
+ "id": "sheet"
1735
+ },
1736
+ "reads": "patterstage"
1737
+ },
1738
+ "problems": [
1739
+ {
1740
+ "path": "views",
1741
+ "code": "not-a-list"
1742
+ },
1743
+ {
1744
+ "path": "reads",
1745
+ "code": "not-a-list"
1746
+ }
1747
+ ]
1441
1748
  }
1442
1749
  ]
1443
1750
  }
@@ -370,7 +370,9 @@ var TYPE_KEYS = ["type", "values", "stages", "renamedStages", "to", "from", "fie
370
370
  var FIELD_KEYS = /* @__PURE__ */ new Set([...TYPE_KEYS, "name", "label", "help", "required", "group", "editorOnly"]);
371
371
  var CLAIM_KEYS = /* @__PURE__ */ new Set([...TYPE_KEYS, "setting", "label", "help", "required"]);
372
372
  var BARE_KEYS = new Set(TYPE_KEYS);
373
- var MANIFEST_KEYS = /* @__PURE__ */ new Set(["schema", "id", "name", "version", "docs", "settings", "targets"]);
373
+ var MANIFEST_KEYS = /* @__PURE__ */ new Set(["schema", "id", "name", "version", "docs", "settings", "targets", "views", "reads"]);
374
+ var VIEW_KEYS = /* @__PURE__ */ new Set(["id", "name", "entry", "element", "hosts"]);
375
+ var ELEMENT = /^[a-z][a-z0-9._]*-[a-z0-9._-]*$/;
374
376
  var err2 = (code, path, message) => ({ severity: "error", code, path, message });
375
377
  function parseManifest(text) {
376
378
  return parseSource(text);
@@ -392,6 +394,8 @@ function manifestProblems(manifest) {
392
394
  }
393
395
  if (m["docs"] !== void 0 && typeof m["docs"] !== "string") out.push(err2("docs", "docs", "docs is a link, as text"));
394
396
  const settings = settingsProblems(m["settings"], out);
397
+ viewsProblems(m["views"], m["targets"], out);
398
+ readsProblems(m["reads"], m["id"], out);
395
399
  const targets = m["targets"];
396
400
  if (!isObject(targets) || Object.keys(targets).length === 0) {
397
401
  out.push(err2("no-targets", "targets", "the manifest targets no host"));
@@ -420,6 +424,10 @@ function manifestProblems(manifest) {
420
424
  }
421
425
  return out;
422
426
  }
427
+ function pickWords(from) {
428
+ if (!Array.isArray(from)) return [];
429
+ return from.flatMap((s) => isObject(s) && Array.isArray(s["words"]) ? s["words"].filter((w) => typeof w === "string") : []);
430
+ }
423
431
  function pickSourcesSound(host, from, path, out) {
424
432
  if (!Array.isArray(from) || from.length === 0) {
425
433
  out.push(err2("pick-from", path, 'a pick lists its sources in "from": { "words": [...] }, { "to": a node kind }, or { "list": a project list }'));
@@ -490,6 +498,72 @@ function pickListProblems(kinds, hostPath, out) {
490
498
  target["fields"].forEach((f, i) => walk(f, atIndex(atKey(atKey(hostPath, kind), "fields"), i)));
491
499
  }
492
500
  }
501
+ function isViewEntry(entry) {
502
+ if (typeof entry !== "string" || !entry.endsWith(".js") || entry.includes("\\")) return false;
503
+ const parts = entry.split("/");
504
+ return parts.every((p) => p !== "" && p !== "." && p !== "..");
505
+ }
506
+ function viewsProblems(views, targets, out) {
507
+ if (views === void 0) return;
508
+ if (!Array.isArray(views)) {
509
+ out.push(err2("not-a-list", "views", "views are a list"));
510
+ return;
511
+ }
512
+ const ids = /* @__PURE__ */ new Set();
513
+ const elements = /* @__PURE__ */ new Set();
514
+ const targeted = isObject(targets) ? Object.keys(targets) : [];
515
+ views.forEach((v, i) => {
516
+ const at = atIndex("views", i);
517
+ if (!isObject(v)) {
518
+ out.push(err2("not-an-object", at, "a view is an object"));
519
+ return;
520
+ }
521
+ unknownKeys(v, VIEW_KEYS, at, out);
522
+ const id = v["id"];
523
+ if (typeof id !== "string" || !ID.test(id)) out.push(err2("view-id", atKey(at, "id"), "a view's id is lower case letters, digits and hyphens, starting with a letter"));
524
+ else if (ids.has(id)) out.push(err2("view-duplicate", atKey(at, "id"), `two views are called "${id}"`));
525
+ else ids.add(id);
526
+ if (typeof v["name"] !== "string" || v["name"].trim() === "") out.push(err2("view-name", atKey(at, "name"), "a view has a name, shown as its window's title"));
527
+ if (!isViewEntry(v["entry"])) {
528
+ out.push(err2("view-entry", atKey(at, "entry"), "a view's entry is a .js file in the extension's package, as a relative path such as view/sheet.js"));
529
+ }
530
+ const element = v["element"];
531
+ if (typeof element !== "string" || !ELEMENT.test(element)) {
532
+ out.push(err2("view-element", atKey(at, "element"), "a view's element is a custom element name: lower case, starting with a letter, with a hyphen, such as probe-view"));
533
+ } else if (elements.has(element)) out.push(err2("view-duplicate", atKey(at, "element"), `two views define <${element}>`));
534
+ else elements.add(element);
535
+ const hosts = v["hosts"];
536
+ if (hosts === void 0) return;
537
+ if (!Array.isArray(hosts) || hosts.length === 0) {
538
+ out.push(err2("view-hosts", atKey(at, "hosts"), "a view's hosts are a list of at least one host"));
539
+ return;
540
+ }
541
+ hosts.forEach((h, j) => {
542
+ const hp = atIndex(atKey(at, "hosts"), j);
543
+ if (typeof h !== "string" || !own(HOSTS, h)) out.push(err2("unknown-host", hp, `${JSON.stringify(h)} is not a host; a view opens in "patter" or "storylets"`));
544
+ else if (!targeted.includes(h)) out.push({ severity: "warning", code: "view-host-untargeted", path: hp, message: `the view opens in ${h}, but the manifest targets nothing there` });
545
+ });
546
+ });
547
+ }
548
+ function readsProblems(reads, self, out) {
549
+ if (reads === void 0) return;
550
+ if (!Array.isArray(reads)) {
551
+ out.push(err2("not-a-list", "reads", "reads is a list of extension ids"));
552
+ return;
553
+ }
554
+ const seen = /* @__PURE__ */ new Set();
555
+ reads.forEach((r, i) => {
556
+ const at = atIndex("reads", i);
557
+ if (typeof r !== "string" || !ID.test(r)) out.push(err2("reads-id", at, "an extension read is named by its id: lower case letters, digits and hyphens"));
558
+ else if (r === self) out.push({ severity: "warning", code: "reads-self", path: at, message: "an extension always reads its own data; this entry is ignored" });
559
+ else if (seen.has(r)) out.push(err2("reads-duplicate", at, `"${r}" is listed twice`));
560
+ else seen.add(r);
561
+ });
562
+ }
563
+ function viewsFor(manifest, host) {
564
+ const targeted = Object.keys(manifest.targets ?? {});
565
+ return (Array.isArray(manifest.views) ? manifest.views : []).filter((v) => isObject(v) && typeof v.id === "string" && ID.test(v.id) && typeof v.name === "string" && isViewEntry(v.entry) && typeof v.element === "string" && ELEMENT.test(v.element) && (v.hosts === void 0 ? targeted.includes(host) : Array.isArray(v.hosts) && v.hosts.includes(host)));
566
+ }
493
567
  function unknownKeys(obj, known, path, out) {
494
568
  for (const k of Object.keys(obj)) {
495
569
  if (!known.has(k)) {
@@ -701,12 +775,14 @@ function typeProblems(host, decl, path, named, out, claim = false) {
701
775
  out.push(err2("value-help", at("valueHelp"), "value help describes the values of an enum or flags field, or a quality's stages"));
702
776
  }
703
777
  } else if (decl.valueHelp !== void 0) {
704
- const vocabulary = decl.type === "quality" ? decl.stages : decl.type === "enum" || decl.type === "flags" ? decl.values : void 0;
778
+ const vocabulary = decl.type === "quality" ? decl.stages : decl.type === "enum" || decl.type === "flags" ? decl.values : decl.type === "pick" ? pickWords(decl.from) : void 0;
705
779
  if (vocabulary === void 0 || !isObject(decl.valueHelp)) {
706
- out.push(err2("value-help", at("valueHelp"), "value help describes the values of an enum or flags field, or a quality's stages"));
780
+ out.push(err2("value-help", at("valueHelp"), "value help describes the values of an enum or flags field, a quality's stages, or a pick's words"));
707
781
  } else {
708
782
  for (const k of Object.keys(decl.valueHelp)) {
709
- if (!vocabulary.includes(k)) out.push(err2("value-help", atKey(at("valueHelp"), k), `"${k}" is not one of its values`));
783
+ if (!vocabulary.includes(k)) {
784
+ out.push(err2("value-help", atKey(at("valueHelp"), k), decl.type === "pick" ? `"${k}" is not one of its words: value help describes a pick's fixed words, never the project's lists or nodes` : `"${k}" is not one of its values`));
785
+ }
710
786
  }
711
787
  }
712
788
  }
@@ -1146,6 +1222,7 @@ exports.effectiveClaim = effectiveClaim;
1146
1222
  exports.entryProblems = entryProblems;
1147
1223
  exports.extensionDataStrategy = extensionDataStrategy;
1148
1224
  exports.isTextList = isTextList;
1225
+ exports.isViewEntry = isViewEntry;
1149
1226
  exports.keyedBy = keyedBy;
1150
1227
  exports.manifestProblems = manifestProblems;
1151
1228
  exports.mapOf = mapOf;
@@ -1169,6 +1246,7 @@ exports.targetOf = targetOf;
1169
1246
  exports.unresolvedOf = unresolvedOf;
1170
1247
  exports.unresolvedProblems = unresolvedProblems;
1171
1248
  exports.valueProblems = valueProblems;
1249
+ exports.viewsFor = viewsFor;
1172
1250
  exports.withoutMark = withoutMark;
1173
1251
  //# sourceMappingURL=extensions.cjs.map
1174
1252
  //# sourceMappingURL=extensions.cjs.map