@uipath/maestro-builder-sdk 6.3.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.
Files changed (161) hide show
  1. package/dist/api-index.md +6 -6
  2. package/dist/cli/commands/registry.js +68 -12
  3. package/dist/registry/archive/entry-policy.d.ts +36 -0
  4. package/dist/registry/archive/entry-policy.js +60 -0
  5. package/dist/registry/archive/extract.d.ts +31 -0
  6. package/dist/registry/archive/extract.js +92 -0
  7. package/dist/registry/archive/zip-reader.d.ts +38 -0
  8. package/dist/registry/archive/zip-reader.js +256 -0
  9. package/dist/registry/codecs/pyjson.d.ts +62 -0
  10. package/dist/registry/codecs/pyjson.js +208 -0
  11. package/dist/registry/compat/compare.d.ts +18 -0
  12. package/dist/registry/compat/compare.js +32 -0
  13. package/dist/registry/compat/paths.d.ts +4 -0
  14. package/dist/registry/compat/paths.js +17 -0
  15. package/dist/registry/compat/pystr.d.ts +50 -0
  16. package/dist/registry/compat/pystr.js +221 -0
  17. package/dist/registry/concurrency.d.ts +10 -0
  18. package/dist/registry/concurrency.js +34 -0
  19. package/dist/registry/curated-overlay.d.ts +36 -4
  20. package/dist/registry/curated-overlay.js +87 -14
  21. package/dist/registry/errors.d.ts +26 -0
  22. package/dist/registry/errors.js +38 -0
  23. package/dist/registry/extractor.d.ts +9 -10
  24. package/dist/registry/extractor.js +5 -15
  25. package/dist/registry/files.d.ts +26 -0
  26. package/dist/registry/files.js +160 -0
  27. package/dist/registry/generators/descriptors.d.ts +81 -0
  28. package/dist/registry/generators/descriptors.js +405 -0
  29. package/dist/registry/generators/fields.d.ts +46 -0
  30. package/dist/registry/generators/fields.js +111 -0
  31. package/dist/registry/generators/markdown/common.d.ts +21 -0
  32. package/dist/registry/generators/markdown/common.js +55 -0
  33. package/dist/registry/generators/markdown/index.d.ts +33 -0
  34. package/dist/registry/generators/markdown/index.js +37 -0
  35. package/dist/registry/generators/markdown/page.d.ts +8 -0
  36. package/dist/registry/generators/markdown/page.js +52 -0
  37. package/dist/registry/generators/markdown/pyvalue.d.ts +34 -0
  38. package/dist/registry/generators/markdown/pyvalue.js +106 -0
  39. package/dist/registry/generators/markdown/sections/choosing-object.d.ts +2 -0
  40. package/dist/registry/generators/markdown/sections/choosing-object.js +52 -0
  41. package/dist/registry/generators/markdown/sections/example.d.ts +22 -0
  42. package/dist/registry/generators/markdown/sections/example.js +295 -0
  43. package/dist/registry/generators/markdown/sections/filtering.d.ts +2 -0
  44. package/dist/registry/generators/markdown/sections/filtering.js +76 -0
  45. package/dist/registry/generators/markdown/sections/header.d.ts +2 -0
  46. package/dist/registry/generators/markdown/sections/header.js +37 -0
  47. package/dist/registry/generators/markdown/sections/inputs.d.ts +30 -0
  48. package/dist/registry/generators/markdown/sections/inputs.js +151 -0
  49. package/dist/registry/generators/markdown/sections/lookups.d.ts +12 -0
  50. package/dist/registry/generators/markdown/sections/lookups.js +150 -0
  51. package/dist/registry/generators/markdown/sections/outputs.d.ts +3 -0
  52. package/dist/registry/generators/markdown/sections/outputs.js +9 -0
  53. package/dist/registry/generators/markdown/sections/resolving-fields.d.ts +2 -0
  54. package/dist/registry/generators/markdown/sections/resolving-fields.js +139 -0
  55. package/dist/registry/generators/markdown/sections/schema-dynamic.d.ts +2 -0
  56. package/dist/registry/generators/markdown/sections/schema-dynamic.js +86 -0
  57. package/dist/registry/generators/materialize.d.ts +30 -0
  58. package/dist/registry/generators/materialize.js +132 -0
  59. package/dist/registry/generators/md-symbols.d.ts +22 -0
  60. package/dist/registry/generators/md-symbols.js +58 -0
  61. package/dist/registry/generators/triggers.d.ts +15 -0
  62. package/dist/registry/generators/triggers.js +85 -0
  63. package/dist/registry/generators/type-registry.d.ts +50 -0
  64. package/dist/registry/generators/type-registry.js +154 -0
  65. package/dist/registry/generators/type-tree.d.ts +65 -0
  66. package/dist/registry/generators/type-tree.js +179 -0
  67. package/dist/registry/library-build/build.d.ts +57 -0
  68. package/dist/registry/library-build/build.js +286 -0
  69. package/dist/registry/library-build/canonical.d.ts +119 -0
  70. package/dist/registry/library-build/canonical.js +512 -0
  71. package/dist/registry/library-build/cli.d.ts +45 -0
  72. package/dist/registry/library-build/cli.js +182 -0
  73. package/dist/registry/library-build/context.d.ts +107 -0
  74. package/dist/registry/library-build/context.js +267 -0
  75. package/dist/registry/library-build/enrich.d.ts +36 -0
  76. package/dist/registry/library-build/enrich.js +158 -0
  77. package/dist/registry/library-build/events.d.ts +108 -0
  78. package/dist/registry/library-build/events.js +476 -0
  79. package/dist/registry/library-build/generic-expand.d.ts +77 -0
  80. package/dist/registry/library-build/generic-expand.js +363 -0
  81. package/dist/registry/library-build/plan.d.ts +71 -0
  82. package/dist/registry/library-build/plan.js +270 -0
  83. package/dist/registry/library-build/py.d.ts +45 -0
  84. package/dist/registry/library-build/py.js +167 -0
  85. package/dist/registry/library-build/schema-action.d.ts +38 -0
  86. package/dist/registry/library-build/schema-action.js +107 -0
  87. package/dist/registry/library-build/write.d.ts +6 -0
  88. package/dist/registry/library-build/write.js +26 -0
  89. package/dist/registry/library-reader.d.ts +20 -0
  90. package/dist/registry/library-reader.js +50 -0
  91. package/dist/registry/lookup-specs.d.ts +55 -0
  92. package/dist/registry/lookup-specs.js +140 -0
  93. package/dist/registry/materialize-cli.js +28 -14
  94. package/dist/registry/materializer.d.ts +25 -3
  95. package/dist/registry/materializer.js +45 -29
  96. package/dist/registry/model/library.d.ts +254 -0
  97. package/dist/registry/model/library.js +23 -0
  98. package/dist/registry/naming.d.ts +54 -0
  99. package/dist/registry/naming.js +142 -0
  100. package/dist/registry/orphans.d.ts +4 -0
  101. package/dist/registry/orphans.js +86 -0
  102. package/dist/registry/prepare/actions.d.ts +26 -0
  103. package/dist/registry/prepare/actions.js +94 -0
  104. package/dist/registry/prepare/connections.d.ts +181 -0
  105. package/dist/registry/prepare/connections.js +399 -0
  106. package/dist/registry/prepare/event.d.ts +96 -0
  107. package/dist/registry/prepare/event.js +268 -0
  108. package/dist/registry/prepare/handoff.d.ts +21 -0
  109. package/dist/registry/prepare/handoff.js +91 -0
  110. package/dist/registry/prepare/ledger.d.ts +104 -0
  111. package/dist/registry/prepare/ledger.js +216 -0
  112. package/dist/registry/prepare/messages.d.ts +190 -0
  113. package/dist/registry/prepare/messages.js +472 -0
  114. package/dist/registry/prepare/objects.d.ts +130 -0
  115. package/dist/registry/prepare/objects.js +318 -0
  116. package/dist/registry/prepare/overlay.d.ts +149 -0
  117. package/dist/registry/prepare/overlay.js +487 -0
  118. package/dist/registry/prepare/parents.d.ts +41 -0
  119. package/dist/registry/prepare/parents.js +122 -0
  120. package/dist/registry/prepare/pipeline.d.ts +91 -0
  121. package/dist/registry/prepare/pipeline.js +376 -0
  122. package/dist/registry/prepare/regenerate.d.ts +9 -0
  123. package/dist/registry/prepare/regenerate.js +50 -0
  124. package/dist/registry/prepare/resolve.d.ts +199 -0
  125. package/dist/registry/prepare/resolve.js +470 -0
  126. package/dist/registry/prepare/retry.d.ts +83 -0
  127. package/dist/registry/prepare/retry.js +109 -0
  128. package/dist/registry/prepare/source-import.d.ts +54 -0
  129. package/dist/registry/prepare/source-import.js +192 -0
  130. package/dist/registry/prepare/support.d.ts +41 -0
  131. package/dist/registry/prepare/support.js +178 -0
  132. package/dist/registry/prepare-cli.d.ts +16 -5
  133. package/dist/registry/prepare-cli.js +60 -53
  134. package/dist/registry/pull-cli.js +8 -7
  135. package/dist/registry/reporter.d.ts +26 -0
  136. package/dist/registry/reporter.js +41 -0
  137. package/dist/registry/uip/client.d.ts +99 -0
  138. package/dist/registry/uip/client.js +226 -0
  139. package/dist/registry/uip/commands.d.ts +40 -0
  140. package/dist/registry/uip/commands.js +80 -0
  141. package/dist/registry/uip/envelope.d.ts +37 -0
  142. package/dist/registry/uip/envelope.js +84 -0
  143. package/dist/registry/uip/response-cache.d.ts +28 -0
  144. package/dist/registry/uip/response-cache.js +57 -0
  145. package/lib/README.md +10 -8
  146. package/package.json +3 -3
  147. package/dist/generators/_connections.py +0 -331
  148. package/dist/generators/_events.py +0 -355
  149. package/dist/generators/_lookups.py +0 -114
  150. package/dist/generators/_naming.py +0 -185
  151. package/dist/generators/_objects.py +0 -299
  152. package/dist/generators/_resolve.py +0 -319
  153. package/dist/generators/convert_library_to_md.py +0 -1441
  154. package/dist/generators/extract_library.py +0 -125
  155. package/dist/generators/generate_connectors_ts.py +0 -936
  156. package/dist/generators/generate_library_json.py +0 -2527
  157. package/dist/generators/generate_triggers_ts.py +0 -110
  158. package/dist/generators/materialize_registry.py +0 -155
  159. package/dist/generators/prepare_connector.py +0 -1860
  160. package/dist/registry/python.d.ts +0 -26
  161. package/dist/registry/python.js +0 -46
@@ -0,0 +1,256 @@
1
+ /**
2
+ * A zip reader for exactly one archive: the connector library `registry pull`
3
+ * downloads.
4
+ *
5
+ * The SDK has no archive dependency and this keeps it that way. Node 22's
6
+ * `node:zlib` carries `inflateRawSync` and `crc32`, which is everything a
7
+ * reader needs for a zip that stores or deflates. The archive is self-produced
8
+ * by the pipeline (`zip -X` over a sorted file list): compression methods 0 and
9
+ * 8 only, no zip64, no data descriptors, no encryption, ASCII names. Everything
10
+ * outside that is refused rather than guessed at — this is a reader for the
11
+ * archive we make, not a general one.
12
+ *
13
+ * Layout, for the reader who has not seen a zip lately: every file's bytes sit
14
+ * behind a LOCAL FILE HEADER; after all of them comes the CENTRAL DIRECTORY,
15
+ * one header per entry pointing back at its local header; and the file ends
16
+ * with the END OF CENTRAL DIRECTORY record, which locates the directory and may
17
+ * be followed by an archive comment. Readers start at the end.
18
+ */
19
+ import { crc32, inflateRawSync } from "node:zlib";
20
+ /** The bytes are not a zip we can read: no end record, bad signatures, corrupt data. */
21
+ export class NotAZip extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = "NotAZip";
25
+ }
26
+ }
27
+ /** A well-formed zip that uses a feature this reader refuses (zip64, encryption, …). */
28
+ export class UnsupportedArchive extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = "UnsupportedArchive";
32
+ }
33
+ }
34
+ const SIG_LOCAL_HEADER = 0x04034b50;
35
+ const SIG_CENTRAL_HEADER = 0x02014b50;
36
+ const SIG_EOCD = 0x06054b50;
37
+ const SIG_ZIP64_LOCATOR = 0x07064b50;
38
+ const LOCAL_HEADER_SIZE = 30;
39
+ const CENTRAL_HEADER_SIZE = 46;
40
+ const EOCD_SIZE = 22;
41
+ const ZIP64_LOCATOR_SIZE = 20;
42
+ const MAX_COMMENT = 0xffff;
43
+ const METHOD_STORED = 0;
44
+ const METHOD_DEFLATE = 8;
45
+ const FLAG_ENCRYPTED = 1 << 0;
46
+ const FLAG_DATA_DESCRIPTOR = 1 << 3;
47
+ const FLAG_UTF8_NAMES = 1 << 11;
48
+ /** Bits 1–2 parameterise deflate and bit 11 names the encoding; nothing else is expected. */
49
+ const FLAGS_UNDERSTOOD = (1 << 1) | (1 << 2) | FLAG_UTF8_NAMES;
50
+ const ZIP64_EXTRA_ID = 0x0001;
51
+ function view(buffer) {
52
+ return new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
53
+ }
54
+ /** Decode an entry name: UTF-8 when the archive says so, otherwise ASCII only. */
55
+ function decodeName(bytes, flags) {
56
+ if (flags & FLAG_UTF8_NAMES) {
57
+ try {
58
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
59
+ }
60
+ catch {
61
+ throw new NotAZip("entry name is not valid UTF-8");
62
+ }
63
+ }
64
+ // Without bit 11 the name is IBM437, which Node cannot decode and this archive
65
+ // never needs: its names are ASCII, where the two agree.
66
+ let name = "";
67
+ for (const byte of bytes) {
68
+ if (byte > 0x7f)
69
+ throw new UnsupportedArchive("non-ASCII entry name without the UTF-8 flag");
70
+ name += String.fromCharCode(byte);
71
+ }
72
+ return name;
73
+ }
74
+ /** Refuse the features this reader does not implement, before any byte is inflated. */
75
+ function assertSupported(entry, extra) {
76
+ if (entry.flags & FLAG_ENCRYPTED) {
77
+ throw new UnsupportedArchive(`encrypted entry: ${entry.name}`);
78
+ }
79
+ // Python's zipfile tolerates bit 3 (sizes and CRC trail the data in a
80
+ // descriptor, the header carries zeros). This archive never sets it — the
81
+ // central directory always has the real values — so a set bit means a
82
+ // different producer, and refusing is the safer contract than trusting a
83
+ // header we would otherwise have to reconcile with the trailer.
84
+ if (entry.flags & FLAG_DATA_DESCRIPTOR) {
85
+ throw new UnsupportedArchive(`data descriptor entry: ${entry.name}`);
86
+ }
87
+ if (entry.flags & ~FLAGS_UNDERSTOOD & 0xffff) {
88
+ throw new UnsupportedArchive(`unrecognised general purpose flags 0x${entry.flags.toString(16)}: ${entry.name}`);
89
+ }
90
+ if (entry.method !== METHOD_STORED && entry.method !== METHOD_DEFLATE) {
91
+ throw new UnsupportedArchive(`compression method ${entry.method}: ${entry.name}`);
92
+ }
93
+ if (entry.compressedSize === 0xffffffff ||
94
+ entry.uncompressedSize === 0xffffffff ||
95
+ entry.localHeaderOffset === 0xffffffff) {
96
+ throw new UnsupportedArchive(`zip64 entry: ${entry.name}`);
97
+ }
98
+ // Extra fields are (id u16, size u16, data) records. Only the zip64 one matters.
99
+ const dv = view(extra);
100
+ let pos = 0;
101
+ while (pos + 4 <= extra.length) {
102
+ const id = dv.getUint16(pos, true);
103
+ const size = dv.getUint16(pos + 2, true);
104
+ if (id === ZIP64_EXTRA_ID)
105
+ throw new UnsupportedArchive(`zip64 entry: ${entry.name}`);
106
+ pos += 4 + size;
107
+ }
108
+ if (pos !== extra.length)
109
+ throw new NotAZip(`corrupt extra field: ${entry.name}`);
110
+ }
111
+ /**
112
+ * Find the End Of Central Directory record.
113
+ *
114
+ * It is the last 22 bytes when the archive has no comment; with one, it sits up
115
+ * to 65535 bytes earlier. We scan backwards for the signature and accept the
116
+ * first candidate whose comment length reaches exactly the end of the file, so
117
+ * a stray signature inside a comment cannot be mistaken for the record.
118
+ */
119
+ function readEndRecord(buffer) {
120
+ const dv = view(buffer);
121
+ const floor = Math.max(0, buffer.length - EOCD_SIZE - MAX_COMMENT);
122
+ for (let pos = buffer.length - EOCD_SIZE; pos >= floor; pos -= 1) {
123
+ if (dv.getUint32(pos, true) !== SIG_EOCD)
124
+ continue;
125
+ const commentLength = dv.getUint16(pos + 20, true);
126
+ if (pos + EOCD_SIZE + commentLength !== buffer.length)
127
+ continue;
128
+ const thisDisk = dv.getUint16(pos + 4, true);
129
+ const directoryDisk = dv.getUint16(pos + 6, true);
130
+ const entriesOnDisk = dv.getUint16(pos + 8, true);
131
+ const totalEntries = dv.getUint16(pos + 10, true);
132
+ const directorySize = dv.getUint32(pos + 12, true);
133
+ const directoryOffset = dv.getUint32(pos + 16, true);
134
+ if (totalEntries === 0xffff ||
135
+ directorySize === 0xffffffff ||
136
+ directoryOffset === 0xffffffff ||
137
+ (pos >= ZIP64_LOCATOR_SIZE && dv.getUint32(pos - ZIP64_LOCATOR_SIZE, true) === SIG_ZIP64_LOCATOR)) {
138
+ throw new UnsupportedArchive("zip64 archive");
139
+ }
140
+ if (thisDisk !== 0 || directoryDisk !== 0 || entriesOnDisk !== totalEntries) {
141
+ throw new UnsupportedArchive("multi-disk archive");
142
+ }
143
+ if (directoryOffset + directorySize > pos) {
144
+ throw new NotAZip("central directory overruns the end record");
145
+ }
146
+ return { totalEntries, directorySize, directoryOffset };
147
+ }
148
+ throw new NotAZip("no end of central directory record found");
149
+ }
150
+ /**
151
+ * Every entry in the archive, in central-directory order, with the features we
152
+ * refuse already rejected. Throws {@link NotAZip} when the bytes are not a zip
153
+ * and {@link UnsupportedArchive} for one we decline to read.
154
+ */
155
+ export function readZipEntries(buffer) {
156
+ const end = readEndRecord(buffer);
157
+ const dv = view(buffer);
158
+ const directoryEnd = end.directoryOffset + end.directorySize;
159
+ const entries = [];
160
+ let pos = end.directoryOffset;
161
+ for (let index = 0; index < end.totalEntries; index += 1) {
162
+ if (pos + CENTRAL_HEADER_SIZE > directoryEnd) {
163
+ throw new NotAZip(`central directory truncated at entry ${index}`);
164
+ }
165
+ if (dv.getUint32(pos, true) !== SIG_CENTRAL_HEADER) {
166
+ throw new NotAZip(`bad central directory signature at entry ${index}`);
167
+ }
168
+ const flags = dv.getUint16(pos + 8, true);
169
+ const method = dv.getUint16(pos + 10, true);
170
+ const crc = dv.getUint32(pos + 16, true);
171
+ const compressedSize = dv.getUint32(pos + 20, true);
172
+ const uncompressedSize = dv.getUint32(pos + 24, true);
173
+ const nameLength = dv.getUint16(pos + 28, true);
174
+ const extraLength = dv.getUint16(pos + 30, true);
175
+ const commentLength = dv.getUint16(pos + 32, true);
176
+ const externalAttr = dv.getUint32(pos + 38, true);
177
+ const localHeaderOffset = dv.getUint32(pos + 42, true);
178
+ const nameStart = pos + CENTRAL_HEADER_SIZE;
179
+ const extraStart = nameStart + nameLength;
180
+ const next = extraStart + extraLength + commentLength;
181
+ if (next > directoryEnd) {
182
+ throw new NotAZip(`central directory truncated at entry ${index}`);
183
+ }
184
+ const name = decodeName(buffer.subarray(nameStart, extraStart), flags);
185
+ const entry = {
186
+ name,
187
+ method,
188
+ flags,
189
+ crc32: crc,
190
+ compressedSize,
191
+ uncompressedSize,
192
+ localHeaderOffset,
193
+ externalAttr,
194
+ isDirectory: name.endsWith("/"),
195
+ };
196
+ assertSupported(entry, buffer.subarray(extraStart, extraStart + extraLength));
197
+ entries.push(entry);
198
+ pos = next;
199
+ }
200
+ return entries;
201
+ }
202
+ /**
203
+ * The uncompressed bytes of one entry, CRC-checked.
204
+ *
205
+ * The local header's name and extra lengths are read from the local header
206
+ * itself: a writer may store a different extra field there than in the central
207
+ * directory, so the directory's lengths cannot be used to find the data.
208
+ */
209
+ export function entryData(buffer, entry) {
210
+ assertSupported(entry, new Uint8Array(0));
211
+ const dv = view(buffer);
212
+ const pos = entry.localHeaderOffset;
213
+ if (pos + LOCAL_HEADER_SIZE > buffer.length) {
214
+ throw new NotAZip(`local header out of range: ${entry.name}`);
215
+ }
216
+ if (dv.getUint32(pos, true) !== SIG_LOCAL_HEADER) {
217
+ throw new NotAZip(`bad local header signature: ${entry.name}`);
218
+ }
219
+ const nameLength = dv.getUint16(pos + 26, true);
220
+ const extraLength = dv.getUint16(pos + 28, true);
221
+ const nameStart = pos + LOCAL_HEADER_SIZE;
222
+ const dataStart = nameStart + nameLength + extraLength;
223
+ const dataEnd = dataStart + entry.compressedSize;
224
+ if (dataEnd > buffer.length) {
225
+ throw new NotAZip(`entry data out of range: ${entry.name}`);
226
+ }
227
+ // Python's zipfile makes this check too: the two names must agree, or the
228
+ // directory is pointing at somebody else's bytes.
229
+ const localName = decodeName(buffer.subarray(nameStart, nameStart + nameLength), entry.flags);
230
+ if (localName !== entry.name) {
231
+ throw new NotAZip(`file name in directory '${entry.name}' and header '${localName}' differ`);
232
+ }
233
+ const compressed = buffer.subarray(dataStart, dataEnd);
234
+ let data;
235
+ if (entry.method === METHOD_STORED) {
236
+ if (entry.compressedSize !== entry.uncompressedSize) {
237
+ throw new NotAZip(`stored entry sizes disagree: ${entry.name}`);
238
+ }
239
+ data = compressed;
240
+ }
241
+ else {
242
+ try {
243
+ data = inflateRawSync(compressed);
244
+ }
245
+ catch (error) {
246
+ throw new NotAZip(`bad deflate data for file '${entry.name}': ${error.message}`);
247
+ }
248
+ }
249
+ if (data.length !== entry.uncompressedSize) {
250
+ throw new NotAZip(`size mismatch for file '${entry.name}': expected ${entry.uncompressedSize}, got ${data.length}`);
251
+ }
252
+ if (crc32(data) !== entry.crc32) {
253
+ throw new NotAZip(`bad CRC-32 for file '${entry.name}'`);
254
+ }
255
+ return data;
256
+ }
@@ -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,4 @@
1
+ /** Replace the platform separator with `/`. */
2
+ export declare function toPosix(p: string): string;
3
+ /** `os.path.relpath(target, base)` spelled with `/`. */
4
+ export declare function relativePosix(base: string, target: string): string;
@@ -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;