@loadmap/cli 0.2.0 → 0.3.1
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 +4 -1
- package/dist/bin.js +1 -1
- package/dist/index.d.ts +15 -2
- package/dist/index.js +1 -1
- package/dist/{src-IlCf12IX.js → src-CCAswkjM.js} +359 -25
- package/dist/viewer/assets/index-DERN8Ima.js +179 -0
- package/dist/viewer/index.html +1 -1
- package/package.json +4 -4
- package/dist/viewer/assets/index-B22DgP4J.js +0 -154
|
@@ -38,7 +38,7 @@ function boundOf(c, bound) {
|
|
|
38
38
|
}
|
|
39
39
|
// ../core/src/format.ts
|
|
40
40
|
/** A number for a reader: 1,234 / 12.3 / 0.4. */
|
|
41
|
-
function fmt(n) {
|
|
41
|
+
function fmt$1(n) {
|
|
42
42
|
if (!Number.isFinite(n)) return n > 0 ? "∞" : "—";
|
|
43
43
|
if (Math.abs(n) >= 1e3) return Math.round(n).toLocaleString("en-US");
|
|
44
44
|
return (Math.round(n * 10) / 10).toString();
|
|
@@ -55,7 +55,7 @@ function compact(n) {
|
|
|
55
55
|
/** A load multiple, rounded down so it never promises more than the model: 2.6× / 53×. */
|
|
56
56
|
function multiple(u, searchMax = 2e3) {
|
|
57
57
|
if (u == null) return "—";
|
|
58
|
-
if (!Number.isFinite(u)) return `beyond ${fmt(searchMax)}×`;
|
|
58
|
+
if (!Number.isFinite(u)) return `beyond ${fmt$1(searchMax)}×`;
|
|
59
59
|
return u >= 10 ? `${Math.floor(u)}×` : `${Math.floor(u * 10) / 10}×`;
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
@@ -1041,7 +1041,7 @@ function sureness(c) {
|
|
|
1041
1041
|
return ` (capacity ${c.provenance}${c.within ? ` ±${pct(c.within)}` : ""})`;
|
|
1042
1042
|
}
|
|
1043
1043
|
function verdict(load, live, first, range, th, searchMax, when) {
|
|
1044
|
-
const at = `At ${fmt(load)}×`;
|
|
1044
|
+
const at = `At ${fmt$1(load)}×`;
|
|
1045
1045
|
const target = `${pct(th.target)} target`;
|
|
1046
1046
|
if (!first) return {
|
|
1047
1047
|
tier: "ok",
|
|
@@ -1157,7 +1157,7 @@ function check$1(model, expectations = model.expectations) {
|
|
|
1157
1157
|
});
|
|
1158
1158
|
}
|
|
1159
1159
|
const failed = claims.filter((x) => !x.ok);
|
|
1160
|
-
const message = failed.length ? failed.map((x) => `expected at least ${fmt(x.min)}×, but ${x.text}`).join("; ") : claims.map((x) => x.text).join("; ");
|
|
1160
|
+
const message = failed.length ? failed.map((x) => `expected at least ${fmt$1(x.min)}×, but ${x.text}`).join("; ") : claims.map((x) => x.text).join("; ");
|
|
1161
1161
|
return {
|
|
1162
1162
|
expectation: e,
|
|
1163
1163
|
ok: !failed.length,
|
|
@@ -1361,6 +1361,26 @@ function distance(a, b) {
|
|
|
1361
1361
|
function own(o, k) {
|
|
1362
1362
|
return Object.hasOwn(o, k) ? o[k] : void 0;
|
|
1363
1363
|
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Which argument of a statement fills which slot: by kind, in order, optional slots skipped. `bad` are the ones that
|
|
1366
|
+
* fit no slot, `missing` the first required slot left empty.
|
|
1367
|
+
*/
|
|
1368
|
+
function matchArgs(slots, args) {
|
|
1369
|
+
const open = [...slots];
|
|
1370
|
+
const filled = /* @__PURE__ */ new Map();
|
|
1371
|
+
const bad = [];
|
|
1372
|
+
for (const a of args) {
|
|
1373
|
+
const kind = a.t === "id" ? "id" : a.t === "str" || a.t === "tpl" ? "text" : a.t === "num" ? "number" : null;
|
|
1374
|
+
const i = open.findIndex((s) => s.kind === kind);
|
|
1375
|
+
if (kind == null || i < 0 || open.slice(0, i).some((s) => !s.optional)) bad.push(a);
|
|
1376
|
+
else filled.set(open.splice(0, i + 1).at(-1), a);
|
|
1377
|
+
}
|
|
1378
|
+
return {
|
|
1379
|
+
filled,
|
|
1380
|
+
bad,
|
|
1381
|
+
missing: open.find((s) => !s.optional)
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1364
1384
|
const STAGE_LABEL = [{
|
|
1365
1385
|
name: "id",
|
|
1366
1386
|
kind: "id",
|
|
@@ -1393,11 +1413,13 @@ const COMPONENT_PROPS = {
|
|
|
1393
1413
|
doc: "\"are past capacity\" instead of \"is\"."
|
|
1394
1414
|
},
|
|
1395
1415
|
demand: {
|
|
1416
|
+
inspector: "main",
|
|
1396
1417
|
value: "number",
|
|
1397
1418
|
scope: "ctx",
|
|
1398
1419
|
doc: "What the component must handle, in its capacity's unit. Without one: the units per second reaching its stages."
|
|
1399
1420
|
},
|
|
1400
1421
|
capacity: {
|
|
1422
|
+
inspector: "main",
|
|
1401
1423
|
value: "capacity",
|
|
1402
1424
|
scope: "ctx",
|
|
1403
1425
|
doc: "What it can handle: measured(value, unit, source) or estimated(value, unit, source), `value ± 30%` saying how sure it is; configured(value, unit, source) for a limit someone set."
|
|
@@ -1461,6 +1483,7 @@ const SCHEMA = {
|
|
|
1461
1483
|
args: [],
|
|
1462
1484
|
props: {
|
|
1463
1485
|
title: {
|
|
1486
|
+
inspector: "main",
|
|
1464
1487
|
value: "string",
|
|
1465
1488
|
scope: "const",
|
|
1466
1489
|
doc: "The heading above the diagram."
|
|
@@ -1519,6 +1542,7 @@ const SCHEMA = {
|
|
|
1519
1542
|
children: []
|
|
1520
1543
|
},
|
|
1521
1544
|
load: {
|
|
1545
|
+
inspector: "model",
|
|
1522
1546
|
doc: "The load, as a multiple of today. It drives the limits.",
|
|
1523
1547
|
example: "load \"Users, × today\" { max: 100; default: 10 }",
|
|
1524
1548
|
args: [{
|
|
@@ -1533,6 +1557,7 @@ const SCHEMA = {
|
|
|
1533
1557
|
doc: "The lowest multiple. Default 1."
|
|
1534
1558
|
},
|
|
1535
1559
|
max: {
|
|
1560
|
+
inspector: "main",
|
|
1536
1561
|
value: "number",
|
|
1537
1562
|
scope: "const",
|
|
1538
1563
|
doc: "The highest multiple on the slider.",
|
|
@@ -1544,6 +1569,7 @@ const SCHEMA = {
|
|
|
1544
1569
|
doc: "The slider step. Default 1."
|
|
1545
1570
|
},
|
|
1546
1571
|
default: {
|
|
1572
|
+
inspector: "main",
|
|
1547
1573
|
value: "number",
|
|
1548
1574
|
scope: "const",
|
|
1549
1575
|
doc: "Where the slider starts. Default the minimum."
|
|
@@ -1552,6 +1578,7 @@ const SCHEMA = {
|
|
|
1552
1578
|
children: []
|
|
1553
1579
|
},
|
|
1554
1580
|
param: {
|
|
1581
|
+
inspector: "model",
|
|
1555
1582
|
doc: "A control: options make a select, min and max a slider, a true or false default a switch.",
|
|
1556
1583
|
example: "param pods \"Worker pods\" { options: [2, 3, 4, 6]; default: 3 }",
|
|
1557
1584
|
args: [{
|
|
@@ -1564,26 +1591,31 @@ const SCHEMA = {
|
|
|
1564
1591
|
}],
|
|
1565
1592
|
props: {
|
|
1566
1593
|
options: {
|
|
1594
|
+
inspector: "main",
|
|
1567
1595
|
value: "options",
|
|
1568
1596
|
scope: "const",
|
|
1569
1597
|
doc: "The choices of a select: [2, 3, 4] or [\"a\", \"b\"]."
|
|
1570
1598
|
},
|
|
1571
1599
|
min: {
|
|
1600
|
+
inspector: "main",
|
|
1572
1601
|
value: "number",
|
|
1573
1602
|
scope: "const",
|
|
1574
1603
|
doc: "The lowest value of a slider."
|
|
1575
1604
|
},
|
|
1576
1605
|
max: {
|
|
1606
|
+
inspector: "main",
|
|
1577
1607
|
value: "number",
|
|
1578
1608
|
scope: "const",
|
|
1579
1609
|
doc: "The highest value of a slider."
|
|
1580
1610
|
},
|
|
1581
1611
|
step: {
|
|
1612
|
+
inspector: "main",
|
|
1582
1613
|
value: "number",
|
|
1583
1614
|
scope: "const",
|
|
1584
1615
|
doc: "The step of a slider. Default 1."
|
|
1585
1616
|
},
|
|
1586
1617
|
default: {
|
|
1618
|
+
inspector: "main",
|
|
1587
1619
|
value: "value",
|
|
1588
1620
|
scope: "const",
|
|
1589
1621
|
doc: "The starting value.",
|
|
@@ -1649,11 +1681,13 @@ const SCHEMA = {
|
|
|
1649
1681
|
}
|
|
1650
1682
|
},
|
|
1651
1683
|
growth: {
|
|
1684
|
+
inspector: "model",
|
|
1652
1685
|
doc: "How fast the load grows, compounded from asOf, where it is 1×: load multiples become dates.",
|
|
1653
1686
|
example: "growth { rate: 0.06; per: month; asOf: \"2026-11-01\" }",
|
|
1654
1687
|
args: [],
|
|
1655
1688
|
props: {
|
|
1656
1689
|
rate: {
|
|
1690
|
+
inspector: "main",
|
|
1657
1691
|
value: "number",
|
|
1658
1692
|
scope: "const",
|
|
1659
1693
|
required: true,
|
|
@@ -1742,6 +1776,7 @@ const SCHEMA = {
|
|
|
1742
1776
|
}
|
|
1743
1777
|
},
|
|
1744
1778
|
let: {
|
|
1779
|
+
inspector: "model",
|
|
1745
1780
|
doc: "A named formula other formulas can use by its name.",
|
|
1746
1781
|
example: "let perPosition = watchOnly ? 0.3 : 1.2",
|
|
1747
1782
|
args: [{
|
|
@@ -1753,6 +1788,7 @@ const SCHEMA = {
|
|
|
1753
1788
|
children: []
|
|
1754
1789
|
},
|
|
1755
1790
|
fit: {
|
|
1791
|
+
inspector: "model",
|
|
1756
1792
|
doc: "cost = a + b × x, fitted from measured points. Call it like a function: workerCores(rate).",
|
|
1757
1793
|
example: "fit workerCores \"Worker CPU per request\" { source: \"load test\"; points rate cores { 200 0.26 } }",
|
|
1758
1794
|
args: [{
|
|
@@ -1793,6 +1829,7 @@ const SCHEMA = {
|
|
|
1793
1829
|
rows: true
|
|
1794
1830
|
},
|
|
1795
1831
|
flow: {
|
|
1832
|
+
inspector: "model",
|
|
1796
1833
|
doc: "Something that moves through the system, with its rate and its route.",
|
|
1797
1834
|
example: "flow orders \"orders\" { unit: \"order\"; rate: 40 * load; route: gateway -> topic -> workers }",
|
|
1798
1835
|
args: [{
|
|
@@ -1810,6 +1847,7 @@ const SCHEMA = {
|
|
|
1810
1847
|
doc: "One unit of the flow, singular: \"order\"."
|
|
1811
1848
|
},
|
|
1812
1849
|
rate: {
|
|
1850
|
+
inspector: "main",
|
|
1813
1851
|
value: "number",
|
|
1814
1852
|
scope: "ctx",
|
|
1815
1853
|
doc: "Units per second.",
|
|
@@ -1891,12 +1929,14 @@ const SCHEMA = {
|
|
|
1891
1929
|
args: [],
|
|
1892
1930
|
props: {
|
|
1893
1931
|
times: {
|
|
1932
|
+
inspector: "main",
|
|
1894
1933
|
value: "number",
|
|
1895
1934
|
scope: "values",
|
|
1896
1935
|
doc: "Retries after the first attempt: a whole number. Reads only the parameters.",
|
|
1897
1936
|
required: true
|
|
1898
1937
|
},
|
|
1899
1938
|
budget: {
|
|
1939
|
+
inspector: "main",
|
|
1900
1940
|
value: "number",
|
|
1901
1941
|
scope: "values",
|
|
1902
1942
|
doc: "At most this share of the flow is added as retries: 0.1 adds at most 10 %. Without one, every retry is sent."
|
|
@@ -1955,6 +1995,7 @@ const SCHEMA = {
|
|
|
1955
1995
|
props: {
|
|
1956
1996
|
sub: SUB,
|
|
1957
1997
|
members: {
|
|
1998
|
+
inspector: "main",
|
|
1958
1999
|
value: "number",
|
|
1959
2000
|
scope: "ctx",
|
|
1960
2001
|
doc: "How many members.",
|
|
@@ -1997,6 +2038,7 @@ const SCHEMA = {
|
|
|
1997
2038
|
args: STAGE_LABEL,
|
|
1998
2039
|
props: {
|
|
1999
2040
|
hit: {
|
|
2041
|
+
inspector: "main",
|
|
2000
2042
|
value: "number",
|
|
2001
2043
|
scope: "ctx",
|
|
2002
2044
|
doc: "The share answered here, 0 to 1.",
|
|
@@ -2055,11 +2097,13 @@ const SCHEMA = {
|
|
|
2055
2097
|
args: STAGE_LABEL,
|
|
2056
2098
|
props: {
|
|
2057
2099
|
pods: {
|
|
2100
|
+
inspector: "main",
|
|
2058
2101
|
value: "number",
|
|
2059
2102
|
scope: "ctx",
|
|
2060
2103
|
doc: "How many pods; an autoscaled pool has autoscale instead."
|
|
2061
2104
|
},
|
|
2062
2105
|
perPod: {
|
|
2106
|
+
inspector: "main",
|
|
2063
2107
|
value: "number",
|
|
2064
2108
|
scope: "ctx",
|
|
2065
2109
|
doc: "Workers per pod.",
|
|
@@ -2081,18 +2125,21 @@ const SCHEMA = {
|
|
|
2081
2125
|
args: [],
|
|
2082
2126
|
props: {
|
|
2083
2127
|
min: {
|
|
2128
|
+
inspector: "main",
|
|
2084
2129
|
value: "number",
|
|
2085
2130
|
scope: "ctx",
|
|
2086
2131
|
doc: "The fewest pods, at least 1.",
|
|
2087
2132
|
required: true
|
|
2088
2133
|
},
|
|
2089
2134
|
max: {
|
|
2135
|
+
inspector: "main",
|
|
2090
2136
|
value: "number",
|
|
2091
2137
|
scope: "ctx",
|
|
2092
2138
|
doc: "The most pods.",
|
|
2093
2139
|
required: true
|
|
2094
2140
|
},
|
|
2095
2141
|
target: {
|
|
2142
|
+
inspector: "main",
|
|
2096
2143
|
value: "number",
|
|
2097
2144
|
scope: "ctx",
|
|
2098
2145
|
doc: "The utilization each running pod is held at, above 0 and at most 1: 0.7 scales out above 70 %.",
|
|
@@ -2110,6 +2157,7 @@ const SCHEMA = {
|
|
|
2110
2157
|
edges: "branches"
|
|
2111
2158
|
},
|
|
2112
2159
|
thresholds: {
|
|
2160
|
+
inspector: "model",
|
|
2113
2161
|
doc: "The utilization of the target and of \"runs out\".",
|
|
2114
2162
|
example: "thresholds { target: 0.5; full: 1 }",
|
|
2115
2163
|
args: [],
|
|
@@ -2226,7 +2274,7 @@ const BUILTINS = {
|
|
|
2226
2274
|
fmt: {
|
|
2227
2275
|
args: ["number"],
|
|
2228
2276
|
result: "string",
|
|
2229
|
-
fn: fmt
|
|
2277
|
+
fn: fmt$1
|
|
2230
2278
|
},
|
|
2231
2279
|
compact: {
|
|
2232
2280
|
args: ["number"],
|
|
@@ -2638,8 +2686,11 @@ const PUNCT = [
|
|
|
2638
2686
|
/**
|
|
2639
2687
|
* Splits `source[from, to)` into tokens. Newlines end statements, except inside parentheses and brackets, so a long
|
|
2640
2688
|
* formula can wrap there. `#` starts a comment to the end of the line.
|
|
2689
|
+
*
|
|
2690
|
+
* With `trivia` (for the formatter, never the parser) every newline is a token, blank lines and wrapped formulas
|
|
2691
|
+
* included, and so is every comment.
|
|
2641
2692
|
*/
|
|
2642
|
-
function lex(source, report, from = 0, to = source.length) {
|
|
2693
|
+
function lex(source, report, from = 0, to = source.length, trivia = false) {
|
|
2643
2694
|
const out = [];
|
|
2644
2695
|
let depth = 0;
|
|
2645
2696
|
let i = from;
|
|
@@ -2647,7 +2698,7 @@ function lex(source, report, from = 0, to = source.length) {
|
|
|
2647
2698
|
while (i < to) {
|
|
2648
2699
|
const c = source[i];
|
|
2649
2700
|
if (c === "\n") {
|
|
2650
|
-
if (depth === 0 && out.at(-1)?.kind !== "newline") push({
|
|
2701
|
+
if (trivia || depth === 0 && out.at(-1)?.kind !== "newline") push({
|
|
2651
2702
|
kind: "newline",
|
|
2652
2703
|
text: "\n",
|
|
2653
2704
|
from: i,
|
|
@@ -2655,8 +2706,16 @@ function lex(source, report, from = 0, to = source.length) {
|
|
|
2655
2706
|
});
|
|
2656
2707
|
i++;
|
|
2657
2708
|
} else if (c === " " || c === " " || c === "\r") i++;
|
|
2658
|
-
else if (c === "#")
|
|
2659
|
-
|
|
2709
|
+
else if (c === "#") {
|
|
2710
|
+
const s = i;
|
|
2711
|
+
while (i < to && source[i] !== "\n") i++;
|
|
2712
|
+
if (trivia) push({
|
|
2713
|
+
kind: "comment",
|
|
2714
|
+
text: source.slice(s, i).trimEnd(),
|
|
2715
|
+
from: s,
|
|
2716
|
+
to: i
|
|
2717
|
+
});
|
|
2718
|
+
} else if (isIdStart(c)) {
|
|
2660
2719
|
const s = i;
|
|
2661
2720
|
while (i < to && isIdPart(source[i])) i++;
|
|
2662
2721
|
push({
|
|
@@ -2859,7 +2918,7 @@ const PREC = {
|
|
|
2859
2918
|
/** Thrown after a syntax error has been reported, to skip to the next statement. */
|
|
2860
2919
|
var Bail$1 = class {};
|
|
2861
2920
|
/** Parses a definition into statements. Never throws: errors go to the reporter, and parsing goes on at the next line. */
|
|
2862
|
-
function parse$
|
|
2921
|
+
function parse$2(source, report = new Reporter(source)) {
|
|
2863
2922
|
if (source.length > 2e5) {
|
|
2864
2923
|
report.error({
|
|
2865
2924
|
from: 0,
|
|
@@ -3343,7 +3402,7 @@ function compile(source, o = {}) {
|
|
|
3343
3402
|
let definition;
|
|
3344
3403
|
let expectations = [];
|
|
3345
3404
|
try {
|
|
3346
|
-
doc = parse$
|
|
3405
|
+
doc = parse$2(source, report).doc;
|
|
3347
3406
|
definition = c.run(doc);
|
|
3348
3407
|
if (definition) expectations = check$1(definition.model).map((result, i) => {
|
|
3349
3408
|
const at = c.expectSpans[i];
|
|
@@ -3748,16 +3807,9 @@ var Compiler = class {
|
|
|
3748
3807
|
return b;
|
|
3749
3808
|
}
|
|
3750
3809
|
args(b, d) {
|
|
3751
|
-
const
|
|
3752
|
-
for (const a of
|
|
3753
|
-
|
|
3754
|
-
const i = slots.findIndex((s) => s.kind === kind);
|
|
3755
|
-
if (kind == null || i < 0 || slots.slice(0, i).some((s) => !s.optional)) {
|
|
3756
|
-
this.report.error(a, `unexpected ${this.slice(a)} after "${b.keyword}"`, usage(b.keyword));
|
|
3757
|
-
continue;
|
|
3758
|
-
}
|
|
3759
|
-
const slot = slots[i];
|
|
3760
|
-
slots.splice(0, i + 1);
|
|
3810
|
+
const { filled, bad, missing } = matchArgs(b.spec.args, d.args);
|
|
3811
|
+
for (const a of bad) this.report.error(a, `unexpected ${this.slice(a)} after "${b.keyword}"`, usage(b.keyword));
|
|
3812
|
+
for (const [slot, a] of filled) {
|
|
3761
3813
|
if (a.t === "tpl" && !slot.dynamic && !a.parts.length) {
|
|
3762
3814
|
b.args[slot.name] = {
|
|
3763
3815
|
t: "str",
|
|
@@ -3775,7 +3827,6 @@ var Compiler = class {
|
|
|
3775
3827
|
};
|
|
3776
3828
|
else b.args[slot.name] = a;
|
|
3777
3829
|
}
|
|
3778
|
-
const missing = slots.find((s) => !s.optional);
|
|
3779
3830
|
if (missing) this.report.error(b.at, `"${b.keyword}" needs its ${missing.name}`, usage(b.keyword));
|
|
3780
3831
|
if (b.spec.init && !d.init) this.report.error(b.at, `"${b.keyword}" needs "= formula"`, usage(b.keyword));
|
|
3781
3832
|
if (!b.spec.init && d.init) this.report.error(d.init, `"${b.keyword}" takes no "="`);
|
|
@@ -4467,6 +4518,200 @@ function where(keyword) {
|
|
|
4467
4518
|
const parents = Object.entries(SCHEMA).filter(([, s]) => s.children.includes(keyword)).map(([k]) => k === "" ? "the top level" : k);
|
|
4468
4519
|
return parents.length ? `it goes in ${parents.join(" or ")}` : void 0;
|
|
4469
4520
|
}
|
|
4521
|
+
// ../dsl/src/ast.ts
|
|
4522
|
+
/** The formulas directly inside a formula, in order; a template's parts only when asked (they are its own text). */
|
|
4523
|
+
function childrenOf(e, templates = false) {
|
|
4524
|
+
switch (e.t) {
|
|
4525
|
+
case "unary": return [e.arg];
|
|
4526
|
+
case "pm": return [e.value, e.within];
|
|
4527
|
+
case "cond": return [
|
|
4528
|
+
e.test,
|
|
4529
|
+
e.then,
|
|
4530
|
+
e.else
|
|
4531
|
+
];
|
|
4532
|
+
case "bin": return [e.left, e.right];
|
|
4533
|
+
case "call": return e.args;
|
|
4534
|
+
case "array": return e.items;
|
|
4535
|
+
case "tpl": return templates ? e.parts : [];
|
|
4536
|
+
default: return [];
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
/**
|
|
4540
|
+
* Lays a definition out the one way: two spaces a block, a statement a line, at most one blank line, one space
|
|
4541
|
+
* around operators. It works on the tokens, so numbers, texts, templates and parentheses stay exactly as written and
|
|
4542
|
+
* a formula never changes. A block written on one line stays on one line while it fits. A text with a syntax error is
|
|
4543
|
+
* left alone.
|
|
4544
|
+
*/
|
|
4545
|
+
function format(source) {
|
|
4546
|
+
const { doc, report } = parse$2(source);
|
|
4547
|
+
if (report.failed) return {
|
|
4548
|
+
ok: false,
|
|
4549
|
+
text: source,
|
|
4550
|
+
diagnostics: report.list
|
|
4551
|
+
};
|
|
4552
|
+
const toks = lex(source, new Reporter(source), 0, source.length, true).filter((t) => t.kind !== "eof");
|
|
4553
|
+
const marks = marksOf(doc.body, toks);
|
|
4554
|
+
const lines = new Reporter(source);
|
|
4555
|
+
const match = /* @__PURE__ */ new Map();
|
|
4556
|
+
const open = [];
|
|
4557
|
+
for (let i = 0; i < toks.length; i++) if (isPunct(toks[i], "{")) open.push(i);
|
|
4558
|
+
else if (isPunct(toks[i], "}")) match.set(open.pop(), i);
|
|
4559
|
+
const empty = /* @__PURE__ */ new Set();
|
|
4560
|
+
const written = /* @__PURE__ */ new Set();
|
|
4561
|
+
for (const [i, j] of match) if (toks.slice(i + 1, j).every((t) => t.kind === "newline")) empty.add(i);
|
|
4562
|
+
else if (lines.position(toks[i].from).line !== lines.position(toks[j].from).line) written.add(i);
|
|
4563
|
+
const index = new Map(toks.map((t, i) => [t, i]));
|
|
4564
|
+
const split = /* @__PURE__ */ new Set();
|
|
4565
|
+
const oneLine = (t) => {
|
|
4566
|
+
const i = index.get(t);
|
|
4567
|
+
return isPunct(t, "{") && !empty.has(i) && !written.has(i) && !split.has(i);
|
|
4568
|
+
};
|
|
4569
|
+
for (;;) {
|
|
4570
|
+
const out = layout(toks, match, (i) => split.has(i) || written.has(i), empty);
|
|
4571
|
+
const text = out.map((l) => l.toks.length ? " ".repeat(l.depth) + join$1(l.toks, marks) : "");
|
|
4572
|
+
const long = out.find((l, n) => [...text[n]].length > 120 && l.toks.some(oneLine));
|
|
4573
|
+
if (!long) return {
|
|
4574
|
+
ok: true,
|
|
4575
|
+
text: text.length ? `${text.join("\n")}\n` : "",
|
|
4576
|
+
diagnostics: []
|
|
4577
|
+
};
|
|
4578
|
+
split.add(index.get(long.toks.find(oneLine)));
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
/** The tokens in lines with their depth. `multi` tells a block laid out a statement a line. */
|
|
4582
|
+
function layout(toks, match, multi, empty) {
|
|
4583
|
+
const out = [];
|
|
4584
|
+
let cur = [];
|
|
4585
|
+
let depth = 0;
|
|
4586
|
+
let lineDepth = 0;
|
|
4587
|
+
let newlines = 0;
|
|
4588
|
+
let start = true;
|
|
4589
|
+
const blocks = [true];
|
|
4590
|
+
const end = () => {
|
|
4591
|
+
if (cur.length) out.push({
|
|
4592
|
+
depth: lineDepth,
|
|
4593
|
+
toks: cur
|
|
4594
|
+
});
|
|
4595
|
+
cur = [];
|
|
4596
|
+
};
|
|
4597
|
+
const add = (t) => {
|
|
4598
|
+
if (!cur.length) {
|
|
4599
|
+
if (newlines > 1 && !start && !isPunct(t, "}")) out.push({
|
|
4600
|
+
depth: 0,
|
|
4601
|
+
toks: []
|
|
4602
|
+
});
|
|
4603
|
+
lineDepth = depth - (isPunct(t, "}") || isPunct(t, ")") || isPunct(t, "]") ? 1 : 0);
|
|
4604
|
+
}
|
|
4605
|
+
newlines = 0;
|
|
4606
|
+
start = false;
|
|
4607
|
+
cur.push(t);
|
|
4608
|
+
};
|
|
4609
|
+
for (let i = 0; i < toks.length; i++) {
|
|
4610
|
+
const t = toks[i];
|
|
4611
|
+
if (t.kind === "newline") {
|
|
4612
|
+
end();
|
|
4613
|
+
newlines++;
|
|
4614
|
+
} else if (isPunct(t, ";")) {
|
|
4615
|
+
if (blocks.at(-1)) {
|
|
4616
|
+
if (toks[i + 1]?.kind !== "comment") end();
|
|
4617
|
+
} else {
|
|
4618
|
+
const next = toks[i + 1];
|
|
4619
|
+
const last = cur.at(-1);
|
|
4620
|
+
if (!(!next || next.kind === "newline" || next.kind === "comment" || isPunct(next, "}") || !last || isPunct(last, "{") || isPunct(last, ";"))) add(t);
|
|
4621
|
+
}
|
|
4622
|
+
} else if (isPunct(t, "{")) {
|
|
4623
|
+
if (empty.has(i)) {
|
|
4624
|
+
add(t);
|
|
4625
|
+
i = match.get(i);
|
|
4626
|
+
add(toks[i]);
|
|
4627
|
+
continue;
|
|
4628
|
+
}
|
|
4629
|
+
const m = multi(i);
|
|
4630
|
+
add(t);
|
|
4631
|
+
depth++;
|
|
4632
|
+
blocks.push(m);
|
|
4633
|
+
if (m) {
|
|
4634
|
+
if (toks[i + 1]?.kind === "comment") add(toks[++i]);
|
|
4635
|
+
end();
|
|
4636
|
+
start = true;
|
|
4637
|
+
}
|
|
4638
|
+
} else if (isPunct(t, "}")) {
|
|
4639
|
+
if (blocks.pop()) end();
|
|
4640
|
+
add(t);
|
|
4641
|
+
depth--;
|
|
4642
|
+
} else if (isPunct(t, "(") || isPunct(t, "[")) {
|
|
4643
|
+
add(t);
|
|
4644
|
+
depth++;
|
|
4645
|
+
} else if (isPunct(t, ")") || isPunct(t, "]")) {
|
|
4646
|
+
add(t);
|
|
4647
|
+
depth--;
|
|
4648
|
+
} else add(t);
|
|
4649
|
+
}
|
|
4650
|
+
end();
|
|
4651
|
+
return out;
|
|
4652
|
+
}
|
|
4653
|
+
function marksOf(body, toks) {
|
|
4654
|
+
const m = {
|
|
4655
|
+
unary: /* @__PURE__ */ new Set(),
|
|
4656
|
+
percent: /* @__PURE__ */ new Set(),
|
|
4657
|
+
ternary: /* @__PURE__ */ new Set()
|
|
4658
|
+
};
|
|
4659
|
+
const after = (at) => {
|
|
4660
|
+
let lo = 0;
|
|
4661
|
+
let hi = toks.length;
|
|
4662
|
+
while (lo < hi) {
|
|
4663
|
+
const mid = lo + hi >> 1;
|
|
4664
|
+
if (toks[mid].from < at) lo = mid + 1;
|
|
4665
|
+
else hi = mid;
|
|
4666
|
+
}
|
|
4667
|
+
return lo;
|
|
4668
|
+
};
|
|
4669
|
+
const find = (from, text) => {
|
|
4670
|
+
for (let i = after(from); i < toks.length; i++) if (isPunct(toks[i], text)) return toks[i].from;
|
|
4671
|
+
return -1;
|
|
4672
|
+
};
|
|
4673
|
+
const expr = (e) => {
|
|
4674
|
+
if (!e) return;
|
|
4675
|
+
if (e.t === "unary") {
|
|
4676
|
+
let at = -1;
|
|
4677
|
+
for (let i = after(e.from); i < toks.length && toks[i].from < e.arg.from; i++) if (isPunct(toks[i], e.op)) at = toks[i].from;
|
|
4678
|
+
m.unary.add(at);
|
|
4679
|
+
} else if (e.t === "pm" && e.percent) m.percent.add(find(e.within.to, "%"));
|
|
4680
|
+
else if (e.t === "cond") m.ternary.add(find(e.then.to, ":"));
|
|
4681
|
+
for (const k of childrenOf(e)) expr(k);
|
|
4682
|
+
};
|
|
4683
|
+
const stmt = (s) => {
|
|
4684
|
+
if (s.kind === "row") s.values.forEach(expr);
|
|
4685
|
+
else if (s.kind === "prop") expr(s.value);
|
|
4686
|
+
else {
|
|
4687
|
+
if (s.kind === "decl") {
|
|
4688
|
+
s.args.forEach(expr);
|
|
4689
|
+
expr(s.init);
|
|
4690
|
+
} else expr(s.value);
|
|
4691
|
+
s.body?.forEach(stmt);
|
|
4692
|
+
}
|
|
4693
|
+
};
|
|
4694
|
+
body.forEach(stmt);
|
|
4695
|
+
return m;
|
|
4696
|
+
}
|
|
4697
|
+
function join$1(toks, m) {
|
|
4698
|
+
let s = toks[0].text;
|
|
4699
|
+
for (let i = 1; i < toks.length; i++) s += gap(toks[i - 1], toks[i], m) + toks[i].text;
|
|
4700
|
+
return s;
|
|
4701
|
+
}
|
|
4702
|
+
function gap(a, b, m) {
|
|
4703
|
+
const A = a.kind === "punct" ? a.text : "";
|
|
4704
|
+
const B = b.kind === "punct" ? b.text : "";
|
|
4705
|
+
if (A === "{" && B === "}") return "";
|
|
4706
|
+
if (A === "(" || A === "[" || A === ".") return "";
|
|
4707
|
+
if ((A === "-" || A === "+" || A === "!") && m.unary.has(a.from)) return "";
|
|
4708
|
+
if (B === ")" || B === "]" || B === "," || B === ";" || B === ".") return "";
|
|
4709
|
+
if (B === "%" && m.percent.has(b.from)) return "";
|
|
4710
|
+
if (B === ":" && !m.ternary.has(b.from)) return "";
|
|
4711
|
+
if (B === "(" && a.kind === "ident") return "";
|
|
4712
|
+
return " ";
|
|
4713
|
+
}
|
|
4714
|
+
const isPunct = (t, text) => t.kind === "punct" && t.text === text;
|
|
4470
4715
|
// src/check.ts
|
|
4471
4716
|
/**
|
|
4472
4717
|
* `loadmap check`: compiles each definition and tests its expect lines. Resolves to the exit code: 1 when a
|
|
@@ -4476,7 +4721,7 @@ async function check(argv, o = {}) {
|
|
|
4476
4721
|
const log = o.log ?? console.log;
|
|
4477
4722
|
let parsed;
|
|
4478
4723
|
try {
|
|
4479
|
-
parsed = parse(argv, o.env ?? process.env);
|
|
4724
|
+
parsed = parse$1(argv, o.env ?? process.env);
|
|
4480
4725
|
} catch (err) {
|
|
4481
4726
|
log(err.message);
|
|
4482
4727
|
return 2;
|
|
@@ -4505,7 +4750,7 @@ async function check(argv, o = {}) {
|
|
|
4505
4750
|
}
|
|
4506
4751
|
return failed ? 1 : 0;
|
|
4507
4752
|
}
|
|
4508
|
-
function parse(argv, env) {
|
|
4753
|
+
function parse$1(argv, env) {
|
|
4509
4754
|
const { values, positionals } = parseArgs({
|
|
4510
4755
|
args: argv,
|
|
4511
4756
|
allowPositionals: true,
|
|
@@ -4540,8 +4785,90 @@ function github(d, file, source) {
|
|
|
4540
4785
|
}
|
|
4541
4786
|
const data = (s) => s.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
4542
4787
|
const prop = (s) => data(s).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
4788
|
+
// src/fmt.ts
|
|
4789
|
+
/**
|
|
4790
|
+
* `loadmap fmt`: lays each definition out the one way (`format()` of @loadmap/dsl), in place. With `--check` it only
|
|
4791
|
+
* names the files that are not laid out so. `-` reads standard input and writes standard output. Resolves to 1 when a
|
|
4792
|
+
* file has a syntax error, cannot be read, or (with --check) needs formatting; 2 for a usage mistake.
|
|
4793
|
+
*/
|
|
4794
|
+
async function fmt(argv, o = {}) {
|
|
4795
|
+
const log = o.log ?? console.log;
|
|
4796
|
+
let parsed;
|
|
4797
|
+
try {
|
|
4798
|
+
parsed = parse(argv);
|
|
4799
|
+
} catch (err) {
|
|
4800
|
+
log(err.message);
|
|
4801
|
+
return 2;
|
|
4802
|
+
}
|
|
4803
|
+
if (parsed.stdin) {
|
|
4804
|
+
const source = await (o.read ?? readStdin)();
|
|
4805
|
+
const r = format(source);
|
|
4806
|
+
if (!r.ok) for (const d of r.diagnostics) log(formatDiagnostic(d, source, "<stdin>"));
|
|
4807
|
+
else (o.write ?? ((s) => process.stdout.write(s)))(r.text);
|
|
4808
|
+
return r.ok ? 0 : 1;
|
|
4809
|
+
}
|
|
4810
|
+
let failed = false;
|
|
4811
|
+
let changed = 0;
|
|
4812
|
+
let skipped = 0;
|
|
4813
|
+
for (const file of parsed.files) {
|
|
4814
|
+
let source;
|
|
4815
|
+
try {
|
|
4816
|
+
source = await readFile(file, "utf8");
|
|
4817
|
+
} catch (err) {
|
|
4818
|
+
log(`${file}: cannot read it (${err.code ?? err.message})`);
|
|
4819
|
+
failed = true;
|
|
4820
|
+
skipped++;
|
|
4821
|
+
continue;
|
|
4822
|
+
}
|
|
4823
|
+
const r = format(source);
|
|
4824
|
+
if (!r.ok) {
|
|
4825
|
+
for (const d of r.diagnostics) log(formatDiagnostic(d, source, file));
|
|
4826
|
+
log(`${file}: left as it is, it has a syntax error`);
|
|
4827
|
+
failed = true;
|
|
4828
|
+
skipped++;
|
|
4829
|
+
} else if (r.text !== source) {
|
|
4830
|
+
changed++;
|
|
4831
|
+
if (parsed.check) {
|
|
4832
|
+
log(`${file}: not laid out by loadmap fmt`);
|
|
4833
|
+
failed = true;
|
|
4834
|
+
} else {
|
|
4835
|
+
await writeFile(file, r.text);
|
|
4836
|
+
log(`${file}: formatted`);
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
const n = parsed.files.length;
|
|
4841
|
+
const files = (k) => `${k} ${k === 1 ? "file" : "files"}`;
|
|
4842
|
+
const left = skipped ? `; ${skipped} left out (unreadable or a syntax error)` : "";
|
|
4843
|
+
let summary = `${changed} of ${files(n)} formatted`;
|
|
4844
|
+
if (parsed.check && changed) summary = `${changed} of ${files(n)} need loadmap fmt`;
|
|
4845
|
+
else if (parsed.check) summary = skipped ? `${n - skipped} of ${files(n)} laid out already` : `${files(n)} laid out already`;
|
|
4846
|
+
log(summary + left);
|
|
4847
|
+
return failed ? 1 : 0;
|
|
4848
|
+
}
|
|
4849
|
+
function parse(argv) {
|
|
4850
|
+
const { values, positionals } = parseArgs({
|
|
4851
|
+
args: argv,
|
|
4852
|
+
allowPositionals: true,
|
|
4853
|
+
options: { check: { type: "boolean" } }
|
|
4854
|
+
});
|
|
4855
|
+
if (!positionals.length) throw new Error("loadmap fmt needs one or more .loadmap files, or - for standard input");
|
|
4856
|
+
const stdin = positionals.includes("-");
|
|
4857
|
+
if (stdin && (positionals.length > 1 || values.check)) throw new Error("loadmap fmt - reads standard input alone, without files or --check");
|
|
4858
|
+
return {
|
|
4859
|
+
files: positionals,
|
|
4860
|
+
check: !!values.check,
|
|
4861
|
+
stdin
|
|
4862
|
+
};
|
|
4863
|
+
}
|
|
4864
|
+
async function readStdin() {
|
|
4865
|
+
const chunks = [];
|
|
4866
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
4867
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
4868
|
+
}
|
|
4543
4869
|
// src/index.ts
|
|
4544
4870
|
const USAGE = `loadmap check <file.loadmap>… [--format text|github]
|
|
4871
|
+
loadmap fmt <file.loadmap>… [--check] | loadmap fmt -
|
|
4545
4872
|
loadmap record <url | file.loadmap> --out <file.gif|.webm|.mp4|.png|.svg> [options]
|
|
4546
4873
|
|
|
4547
4874
|
check compiles each definition and tests its expect lines on the pessimistic edge (every capacity at its low
|
|
@@ -4549,6 +4876,12 @@ bound). It exits 1 when a definition has an error or an expectation fails, 0 whe
|
|
|
4549
4876
|
|
|
4550
4877
|
--format <f> text, or github to add annotations on the lines (the default under GitHub Actions)
|
|
4551
4878
|
|
|
4879
|
+
fmt lays each definition out the one way, in place: two spaces a block, a statement a line, one space around
|
|
4880
|
+
operators; numbers, texts and formulas stay as written. A file with a syntax error is left alone (exit 1).
|
|
4881
|
+
|
|
4882
|
+
--check change nothing; exit 1 when a file is not laid out so (for CI)
|
|
4883
|
+
- read standard input, write standard output
|
|
4884
|
+
|
|
4552
4885
|
record opens the page in headless Chromium, finds the loadmap it exposes (expose() from @loadmap/export) and
|
|
4553
4886
|
records it on a fixed clock, so the same options give the same file. A .loadmap file is drawn by the viewer that
|
|
4554
4887
|
comes with the command.
|
|
@@ -4572,6 +4905,7 @@ comes with the command.
|
|
|
4572
4905
|
async function main(argv, log = console.log) {
|
|
4573
4906
|
const [command, ...rest] = argv;
|
|
4574
4907
|
if (command === "check") return check(rest, { log });
|
|
4908
|
+
if (command === "fmt") return fmt(rest, { log });
|
|
4575
4909
|
if (command !== "record") {
|
|
4576
4910
|
log(USAGE);
|
|
4577
4911
|
return command === "--help" || command === "-h" ? 0 : 2;
|
|
@@ -4775,4 +5109,4 @@ async function recordPage(a, format) {
|
|
|
4775
5109
|
await browser.close();
|
|
4776
5110
|
}
|
|
4777
5111
|
}
|
|
4778
|
-
export {
|
|
5112
|
+
export { fmt as a, record as i, main as n, check as o, parseRecord as r, github as s, USAGE as t };
|