@uipath/maestro-builder-sdk 6.4.0 → 6.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/dist/cli/commands/registry.js +68 -12
- package/dist/registry/archive/entry-policy.d.ts +36 -0
- package/dist/registry/archive/entry-policy.js +60 -0
- package/dist/registry/archive/extract.d.ts +31 -0
- package/dist/registry/archive/extract.js +92 -0
- package/dist/registry/archive/zip-reader.d.ts +38 -0
- package/dist/registry/archive/zip-reader.js +256 -0
- package/dist/registry/codecs/pyjson.d.ts +62 -0
- package/dist/registry/codecs/pyjson.js +208 -0
- package/dist/registry/compat/compare.d.ts +18 -0
- package/dist/registry/compat/compare.js +32 -0
- package/dist/registry/compat/paths.d.ts +4 -0
- package/dist/registry/compat/paths.js +17 -0
- package/dist/registry/compat/pystr.d.ts +50 -0
- package/dist/registry/compat/pystr.js +221 -0
- package/dist/registry/concurrency.d.ts +10 -0
- package/dist/registry/concurrency.js +34 -0
- package/dist/registry/curated-overlay.d.ts +36 -4
- package/dist/registry/curated-overlay.js +87 -14
- package/dist/registry/errors.d.ts +26 -0
- package/dist/registry/errors.js +38 -0
- package/dist/registry/extractor.d.ts +9 -10
- package/dist/registry/extractor.js +5 -15
- package/dist/registry/files.d.ts +26 -0
- package/dist/registry/files.js +160 -0
- package/dist/registry/generators/descriptors.d.ts +81 -0
- package/dist/registry/generators/descriptors.js +405 -0
- package/dist/registry/generators/fields.d.ts +46 -0
- package/dist/registry/generators/fields.js +111 -0
- package/dist/registry/generators/markdown/common.d.ts +21 -0
- package/dist/registry/generators/markdown/common.js +55 -0
- package/dist/registry/generators/markdown/index.d.ts +33 -0
- package/dist/registry/generators/markdown/index.js +37 -0
- package/dist/registry/generators/markdown/page.d.ts +8 -0
- package/dist/registry/generators/markdown/page.js +52 -0
- package/dist/registry/generators/markdown/pyvalue.d.ts +34 -0
- package/dist/registry/generators/markdown/pyvalue.js +106 -0
- package/dist/registry/generators/markdown/sections/choosing-object.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/choosing-object.js +52 -0
- package/dist/registry/generators/markdown/sections/example.d.ts +22 -0
- package/dist/registry/generators/markdown/sections/example.js +295 -0
- package/dist/registry/generators/markdown/sections/filtering.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/filtering.js +76 -0
- package/dist/registry/generators/markdown/sections/header.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/header.js +37 -0
- package/dist/registry/generators/markdown/sections/inputs.d.ts +30 -0
- package/dist/registry/generators/markdown/sections/inputs.js +151 -0
- package/dist/registry/generators/markdown/sections/lookups.d.ts +12 -0
- package/dist/registry/generators/markdown/sections/lookups.js +150 -0
- package/dist/registry/generators/markdown/sections/outputs.d.ts +3 -0
- package/dist/registry/generators/markdown/sections/outputs.js +9 -0
- package/dist/registry/generators/markdown/sections/resolving-fields.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/resolving-fields.js +139 -0
- package/dist/registry/generators/markdown/sections/schema-dynamic.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/schema-dynamic.js +86 -0
- package/dist/registry/generators/materialize.d.ts +30 -0
- package/dist/registry/generators/materialize.js +132 -0
- package/dist/registry/generators/md-symbols.d.ts +22 -0
- package/dist/registry/generators/md-symbols.js +58 -0
- package/dist/registry/generators/triggers.d.ts +15 -0
- package/dist/registry/generators/triggers.js +85 -0
- package/dist/registry/generators/type-registry.d.ts +50 -0
- package/dist/registry/generators/type-registry.js +154 -0
- package/dist/registry/generators/type-tree.d.ts +65 -0
- package/dist/registry/generators/type-tree.js +179 -0
- package/dist/registry/library-build/build.d.ts +57 -0
- package/dist/registry/library-build/build.js +286 -0
- package/dist/registry/library-build/canonical.d.ts +119 -0
- package/dist/registry/library-build/canonical.js +512 -0
- package/dist/registry/library-build/cli.d.ts +45 -0
- package/dist/registry/library-build/cli.js +182 -0
- package/dist/registry/library-build/context.d.ts +107 -0
- package/dist/registry/library-build/context.js +267 -0
- package/dist/registry/library-build/enrich.d.ts +36 -0
- package/dist/registry/library-build/enrich.js +158 -0
- package/dist/registry/library-build/events.d.ts +108 -0
- package/dist/registry/library-build/events.js +476 -0
- package/dist/registry/library-build/generic-expand.d.ts +77 -0
- package/dist/registry/library-build/generic-expand.js +363 -0
- package/dist/registry/library-build/plan.d.ts +71 -0
- package/dist/registry/library-build/plan.js +270 -0
- package/dist/registry/library-build/py.d.ts +45 -0
- package/dist/registry/library-build/py.js +167 -0
- package/dist/registry/library-build/schema-action.d.ts +38 -0
- package/dist/registry/library-build/schema-action.js +107 -0
- package/dist/registry/library-build/write.d.ts +6 -0
- package/dist/registry/library-build/write.js +26 -0
- package/dist/registry/library-reader.d.ts +20 -0
- package/dist/registry/library-reader.js +50 -0
- package/dist/registry/lookup-specs.d.ts +55 -0
- package/dist/registry/lookup-specs.js +140 -0
- package/dist/registry/materialize-cli.js +28 -14
- package/dist/registry/materializer.d.ts +25 -3
- package/dist/registry/materializer.js +45 -29
- package/dist/registry/model/library.d.ts +254 -0
- package/dist/registry/model/library.js +23 -0
- package/dist/registry/naming.d.ts +54 -0
- package/dist/registry/naming.js +142 -0
- package/dist/registry/orphans.d.ts +4 -0
- package/dist/registry/orphans.js +86 -0
- package/dist/registry/prepare/actions.d.ts +26 -0
- package/dist/registry/prepare/actions.js +94 -0
- package/dist/registry/prepare/connections.d.ts +181 -0
- package/dist/registry/prepare/connections.js +399 -0
- package/dist/registry/prepare/event.d.ts +96 -0
- package/dist/registry/prepare/event.js +268 -0
- package/dist/registry/prepare/handoff.d.ts +21 -0
- package/dist/registry/prepare/handoff.js +91 -0
- package/dist/registry/prepare/ledger.d.ts +104 -0
- package/dist/registry/prepare/ledger.js +216 -0
- package/dist/registry/prepare/messages.d.ts +190 -0
- package/dist/registry/prepare/messages.js +472 -0
- package/dist/registry/prepare/objects.d.ts +130 -0
- package/dist/registry/prepare/objects.js +318 -0
- package/dist/registry/prepare/overlay.d.ts +149 -0
- package/dist/registry/prepare/overlay.js +487 -0
- package/dist/registry/prepare/parents.d.ts +41 -0
- package/dist/registry/prepare/parents.js +122 -0
- package/dist/registry/prepare/pipeline.d.ts +91 -0
- package/dist/registry/prepare/pipeline.js +376 -0
- package/dist/registry/prepare/regenerate.d.ts +9 -0
- package/dist/registry/prepare/regenerate.js +50 -0
- package/dist/registry/prepare/resolve.d.ts +199 -0
- package/dist/registry/prepare/resolve.js +470 -0
- package/dist/registry/prepare/retry.d.ts +83 -0
- package/dist/registry/prepare/retry.js +109 -0
- package/dist/registry/prepare/source-import.d.ts +54 -0
- package/dist/registry/prepare/source-import.js +192 -0
- package/dist/registry/prepare/support.d.ts +41 -0
- package/dist/registry/prepare/support.js +178 -0
- package/dist/registry/prepare-cli.d.ts +16 -5
- package/dist/registry/prepare-cli.js +60 -53
- package/dist/registry/pull-cli.js +8 -7
- package/dist/registry/reporter.d.ts +26 -0
- package/dist/registry/reporter.js +41 -0
- package/dist/registry/uip/client.d.ts +99 -0
- package/dist/registry/uip/client.js +226 -0
- package/dist/registry/uip/commands.d.ts +40 -0
- package/dist/registry/uip/commands.js +80 -0
- package/dist/registry/uip/envelope.d.ts +37 -0
- package/dist/registry/uip/envelope.js +84 -0
- package/dist/registry/uip/response-cache.d.ts +28 -0
- package/dist/registry/uip/response-cache.js +57 -0
- package/lib/README.md +10 -8
- package/package.json +3 -3
- package/dist/generators/_connections.py +0 -331
- package/dist/generators/_events.py +0 -355
- package/dist/generators/_lookups.py +0 -114
- package/dist/generators/_naming.py +0 -185
- package/dist/generators/_objects.py +0 -299
- package/dist/generators/_resolve.py +0 -319
- package/dist/generators/convert_library_to_md.py +0 -1441
- package/dist/generators/extract_library.py +0 -125
- package/dist/generators/generate_connectors_ts.py +0 -936
- package/dist/generators/generate_library_json.py +0 -2527
- package/dist/generators/generate_triggers_ts.py +0 -110
- package/dist/generators/materialize_registry.py +0 -155
- package/dist/generators/prepare_connector.py +0 -1860
- package/dist/registry/python.d.ts +0 -26
- package/dist/registry/python.js +0 -46
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A JSON writer with the exact output of Python's `json.dumps`.
|
|
3
|
+
*
|
|
4
|
+
* The connector library the Azure pipeline publishes, the overlay `prepare`
|
|
5
|
+
* writes into a project, and the string literals inside every generated
|
|
6
|
+
* descriptor module were all produced by `json.dumps`. Its defaults differ from
|
|
7
|
+
* `JSON.stringify` in ways that change bytes, and the library's sha256 is
|
|
8
|
+
* published precisely so an unchanged refresh is a no-op:
|
|
9
|
+
*
|
|
10
|
+
* - `ensure_ascii=True` escapes every character outside `0x20..0x7e` as a
|
|
11
|
+
* lowercase `\uXXXX`, astral characters as two surrogate escapes. 1,651 of
|
|
12
|
+
* the 16,082 files in a current library carry such escapes.
|
|
13
|
+
* - with `indent`, the item separator is `,` + newline and the key separator
|
|
14
|
+
* is `: `; without, they are `, ` and `: ` — not `JSON.stringify`'s `,`/`:`.
|
|
15
|
+
* - `sort_keys` orders by code point, not by UTF-16 code unit.
|
|
16
|
+
* - a float prints as Python's `repr`: `1e-05`, `1e+16`, `2.5`, never `1e-5`.
|
|
17
|
+
*
|
|
18
|
+
* Everything the registry writes goes through this module, so the byte question
|
|
19
|
+
* is answered in one place and proven by one test: parse every library file,
|
|
20
|
+
* format it again, require the same bytes.
|
|
21
|
+
*/
|
|
22
|
+
export interface PyJsonOptions {
|
|
23
|
+
/** Spaces per level, as `json.dumps(indent=n)`. `undefined` means compact. */
|
|
24
|
+
indent?: number;
|
|
25
|
+
/** `json.dumps(sort_keys=True)`: keys ordered by code point at every level. */
|
|
26
|
+
sortKeys?: boolean;
|
|
27
|
+
/** `json.dumps(ensure_ascii=…)`; Python's default is `true`. */
|
|
28
|
+
ensureAscii?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* `json.dumps(separators=(item, key))`. Defaults follow Python: `(", ", ": ")`
|
|
31
|
+
* when compact, `(",", ": ")` when indented.
|
|
32
|
+
*/
|
|
33
|
+
separators?: readonly [string, string];
|
|
34
|
+
/**
|
|
35
|
+
* Plain objects whose keys look like integers are refused, because
|
|
36
|
+
* `JSON.parse` and object literals both hoist such keys to the front in
|
|
37
|
+
* numeric order, so their insertion order is already lost. Pass a `Map` to
|
|
38
|
+
* carry an explicit order, or set this to accept the hoisted order knowingly.
|
|
39
|
+
*/
|
|
40
|
+
allowIntegerKeys?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Python `str` comparison: by Unicode code point, not UTF-16 code unit. */
|
|
43
|
+
export declare function compareCodePoints(a: string, b: string): number;
|
|
44
|
+
/** `repr(float)` for a JavaScript number that is not an integer. */
|
|
45
|
+
export declare function pyFloatRepr(x: number): string;
|
|
46
|
+
/** `json.dumps(str)`: the quoted, escaped form of one string. */
|
|
47
|
+
export declare function pyJsonString(s: string, ensureAscii?: boolean): string;
|
|
48
|
+
/**
|
|
49
|
+
* `json.dumps(value, **options)`.
|
|
50
|
+
*
|
|
51
|
+
* `undefined` values inside arrays become `null` (as `JSON.stringify` does);
|
|
52
|
+
* `undefined` object properties are omitted, matching the usual TypeScript
|
|
53
|
+
* habit of optional fields. Python has no `undefined`, so neither choice can
|
|
54
|
+
* disagree with it — but a `Map` may hold `undefined` deliberately and is
|
|
55
|
+
* written as `null` there, so prefer omitting keys over storing `undefined`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function pyJsonStringify(value: unknown, options?: PyJsonOptions): string;
|
|
58
|
+
/**
|
|
59
|
+
* `json.dump(value, file, indent=2)` followed by `file.write("\n")` — the
|
|
60
|
+
* shape every library entry, sidecar and index is written in.
|
|
61
|
+
*/
|
|
62
|
+
export declare function pyJsonDocument(value: unknown, options?: PyJsonOptions): string;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A JSON writer with the exact output of Python's `json.dumps`.
|
|
3
|
+
*
|
|
4
|
+
* The connector library the Azure pipeline publishes, the overlay `prepare`
|
|
5
|
+
* writes into a project, and the string literals inside every generated
|
|
6
|
+
* descriptor module were all produced by `json.dumps`. Its defaults differ from
|
|
7
|
+
* `JSON.stringify` in ways that change bytes, and the library's sha256 is
|
|
8
|
+
* published precisely so an unchanged refresh is a no-op:
|
|
9
|
+
*
|
|
10
|
+
* - `ensure_ascii=True` escapes every character outside `0x20..0x7e` as a
|
|
11
|
+
* lowercase `\uXXXX`, astral characters as two surrogate escapes. 1,651 of
|
|
12
|
+
* the 16,082 files in a current library carry such escapes.
|
|
13
|
+
* - with `indent`, the item separator is `,` + newline and the key separator
|
|
14
|
+
* is `: `; without, they are `, ` and `: ` — not `JSON.stringify`'s `,`/`:`.
|
|
15
|
+
* - `sort_keys` orders by code point, not by UTF-16 code unit.
|
|
16
|
+
* - a float prints as Python's `repr`: `1e-05`, `1e+16`, `2.5`, never `1e-5`.
|
|
17
|
+
*
|
|
18
|
+
* Everything the registry writes goes through this module, so the byte question
|
|
19
|
+
* is answered in one place and proven by one test: parse every library file,
|
|
20
|
+
* format it again, require the same bytes.
|
|
21
|
+
*/
|
|
22
|
+
const INTEGER_LIKE = /^(?:0|[1-9][0-9]*)$/;
|
|
23
|
+
/** Python `str` comparison: by Unicode code point, not UTF-16 code unit. */
|
|
24
|
+
export function compareCodePoints(a, b) {
|
|
25
|
+
const ai = a[Symbol.iterator]();
|
|
26
|
+
const bi = b[Symbol.iterator]();
|
|
27
|
+
for (;;) {
|
|
28
|
+
const x = ai.next();
|
|
29
|
+
const y = bi.next();
|
|
30
|
+
if (x.done && y.done)
|
|
31
|
+
return 0;
|
|
32
|
+
if (x.done)
|
|
33
|
+
return -1;
|
|
34
|
+
if (y.done)
|
|
35
|
+
return 1;
|
|
36
|
+
const cx = x.value.codePointAt(0);
|
|
37
|
+
const cy = y.value.codePointAt(0);
|
|
38
|
+
if (cx !== cy)
|
|
39
|
+
return cx < cy ? -1 : 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** `repr(float)` for a JavaScript number that is not an integer. */
|
|
43
|
+
export function pyFloatRepr(x) {
|
|
44
|
+
if (Number.isNaN(x))
|
|
45
|
+
return "NaN";
|
|
46
|
+
if (x === Infinity)
|
|
47
|
+
return "Infinity";
|
|
48
|
+
if (x === -Infinity)
|
|
49
|
+
return "-Infinity";
|
|
50
|
+
if (x === 0)
|
|
51
|
+
return Object.is(x, -0) ? "-0.0" : "0.0";
|
|
52
|
+
const negative = x < 0;
|
|
53
|
+
// `toExponential()` with no argument yields the shortest digit string that
|
|
54
|
+
// round-trips, which is the same digit string Python's `repr` starts from.
|
|
55
|
+
const match = /^(\d)(?:\.(\d+))?e([+-]\d+)$/.exec(Math.abs(x).toExponential());
|
|
56
|
+
if (!match)
|
|
57
|
+
return String(x);
|
|
58
|
+
const digits = (match[1] + (match[2] ?? "")).replace(/0+$/, "") || "0";
|
|
59
|
+
const decimalPoint = parseInt(match[3], 10) + 1; // value = 0.<digits> × 10^decimalPoint
|
|
60
|
+
let body;
|
|
61
|
+
if (decimalPoint <= -4 || decimalPoint > 16) {
|
|
62
|
+
const mantissa = digits.length > 1 ? `${digits[0]}.${digits.slice(1)}` : digits;
|
|
63
|
+
const exponent = decimalPoint - 1;
|
|
64
|
+
body = `${mantissa}e${exponent < 0 ? "-" : "+"}${String(Math.abs(exponent)).padStart(2, "0")}`;
|
|
65
|
+
}
|
|
66
|
+
else if (decimalPoint <= 0) {
|
|
67
|
+
body = `0.${"0".repeat(-decimalPoint)}${digits}`;
|
|
68
|
+
}
|
|
69
|
+
else if (decimalPoint >= digits.length) {
|
|
70
|
+
body = `${digits}${"0".repeat(decimalPoint - digits.length)}.0`;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
body = `${digits.slice(0, decimalPoint)}.${digits.slice(decimalPoint)}`;
|
|
74
|
+
}
|
|
75
|
+
return negative ? `-${body}` : body;
|
|
76
|
+
}
|
|
77
|
+
function formatNumber(n) {
|
|
78
|
+
if (Number.isInteger(n) && !Object.is(n, -0)) {
|
|
79
|
+
// Python prints every digit of an int; JS switches to exponent form at 1e21.
|
|
80
|
+
return Math.abs(n) < 1e21 ? String(n) : BigInt(n).toString();
|
|
81
|
+
}
|
|
82
|
+
return pyFloatRepr(n);
|
|
83
|
+
}
|
|
84
|
+
const SHORT_ESCAPES = {
|
|
85
|
+
'"': '\\"',
|
|
86
|
+
"\\": "\\\\",
|
|
87
|
+
"\n": "\\n",
|
|
88
|
+
"\r": "\\r",
|
|
89
|
+
"\t": "\\t",
|
|
90
|
+
"\b": "\\b",
|
|
91
|
+
"\f": "\\f",
|
|
92
|
+
};
|
|
93
|
+
function hex4(unit) {
|
|
94
|
+
return `\\u${unit.toString(16).padStart(4, "0")}`;
|
|
95
|
+
}
|
|
96
|
+
/** `json.dumps(str)`: the quoted, escaped form of one string. */
|
|
97
|
+
export function pyJsonString(s, ensureAscii = true) {
|
|
98
|
+
// Indexing a number or an object by position finds nothing and the loop
|
|
99
|
+
// below would answer `""` — an unquoted nothing in the middle of a generated
|
|
100
|
+
// module — where `json.dumps(5)` writes `5`. Callers with a non-string want
|
|
101
|
+
// `pyJsonStringify`; this one is for strings only, and says so.
|
|
102
|
+
if (typeof s !== "string") {
|
|
103
|
+
throw new TypeError(`pyJsonString: expected a string, got ${s === null ? "null" : typeof s}`);
|
|
104
|
+
}
|
|
105
|
+
let out = '"';
|
|
106
|
+
for (let i = 0; i < s.length; i++) {
|
|
107
|
+
const ch = s[i];
|
|
108
|
+
const short = SHORT_ESCAPES[ch];
|
|
109
|
+
if (short !== undefined) {
|
|
110
|
+
out += short;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const unit = s.charCodeAt(i);
|
|
114
|
+
if (unit < 0x20) {
|
|
115
|
+
out += hex4(unit);
|
|
116
|
+
}
|
|
117
|
+
else if (ensureAscii && unit > 0x7e) {
|
|
118
|
+
// Astral characters are two UTF-16 units and Python escapes them as the
|
|
119
|
+
// two surrogates, so escaping unit by unit is exactly right.
|
|
120
|
+
out += hex4(unit);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
out += ch;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out + '"';
|
|
127
|
+
}
|
|
128
|
+
function entriesOf(value, options) {
|
|
129
|
+
let entries;
|
|
130
|
+
if (value instanceof Map) {
|
|
131
|
+
entries = [];
|
|
132
|
+
for (const [key, v] of value) {
|
|
133
|
+
if (typeof key !== "string") {
|
|
134
|
+
throw new TypeError(`pyjson: Map keys must be strings, got ${typeof key}`);
|
|
135
|
+
}
|
|
136
|
+
entries.push([key, v]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
entries = Object.entries(value);
|
|
141
|
+
if (!options.allowIntegerKeys) {
|
|
142
|
+
const hoisted = entries.find(([key]) => INTEGER_LIKE.test(key));
|
|
143
|
+
if (hoisted) {
|
|
144
|
+
throw new TypeError(`pyjson: object key "${hoisted[0]}" looks like an integer, so JavaScript has ` +
|
|
145
|
+
"already moved it to the front; carry the object as a Map to keep its order, " +
|
|
146
|
+
"or pass allowIntegerKeys to accept the hoisted order.");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (options.sortKeys) {
|
|
151
|
+
entries = [...entries].sort((a, b) => compareCodePoints(a[0], b[0]));
|
|
152
|
+
}
|
|
153
|
+
return entries;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* `json.dumps(value, **options)`.
|
|
157
|
+
*
|
|
158
|
+
* `undefined` values inside arrays become `null` (as `JSON.stringify` does);
|
|
159
|
+
* `undefined` object properties are omitted, matching the usual TypeScript
|
|
160
|
+
* habit of optional fields. Python has no `undefined`, so neither choice can
|
|
161
|
+
* disagree with it — but a `Map` may hold `undefined` deliberately and is
|
|
162
|
+
* written as `null` there, so prefer omitting keys over storing `undefined`.
|
|
163
|
+
*/
|
|
164
|
+
export function pyJsonStringify(value, options = {}) {
|
|
165
|
+
const indent = options.indent;
|
|
166
|
+
const ensureAscii = options.ensureAscii ?? true;
|
|
167
|
+
const [itemSeparator, keySeparator] = options.separators ?? (indent === undefined ? [", ", ": "] : [",", ": "]);
|
|
168
|
+
const write = (v, depth) => {
|
|
169
|
+
if (v === null || v === undefined)
|
|
170
|
+
return "null";
|
|
171
|
+
switch (typeof v) {
|
|
172
|
+
case "string":
|
|
173
|
+
return pyJsonString(v, ensureAscii);
|
|
174
|
+
case "number":
|
|
175
|
+
return formatNumber(v);
|
|
176
|
+
case "boolean":
|
|
177
|
+
return v ? "true" : "false";
|
|
178
|
+
case "bigint":
|
|
179
|
+
return v.toString();
|
|
180
|
+
case "object":
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
throw new TypeError(`pyjson: cannot serialize a ${typeof v}`);
|
|
184
|
+
}
|
|
185
|
+
const inner = indent === undefined ? "" : `\n${" ".repeat(indent * (depth + 1))}`;
|
|
186
|
+
const closing = indent === undefined ? "" : `\n${" ".repeat(indent * depth)}`;
|
|
187
|
+
if (Array.isArray(v)) {
|
|
188
|
+
if (v.length === 0)
|
|
189
|
+
return "[]";
|
|
190
|
+
return `[${inner}${v.map((item) => write(item, depth + 1)).join(itemSeparator + inner)}${closing}]`;
|
|
191
|
+
}
|
|
192
|
+
const entries = entriesOf(v, options).filter(([, item]) => item !== undefined || v instanceof Map);
|
|
193
|
+
if (entries.length === 0)
|
|
194
|
+
return "{}";
|
|
195
|
+
const body = entries
|
|
196
|
+
.map(([key, item]) => `${pyJsonString(key, ensureAscii)}${keySeparator}${write(item, depth + 1)}`)
|
|
197
|
+
.join(itemSeparator + inner);
|
|
198
|
+
return `{${inner}${body}${closing}}`;
|
|
199
|
+
};
|
|
200
|
+
return write(value, 0);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* `json.dump(value, file, indent=2)` followed by `file.write("\n")` — the
|
|
204
|
+
* shape every library entry, sidecar and index is written in.
|
|
205
|
+
*/
|
|
206
|
+
export function pyJsonDocument(value, options = {}) {
|
|
207
|
+
return `${pyJsonStringify(value, { indent: 2, ...options })}\n`;
|
|
208
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orderings that must match Python's `sorted()`.
|
|
3
|
+
*
|
|
4
|
+
* Python compares strings by code point and tuples element by element;
|
|
5
|
+
* `Array.prototype.sort` with no comparator compares UTF-16 code units of the
|
|
6
|
+
* stringified value. The two agree on ASCII and disagree above U+FFFF, which is
|
|
7
|
+
* why every sort that decides output bytes goes through one of these.
|
|
8
|
+
*/
|
|
9
|
+
export { compareCodePoints } from "../codecs/pyjson.js";
|
|
10
|
+
/** A stable sort by one string key, code-point ordered. */
|
|
11
|
+
export declare function sortedByCodePoint<T>(items: readonly T[], key: (item: T) => string): T[];
|
|
12
|
+
/**
|
|
13
|
+
* `sorted(items, key=lambda x: (k1(x), k2(x), …))`: compare the first key, then
|
|
14
|
+
* the next, each by code point. Stable, like Python's.
|
|
15
|
+
*/
|
|
16
|
+
export declare function sortedByKeys<T>(items: readonly T[], ...keys: Array<(item: T) => string>): T[];
|
|
17
|
+
/** `sorted(strings)`. */
|
|
18
|
+
export declare function sortedStrings(items: Iterable<string>): string[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orderings that must match Python's `sorted()`.
|
|
3
|
+
*
|
|
4
|
+
* Python compares strings by code point and tuples element by element;
|
|
5
|
+
* `Array.prototype.sort` with no comparator compares UTF-16 code units of the
|
|
6
|
+
* stringified value. The two agree on ASCII and disagree above U+FFFF, which is
|
|
7
|
+
* why every sort that decides output bytes goes through one of these.
|
|
8
|
+
*/
|
|
9
|
+
export { compareCodePoints } from "../codecs/pyjson.js";
|
|
10
|
+
import { compareCodePoints } from "../codecs/pyjson.js";
|
|
11
|
+
/** A stable sort by one string key, code-point ordered. */
|
|
12
|
+
export function sortedByCodePoint(items, key) {
|
|
13
|
+
return [...items].sort((a, b) => compareCodePoints(key(a), key(b)));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `sorted(items, key=lambda x: (k1(x), k2(x), …))`: compare the first key, then
|
|
17
|
+
* the next, each by code point. Stable, like Python's.
|
|
18
|
+
*/
|
|
19
|
+
export function sortedByKeys(items, ...keys) {
|
|
20
|
+
return [...items].sort((a, b) => {
|
|
21
|
+
for (const key of keys) {
|
|
22
|
+
const c = compareCodePoints(key(a), key(b));
|
|
23
|
+
if (c !== 0)
|
|
24
|
+
return c;
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** `sorted(strings)`. */
|
|
30
|
+
export function sortedStrings(items) {
|
|
31
|
+
return [...items].sort(compareCodePoints);
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path spelling for values that land inside files.
|
|
3
|
+
*
|
|
4
|
+
* `index.json` rows carry `path`, and the overlay index carries
|
|
5
|
+
* `descriptors/<key>/…`. Both are read as `/`-separated by every consumer, so
|
|
6
|
+
* they are forced to POSIX here regardless of the host, where Python's
|
|
7
|
+
* `os.path.relpath` would have used the OS separator.
|
|
8
|
+
*/
|
|
9
|
+
import { relative, sep } from "node:path";
|
|
10
|
+
/** Replace the platform separator with `/`. */
|
|
11
|
+
export function toPosix(p) {
|
|
12
|
+
return sep === "/" ? p : p.split(sep).join("/");
|
|
13
|
+
}
|
|
14
|
+
/** `os.path.relpath(target, base)` spelled with `/`. */
|
|
15
|
+
export function relativePosix(base, target) {
|
|
16
|
+
return toPosix(relative(base, target));
|
|
17
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** `str.strip()` with no argument. */
|
|
2
|
+
export declare function pyStrip(s: string): string;
|
|
3
|
+
/** `str.rstrip()` with no argument. */
|
|
4
|
+
export declare function pyRstrip(s: string): string;
|
|
5
|
+
/** `str.strip(chars)`: strip any of the given characters from both ends. */
|
|
6
|
+
export declare function pyStripChars(s: string, chars: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* `str(value)` for the JSON-shaped values the generators coerce: `True`,
|
|
9
|
+
* `None`, `1.0`, never `true`, `null`, `1`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function pyStr(value: unknown): string;
|
|
12
|
+
/**
|
|
13
|
+
* `str.title()`: each character after a non-cased one (or at the start) is
|
|
14
|
+
* titlecased, every other one lowercased — so digits and apostrophes start a
|
|
15
|
+
* new word (`"a1b"` → `"A1B"`, `"o'brien"` → `"O'Brien"`), unlike a
|
|
16
|
+
* split-on-space capitaliser, and a capital sigma ending a word becomes `ς`
|
|
17
|
+
* (`"ΟΔΟΣ"` → `"Οδος"`).
|
|
18
|
+
*
|
|
19
|
+
* Checked against CPython 3.13 (Unicode 15.1) over all 1,112,064 code points,
|
|
20
|
+
* each as `chr(cp)` — which exercises the titlecase branch — and again as
|
|
21
|
+
* `"A" + chr(cp)`, `"AΣ" + chr(cp)` and `chr(cp) + "Σ"`, which exercise the
|
|
22
|
+
* lowercase branch and the final-sigma rule in both directions. Every
|
|
23
|
+
* disagreement was a code point Node's ICU 16 cases and Unicode 15.1 does not:
|
|
24
|
+
* U+019B, U+0264, U+1C89, U+1C8A, U+A7CB–A7CD, U+A7DA–A7DC and the Garay block
|
|
25
|
+
* (U+10D50–10D65, U+10D70–10D85) — 54 code points in total. So this matches
|
|
26
|
+
* `str.title()` exactly on the repertoire the two share, and an engine newer
|
|
27
|
+
* than the Python it is compared with will case a handful of letters Python
|
|
28
|
+
* leaves alone.
|
|
29
|
+
*
|
|
30
|
+
* None of the 54 appears in the committed library's slugs, labels or field
|
|
31
|
+
* names, which is where it matters — but that is an observation about how the
|
|
32
|
+
* registry names things today, not a rule anything enforces, so a future
|
|
33
|
+
* connector could in principle carry one. The curated cases that guard the
|
|
34
|
+
* behaviour are in `registry-compat.test.ts`, via the fixture Python wrote.
|
|
35
|
+
*/
|
|
36
|
+
export declare function pyTitle(s: string): string;
|
|
37
|
+
/** `s[:1].isupper()`: the first code point is an uppercase letter. */
|
|
38
|
+
export declare function startsWithUpper(s: string): boolean;
|
|
39
|
+
/** `s[:1].upper() + s[1:]`, slicing by code point. */
|
|
40
|
+
export declare function upperFirst(s: string): string;
|
|
41
|
+
/** `s[:1].lower() + s[1:]`, slicing by code point. */
|
|
42
|
+
export declare function lowerFirst(s: string): string;
|
|
43
|
+
/** `s.rsplit(sep, 1)[-1]`. */
|
|
44
|
+
export declare function afterLast(s: string, sep: string): string;
|
|
45
|
+
/** `s.rsplit(sep, 1)[0]` when `sep` occurs, else `s`. */
|
|
46
|
+
export declare function beforeLast(s: string, sep: string): string;
|
|
47
|
+
/** `s.split(sep, 1)` as a pair; the second element is `undefined` when absent. */
|
|
48
|
+
export declare function splitOnce(s: string, sep: string): [string, string | undefined];
|
|
49
|
+
/** `str.isdigit()` for ASCII digits, which is all the generators ever test. */
|
|
50
|
+
export declare function isAsciiDigits(s: string): boolean;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The few string operations that must behave like Python's, because their
|
|
3
|
+
* result reaches a file the Python generators used to write.
|
|
4
|
+
*
|
|
5
|
+
* Everything else in the registry is ordinary TypeScript. This module exists
|
|
6
|
+
* so the places where JavaScript and Python disagree — `strip()`'s whitespace
|
|
7
|
+
* set, `str()` of a bool, `title()` after a digit, slicing by code point — are
|
|
8
|
+
* spelled once and tested against a table Python itself produced
|
|
9
|
+
* (`typescript/tests/fixtures/registry/pycompat.json`).
|
|
10
|
+
*/
|
|
11
|
+
import { pyFloatRepr } from "../codecs/pyjson.js";
|
|
12
|
+
/**
|
|
13
|
+
* Characters `str.isspace()` is true for. Not the same set as JavaScript's
|
|
14
|
+
* `\s`: Python includes the C1 separators `\x1c`–`\x1f` and `\x85`, and
|
|
15
|
+
* excludes the BOM ``.
|
|
16
|
+
*/
|
|
17
|
+
const PY_SPACE = "\t\n\u000b\u000c\r\u001c\u001d\u001e\u001f \u0085\u00a0\u1680" +
|
|
18
|
+
"\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a" +
|
|
19
|
+
"\u2028\u2029\u202f\u205f\u3000";
|
|
20
|
+
function isPySpace(ch) {
|
|
21
|
+
return PY_SPACE.includes(ch);
|
|
22
|
+
}
|
|
23
|
+
/** `str.strip()` with no argument. */
|
|
24
|
+
export function pyStrip(s) {
|
|
25
|
+
let start = 0;
|
|
26
|
+
let end = s.length;
|
|
27
|
+
while (start < end && isPySpace(s[start]))
|
|
28
|
+
start++;
|
|
29
|
+
while (end > start && isPySpace(s[end - 1]))
|
|
30
|
+
end--;
|
|
31
|
+
return s.slice(start, end);
|
|
32
|
+
}
|
|
33
|
+
/** `str.rstrip()` with no argument. */
|
|
34
|
+
export function pyRstrip(s) {
|
|
35
|
+
let end = s.length;
|
|
36
|
+
while (end > 0 && isPySpace(s[end - 1]))
|
|
37
|
+
end--;
|
|
38
|
+
return s.slice(0, end);
|
|
39
|
+
}
|
|
40
|
+
/** `str.strip(chars)`: strip any of the given characters from both ends. */
|
|
41
|
+
export function pyStripChars(s, chars) {
|
|
42
|
+
let start = 0;
|
|
43
|
+
let end = s.length;
|
|
44
|
+
while (start < end && chars.includes(s[start]))
|
|
45
|
+
start++;
|
|
46
|
+
while (end > start && chars.includes(s[end - 1]))
|
|
47
|
+
end--;
|
|
48
|
+
return s.slice(start, end);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `str(value)` for the JSON-shaped values the generators coerce: `True`,
|
|
52
|
+
* `None`, `1.0`, never `true`, `null`, `1`.
|
|
53
|
+
*/
|
|
54
|
+
export function pyStr(value) {
|
|
55
|
+
if (value === null || value === undefined)
|
|
56
|
+
return "None";
|
|
57
|
+
if (typeof value === "boolean")
|
|
58
|
+
return value ? "True" : "False";
|
|
59
|
+
if (typeof value === "number") {
|
|
60
|
+
return Number.isInteger(value) && !Object.is(value, -0) ? String(value) : pyFloatRepr(value);
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "string")
|
|
63
|
+
return value;
|
|
64
|
+
if (typeof value === "bigint")
|
|
65
|
+
return value.toString();
|
|
66
|
+
// Python would print a repr of a dict or list here; the generators never do
|
|
67
|
+
// that on purpose, so make it loud rather than plausible.
|
|
68
|
+
throw new TypeError(`pyStr: refusing to stringify a ${Array.isArray(value) ? "list" : typeof value}`);
|
|
69
|
+
}
|
|
70
|
+
const UPPER = /\p{Lu}/u;
|
|
71
|
+
/**
|
|
72
|
+
* `str.title()`'s word-boundary test is the Unicode `Cased` property, not "a
|
|
73
|
+
* letter with a case category": a modifier letter such as `ʰ`, a circled `Ⓐ`
|
|
74
|
+
* or a Roman numeral `ⅷ` is cased and continues a word, while the apostrophe
|
|
75
|
+
* `ʼ` in `ʼn`'s titlecase is not and starts one.
|
|
76
|
+
*/
|
|
77
|
+
const CASED = /\p{Cased}/u;
|
|
78
|
+
/** `Case_Ignorable`: what the final-sigma rule looks past on either side. */
|
|
79
|
+
const CASE_IGNORABLE = /\p{Case_Ignorable}/u;
|
|
80
|
+
const TITLECASE_LETTER = /\p{Lt}/u;
|
|
81
|
+
/**
|
|
82
|
+
* Code points whose titlecase is neither themselves nor their `toUpperCase()`,
|
|
83
|
+
* from Unicode 15.1 (CPython 3.13). `ß` and the ligatures titlecase to a
|
|
84
|
+
* capital followed by lowercase, the digraphs to their dedicated titlecase
|
|
85
|
+
* code point, and the Greek forms with iota subscript to the base capital plus
|
|
86
|
+
* COMBINING GREEK YPOGEGRAMMENI (U+0345), where uppercasing gives a full
|
|
87
|
+
* capital iota instead. (`ʼn` → `ʼN` and `ǰ` → `J̌` are NOT here: there the
|
|
88
|
+
* titlecase equals the uppercase, which `toUpperCase()` already spells.)
|
|
89
|
+
*/
|
|
90
|
+
const TITLECASE_SPECIAL = new Map([
|
|
91
|
+
["ß", "Ss"],
|
|
92
|
+
["DŽ", "Dž"], ["dž", "Dž"], ["LJ", "Lj"], ["lj", "Lj"], ["NJ", "Nj"], ["nj", "Nj"], ["DZ", "Dz"], ["dz", "Dz"],
|
|
93
|
+
["և", "Եւ"],
|
|
94
|
+
["ᾲ", "Ὰͅ"], ["ᾳ", "ᾼ"], ["ᾴ", "Άͅ"], ["ᾷ", "ᾼ͂"],
|
|
95
|
+
["ῂ", "Ὴͅ"], ["ῃ", "ῌ"], ["ῄ", "Ήͅ"], ["ῇ", "ῌ͂"],
|
|
96
|
+
["ῲ", "Ὼͅ"], ["ῳ", "ῼ"], ["ῴ", "Ώͅ"], ["ῷ", "ῼ͂"],
|
|
97
|
+
["ff", "Ff"], ["fi", "Fi"], ["fl", "Fl"], ["ffi", "Ffi"], ["ffl", "Ffl"], ["ſt", "St"], ["st", "St"],
|
|
98
|
+
["ﬓ", "Մն"], ["ﬔ", "Մե"], ["ﬕ", "Մի"], ["ﬖ", "Վն"], ["ﬗ", "Մխ"],
|
|
99
|
+
]);
|
|
100
|
+
/**
|
|
101
|
+
* `_PyUnicode_ToTitleFull`: Python's titlecase of one code point, which
|
|
102
|
+
* JavaScript lacks. UnicodeData gives most code points no titlecase of their
|
|
103
|
+
* own, in which case it is their uppercase — and `toUpperCase()` already
|
|
104
|
+
* applies SpecialCasing there — so only the exceptions need spelling: a
|
|
105
|
+
* titlecase letter is its own titlecase; Georgian Mkhedruli titlecases to
|
|
106
|
+
* itself although it uppercases to Mtavruli; the Greek small letters with
|
|
107
|
+
* iota subscript (`ᾀ`…`ᾧ`) titlecase to the titlecase letter eight code points
|
|
108
|
+
* up; and the table above.
|
|
109
|
+
*/
|
|
110
|
+
function titlecaseChar(ch) {
|
|
111
|
+
const special = TITLECASE_SPECIAL.get(ch);
|
|
112
|
+
if (special !== undefined)
|
|
113
|
+
return special;
|
|
114
|
+
if (TITLECASE_LETTER.test(ch))
|
|
115
|
+
return ch;
|
|
116
|
+
const cp = ch.codePointAt(0);
|
|
117
|
+
if (cp >= 0x10d0 && cp <= 0x10ff)
|
|
118
|
+
return ch;
|
|
119
|
+
if ((cp >= 0x1f80 && cp <= 0x1f87) || (cp >= 0x1f90 && cp <= 0x1f97) || (cp >= 0x1fa0 && cp <= 0x1fa7)) {
|
|
120
|
+
return String.fromCodePoint(cp + 8);
|
|
121
|
+
}
|
|
122
|
+
return ch.toUpperCase();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Whether the `Σ` at `chars[i]` is in Final_Sigma context: preceded, past any
|
|
126
|
+
* case-ignorables, by a cased character, and not followed, past any
|
|
127
|
+
* case-ignorables, by one. CPython applies this inside `title()` and `lower()`;
|
|
128
|
+
* `toLowerCase()` applies it only when given the whole string, which lowering
|
|
129
|
+
* one character at a time forfeits.
|
|
130
|
+
*/
|
|
131
|
+
function isFinalSigma(chars, i) {
|
|
132
|
+
let j = i - 1;
|
|
133
|
+
while (j >= 0 && CASE_IGNORABLE.test(chars[j]))
|
|
134
|
+
j--;
|
|
135
|
+
if (j < 0 || !CASED.test(chars[j]))
|
|
136
|
+
return false;
|
|
137
|
+
j = i + 1;
|
|
138
|
+
while (j < chars.length && CASE_IGNORABLE.test(chars[j]))
|
|
139
|
+
j++;
|
|
140
|
+
return j === chars.length || !CASED.test(chars[j]);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* `str.title()`: each character after a non-cased one (or at the start) is
|
|
144
|
+
* titlecased, every other one lowercased — so digits and apostrophes start a
|
|
145
|
+
* new word (`"a1b"` → `"A1B"`, `"o'brien"` → `"O'Brien"`), unlike a
|
|
146
|
+
* split-on-space capitaliser, and a capital sigma ending a word becomes `ς`
|
|
147
|
+
* (`"ΟΔΟΣ"` → `"Οδος"`).
|
|
148
|
+
*
|
|
149
|
+
* Checked against CPython 3.13 (Unicode 15.1) over all 1,112,064 code points,
|
|
150
|
+
* each as `chr(cp)` — which exercises the titlecase branch — and again as
|
|
151
|
+
* `"A" + chr(cp)`, `"AΣ" + chr(cp)` and `chr(cp) + "Σ"`, which exercise the
|
|
152
|
+
* lowercase branch and the final-sigma rule in both directions. Every
|
|
153
|
+
* disagreement was a code point Node's ICU 16 cases and Unicode 15.1 does not:
|
|
154
|
+
* U+019B, U+0264, U+1C89, U+1C8A, U+A7CB–A7CD, U+A7DA–A7DC and the Garay block
|
|
155
|
+
* (U+10D50–10D65, U+10D70–10D85) — 54 code points in total. So this matches
|
|
156
|
+
* `str.title()` exactly on the repertoire the two share, and an engine newer
|
|
157
|
+
* than the Python it is compared with will case a handful of letters Python
|
|
158
|
+
* leaves alone.
|
|
159
|
+
*
|
|
160
|
+
* None of the 54 appears in the committed library's slugs, labels or field
|
|
161
|
+
* names, which is where it matters — but that is an observation about how the
|
|
162
|
+
* registry names things today, not a rule anything enforces, so a future
|
|
163
|
+
* connector could in principle carry one. The curated cases that guard the
|
|
164
|
+
* behaviour are in `registry-compat.test.ts`, via the fixture Python wrote.
|
|
165
|
+
*/
|
|
166
|
+
export function pyTitle(s) {
|
|
167
|
+
const chars = [...s];
|
|
168
|
+
let out = "";
|
|
169
|
+
let previousCased = false;
|
|
170
|
+
for (let i = 0; i < chars.length; i++) {
|
|
171
|
+
const ch = chars[i];
|
|
172
|
+
if (!previousCased)
|
|
173
|
+
out += titlecaseChar(ch);
|
|
174
|
+
else if (ch === "Σ")
|
|
175
|
+
out += isFinalSigma(chars, i) ? "ς" : "σ";
|
|
176
|
+
else
|
|
177
|
+
out += ch.toLowerCase();
|
|
178
|
+
previousCased = CASED.test(ch);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
/** `s[:1].isupper()`: the first code point is an uppercase letter. */
|
|
183
|
+
export function startsWithUpper(s) {
|
|
184
|
+
const first = s.codePointAt(0);
|
|
185
|
+
return first !== undefined && UPPER.test(String.fromCodePoint(first));
|
|
186
|
+
}
|
|
187
|
+
/** `s[:1].upper() + s[1:]`, slicing by code point. */
|
|
188
|
+
export function upperFirst(s) {
|
|
189
|
+
const first = s.codePointAt(0);
|
|
190
|
+
if (first === undefined)
|
|
191
|
+
return s;
|
|
192
|
+
const head = String.fromCodePoint(first);
|
|
193
|
+
return head.toUpperCase() + s.slice(head.length);
|
|
194
|
+
}
|
|
195
|
+
/** `s[:1].lower() + s[1:]`, slicing by code point. */
|
|
196
|
+
export function lowerFirst(s) {
|
|
197
|
+
const first = s.codePointAt(0);
|
|
198
|
+
if (first === undefined)
|
|
199
|
+
return s;
|
|
200
|
+
const head = String.fromCodePoint(first);
|
|
201
|
+
return head.toLowerCase() + s.slice(head.length);
|
|
202
|
+
}
|
|
203
|
+
/** `s.rsplit(sep, 1)[-1]`. */
|
|
204
|
+
export function afterLast(s, sep) {
|
|
205
|
+
const at = s.lastIndexOf(sep);
|
|
206
|
+
return at < 0 ? s : s.slice(at + sep.length);
|
|
207
|
+
}
|
|
208
|
+
/** `s.rsplit(sep, 1)[0]` when `sep` occurs, else `s`. */
|
|
209
|
+
export function beforeLast(s, sep) {
|
|
210
|
+
const at = s.lastIndexOf(sep);
|
|
211
|
+
return at < 0 ? s : s.slice(0, at);
|
|
212
|
+
}
|
|
213
|
+
/** `s.split(sep, 1)` as a pair; the second element is `undefined` when absent. */
|
|
214
|
+
export function splitOnce(s, sep) {
|
|
215
|
+
const at = s.indexOf(sep);
|
|
216
|
+
return at < 0 ? [s, undefined] : [s.slice(0, at), s.slice(at + sep.length)];
|
|
217
|
+
}
|
|
218
|
+
/** `str.isdigit()` for ASCII digits, which is all the generators ever test. */
|
|
219
|
+
export function isAsciiDigits(s) {
|
|
220
|
+
return /^[0-9]+$/.test(s);
|
|
221
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded parallelism with results in input order.
|
|
3
|
+
*
|
|
4
|
+
* The library builder used seven thread pools; four of them appended results as
|
|
5
|
+
* they completed, so the order of expanded entries — and, on a tie, which of two
|
|
6
|
+
* colliding entries won — depended on scheduling. This helper runs at most
|
|
7
|
+
* `limit` tasks at once and always returns `results[i]` for `items[i]`, so the
|
|
8
|
+
* output of a build is a function of its inputs alone.
|
|
9
|
+
*/
|
|
10
|
+
export declare function mapConcurrent<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded parallelism with results in input order.
|
|
3
|
+
*
|
|
4
|
+
* The library builder used seven thread pools; four of them appended results as
|
|
5
|
+
* they completed, so the order of expanded entries — and, on a tie, which of two
|
|
6
|
+
* colliding entries won — depended on scheduling. This helper runs at most
|
|
7
|
+
* `limit` tasks at once and always returns `results[i]` for `items[i]`, so the
|
|
8
|
+
* output of a build is a function of its inputs alone.
|
|
9
|
+
*/
|
|
10
|
+
export async function mapConcurrent(items, limit, fn) {
|
|
11
|
+
// `ThreadPoolExecutor(max_workers=0)` raised `ValueError: max_workers must
|
|
12
|
+
// be greater than 0`, and so does this, even for no work: a limit that is
|
|
13
|
+
// not a positive number is a caller's mistake, and clamping it to one
|
|
14
|
+
// worker would turn a pipeline typo into a build that runs serially for
|
|
15
|
+
// hours instead of failing at once.
|
|
16
|
+
if (!(Number.isFinite(limit) && limit >= 1)) {
|
|
17
|
+
throw new RangeError(`mapConcurrent: limit must be a positive number, got ${limit}`);
|
|
18
|
+
}
|
|
19
|
+
const results = new Array(items.length);
|
|
20
|
+
if (items.length === 0)
|
|
21
|
+
return results;
|
|
22
|
+
const workers = Math.min(Math.floor(limit), items.length);
|
|
23
|
+
let next = 0;
|
|
24
|
+
const run = async () => {
|
|
25
|
+
for (;;) {
|
|
26
|
+
const index = next++;
|
|
27
|
+
if (index >= items.length)
|
|
28
|
+
return;
|
|
29
|
+
results[index] = await fn(items[index], index);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
await Promise.all(Array.from({ length: workers }, run));
|
|
33
|
+
return results;
|
|
34
|
+
}
|