@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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry the other connections when a lookup exhausts one (flow-builder-sdk#744).
|
|
3
|
+
*
|
|
4
|
+
* Discovery lists the caller's own folder first and widens to the tenant only
|
|
5
|
+
* when that finds nothing — right for CHOOSING a connection, wrong for
|
|
6
|
+
* retrying one. When the own folder holds exactly one, the tenant's other
|
|
7
|
+
* connections are never looked at, so a `--resolve` that reads a collection to
|
|
8
|
+
* the end without a match reports a bad VALUE while the record sits in another
|
|
9
|
+
* folder's connection. Measured on two Jira connections where only the second
|
|
10
|
+
* carried project `TS`: the run failed on the first and never asked the second.
|
|
11
|
+
*
|
|
12
|
+
* Only an exhaustive miss demotes a connection. A transport or permission
|
|
13
|
+
* failure says nothing about whether the value exists; a scan stopped at the
|
|
14
|
+
* record cap did not reach the end; an unsearchable `by` is an authoring error
|
|
15
|
+
* that is the same on every connection. Those three pass through untouched.
|
|
16
|
+
*
|
|
17
|
+
* The "must not guess" rule is intact: {@link candidateConnections} lists and
|
|
18
|
+
* does not choose; what chooses is evidence. Exactly one candidate resolving
|
|
19
|
+
* everything wins. Two or more is a refusal listing them — the ids a lookup
|
|
20
|
+
* returns differ per connection, so "both work" is not a licence to pick. A
|
|
21
|
+
* connection the author pinned is never retried; the refusal names the
|
|
22
|
+
* alternatives instead.
|
|
23
|
+
*
|
|
24
|
+
* Ported from `_candidate_label`, `_probe_candidate` and `_retry_or_exit` in
|
|
25
|
+
* `prepare_connector.py`. The Python re-exec'd itself with `--connection-id
|
|
26
|
+
* <winner> --refresh-lookups`; here {@link retryOtherConnections} hands the
|
|
27
|
+
* winner back and `pipeline.ts` restores `bindings.json` and re-enters
|
|
28
|
+
* `prepareConnector` pinned to it — the same run, without a process boundary.
|
|
29
|
+
*/
|
|
30
|
+
import type { Reporter } from "../reporter.js";
|
|
31
|
+
import type { UipClient } from "../uip/client.js";
|
|
32
|
+
import { type ConnectionRow } from "./connections.js";
|
|
33
|
+
import type { ResolutionPlan } from "./ledger.js";
|
|
34
|
+
import { LookupExhausted, ResolutionRefusal } from "./resolve.js";
|
|
35
|
+
/**
|
|
36
|
+
* Why a candidate did not resolve everything. The two kinds are kept apart all
|
|
37
|
+
* the way up, because only the first is evidence about the connection.
|
|
38
|
+
*/
|
|
39
|
+
export type ProbeOutcome = {
|
|
40
|
+
kind: "exhausted";
|
|
41
|
+
error: LookupExhausted;
|
|
42
|
+
} | {
|
|
43
|
+
kind: "error";
|
|
44
|
+
error: ResolutionRefusal;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* `_probe_candidate`: resolve every requested lookup against one connection.
|
|
48
|
+
*
|
|
49
|
+
* `undefined` when all of them resolve, else the FIRST reason they did not —
|
|
50
|
+
* the probe stops there, since a later lookup may depend on the one that just
|
|
51
|
+
* failed and every further call would be spent on a connection already known
|
|
52
|
+
* not to serve this run.
|
|
53
|
+
*/
|
|
54
|
+
export declare function probeCandidate(client: UipClient, connectorKey: string, plan: ResolutionPlan, connectionId: string): Promise<ProbeOutcome | undefined>;
|
|
55
|
+
/** What the retry needs to know about the run that just exhausted a connection. */
|
|
56
|
+
export interface RetryContext {
|
|
57
|
+
client: UipClient;
|
|
58
|
+
reporter: Reporter;
|
|
59
|
+
connectorKey: string;
|
|
60
|
+
/** The connection the exhausted lookup read. */
|
|
61
|
+
connectionId: string;
|
|
62
|
+
/** `--connection <name>`, passed on to the candidate listing as the Python did. */
|
|
63
|
+
connection?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Whether the CALLER chose the connection (`--connection-id` / `--connection`).
|
|
66
|
+
* An explicit choice is never second-guessed: the retry is for a connection
|
|
67
|
+
* this command picked, not for one the author named.
|
|
68
|
+
*/
|
|
69
|
+
pinned: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `_retry_or_exit`: the ONE other connection that resolves every requested
|
|
73
|
+
* lookup, or throw a {@link PrepareRefusal} explaining what was tried.
|
|
74
|
+
*
|
|
75
|
+
* Returns only on the path the Python re-exec'd on; every other path is a
|
|
76
|
+
* refusal carrying the Python's `sys.exit` text. The caller undoes the bindings
|
|
77
|
+
* this run wrote and re-runs pinned to the winner — re-running rather than
|
|
78
|
+
* switching in place is deliberate: the overlay and its lookup specs are
|
|
79
|
+
* described THROUGH a connection (`lookupSpecsFromOverlay`), so adopting a
|
|
80
|
+
* different connection after the describe would leave the overlay describing
|
|
81
|
+
* one connection while `bindings.json` names another.
|
|
82
|
+
*/
|
|
83
|
+
export declare function retryOtherConnections(ctx: RetryContext, plan: ResolutionPlan, first: LookupExhausted): Promise<ConnectionRow>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ConnectionRefusal, candidateConnections, candidateListing } from "./connections.js";
|
|
2
|
+
import { PrepareRefusal, candidateErrorLine, candidateExhaustedLine, candidateLabel, candidateListingFailedMessage, candidateResolvesAllLine, noConnectionResolvesMessage, onlyConnectionMessage, pinnedConnectionExhaustedMessage, severalConnectionsResolveMessage, tryingOtherConnectionsLine, } from "./messages.js";
|
|
3
|
+
import { LookupExhausted, ResolutionRefusal, resolveOne } from "./resolve.js";
|
|
4
|
+
import { pyGet, pyStrAny } from "./support.js";
|
|
5
|
+
/**
|
|
6
|
+
* `_probe_candidate`: resolve every requested lookup against one connection.
|
|
7
|
+
*
|
|
8
|
+
* `undefined` when all of them resolve, else the FIRST reason they did not —
|
|
9
|
+
* the probe stops there, since a later lookup may depend on the one that just
|
|
10
|
+
* failed and every further call would be spent on a connection already known
|
|
11
|
+
* not to serve this run.
|
|
12
|
+
*/
|
|
13
|
+
export async function probeCandidate(client, connectorKey, plan, connectionId) {
|
|
14
|
+
const resolved = new Map();
|
|
15
|
+
for (const [field, by, value] of plan.ordered) {
|
|
16
|
+
try {
|
|
17
|
+
resolved.set(field, await resolveOne(client, connectorKey, plan.specs.get(field), field, by, value, connectionId, resolved));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
// `capped` is classified with the broken calls rather than with the
|
|
21
|
+
// exhaustive misses, for the same reason it does not demote the first
|
|
22
|
+
// connection: the scan stopped at the record cap without reaching the
|
|
23
|
+
// end, so "N record(s) scanned, no match" would state an absence
|
|
24
|
+
// nothing established. Reported as its own message instead, which says
|
|
25
|
+
// the cap stopped it and that the value may be further in.
|
|
26
|
+
if (error instanceof LookupExhausted && !error.capped)
|
|
27
|
+
return { kind: "exhausted", error };
|
|
28
|
+
if (error instanceof ResolutionRefusal)
|
|
29
|
+
return { kind: "error", error };
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `_retry_or_exit`: the ONE other connection that resolves every requested
|
|
37
|
+
* lookup, or throw a {@link PrepareRefusal} explaining what was tried.
|
|
38
|
+
*
|
|
39
|
+
* Returns only on the path the Python re-exec'd on; every other path is a
|
|
40
|
+
* refusal carrying the Python's `sys.exit` text. The caller undoes the bindings
|
|
41
|
+
* this run wrote and re-runs pinned to the winner — re-running rather than
|
|
42
|
+
* switching in place is deliberate: the overlay and its lookup specs are
|
|
43
|
+
* described THROUGH a connection (`lookupSpecsFromOverlay`), so adopting a
|
|
44
|
+
* different connection after the describe would leave the overlay describing
|
|
45
|
+
* one connection while `bindings.json` names another.
|
|
46
|
+
*/
|
|
47
|
+
export async function retryOtherConnections(ctx, plan, first) {
|
|
48
|
+
const sameId = (row) => pyStrAny(pyGet(row, "Id", null)) === pyStrAny(ctx.connectionId);
|
|
49
|
+
if (ctx.pinned) {
|
|
50
|
+
// The alternatives are still named, because the whole cost of this bug
|
|
51
|
+
// was not knowing they existed. INTENTIONAL DIVERGENCE: the Python did not
|
|
52
|
+
// guard this listing, so a listing failure here surfaced as a traceback
|
|
53
|
+
// that swallowed the lookup failure the author asked about; it is reported
|
|
54
|
+
// the way the unpinned branch reports it instead.
|
|
55
|
+
let others;
|
|
56
|
+
try {
|
|
57
|
+
others = (await candidateConnections(ctx.client, ctx.connectorKey)).filter((row) => !sameId(row));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (!(error instanceof ConnectionRefusal))
|
|
61
|
+
throw error;
|
|
62
|
+
throw new PrepareRefusal(candidateListingFailedMessage(first.message, error), "LOOKUP_EXHAUSTED_PINNED");
|
|
63
|
+
}
|
|
64
|
+
throw new PrepareRefusal(pinnedConnectionExhaustedMessage(first.message, ctx.connectorKey, others.length, candidateListing(others)), "LOOKUP_EXHAUSTED_PINNED");
|
|
65
|
+
}
|
|
66
|
+
let candidates;
|
|
67
|
+
try {
|
|
68
|
+
candidates = await candidateConnections(ctx.client, ctx.connectorKey, ctx.connection);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!(error instanceof ConnectionRefusal))
|
|
72
|
+
throw error;
|
|
73
|
+
// The listing itself failed; report the original lookup failure, which is
|
|
74
|
+
// the thing the author asked about.
|
|
75
|
+
throw new PrepareRefusal(candidateListingFailedMessage(first.message, error), "LOOKUP_EXHAUSTED");
|
|
76
|
+
}
|
|
77
|
+
const others = candidates.filter((row) => !sameId(row));
|
|
78
|
+
if (others.length === 0) {
|
|
79
|
+
throw new PrepareRefusal(onlyConnectionMessage(first.message, ctx.connectorKey), "LOOKUP_EXHAUSTED_ONLY_CONNECTION");
|
|
80
|
+
}
|
|
81
|
+
ctx.reporter.info(tryingOtherConnectionsLine(first.field, others.length, ctx.connectorKey));
|
|
82
|
+
const winners = [];
|
|
83
|
+
const report = [];
|
|
84
|
+
for (const row of others) {
|
|
85
|
+
const outcome = await probeCandidate(ctx.client, ctx.connectorKey, plan, pyStrAny(pyGet(row, "Id", null)));
|
|
86
|
+
const label = candidateLabel(row);
|
|
87
|
+
if (outcome === undefined) {
|
|
88
|
+
winners.push(row);
|
|
89
|
+
report.push(candidateResolvesAllLine(label, plan.ordered.length));
|
|
90
|
+
}
|
|
91
|
+
else if (outcome.kind === "exhausted") {
|
|
92
|
+
report.push(candidateExhaustedLine(label, outcome.error.field, outcome.error.scanned));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
report.push(candidateErrorLine(label, outcome.error));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (winners.length === 1)
|
|
99
|
+
return winners[0];
|
|
100
|
+
if (winners.length > 1) {
|
|
101
|
+
throw new PrepareRefusal(severalConnectionsResolveMessage(winners.length, ctx.connectorKey, candidateListing(winners)), "LOOKUP_EXHAUSTED_AMBIGUOUS");
|
|
102
|
+
}
|
|
103
|
+
// Nothing resolved anywhere. Report every candidate and what it held, the
|
|
104
|
+
// one connection first, so the reader can see the value really is absent
|
|
105
|
+
// rather than guess which connection was asked.
|
|
106
|
+
const original = candidates.find(sameId) ?? { Name: ctx.connectionId, Folder: "?" };
|
|
107
|
+
const lines = [candidateExhaustedLine(candidateLabel(original), first.field, first.scanned), ...report];
|
|
108
|
+
throw new PrepareRefusal(noConnectionResolvesMessage(ctx.connectorKey, first, lines, ctx.connectionId), "LOOKUP_EXHAUSTED_EVERYWHERE");
|
|
109
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `import … from "<specifier>"` — the specifier is group 3, quotes preserved.
|
|
3
|
+
* Anchored to a real import statement on purpose (see the module comment).
|
|
4
|
+
*/
|
|
5
|
+
export declare const IMPORT_PATTERN: RegExp;
|
|
6
|
+
/** Where a `.flow.ts` will not be: generated output and installed packages. */
|
|
7
|
+
export declare const SOURCE_SKIP_DIRS: ReadonlySet<string>;
|
|
8
|
+
/**
|
|
9
|
+
* `Path(p).resolve()` with `strict=False`: absolute, `..` collapsed, and
|
|
10
|
+
* symlinks followed as far as the path exists. Both sides of every comparison
|
|
11
|
+
* go through this, so `/var/...` and `/private/var/...` name one file.
|
|
12
|
+
*/
|
|
13
|
+
export declare function canonicalPath(p: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* `_resolves_to`: does `specifier`, read from `source`, name the file `target`?
|
|
16
|
+
* A bare package specifier never does — it is not our overlay.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolvesTo(specifier: string, source: string, target: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* `_sources_importing`: every `*.flow.ts` under `root` whose import resolves
|
|
21
|
+
* to `baked`. Selection is by CONTENT rather than "the single flow in this
|
|
22
|
+
* directory": a workspace with three flows, one of which uses this connector,
|
|
23
|
+
* has one correct answer and this finds it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sourcesImporting(baked: string, root: string): string[];
|
|
26
|
+
/**
|
|
27
|
+
* `_repoint_import`: point `source`'s import at the prepared descriptor and
|
|
28
|
+
* return the new specifier, or `undefined` when nothing changed.
|
|
29
|
+
*
|
|
30
|
+
* Only the quoted path changes. The rest of the statement — the named
|
|
31
|
+
* bindings, an `as` alias, the quote style — is carried through untouched,
|
|
32
|
+
* which is the whole difference between this and the find-and-replace it
|
|
33
|
+
* replaces.
|
|
34
|
+
*/
|
|
35
|
+
export declare function repointImport(source: string, baked: string, prepared: string): string | undefined;
|
|
36
|
+
export interface RepointOptions {
|
|
37
|
+
/** The overlay directory (`--out`), relative to `cwd` or absolute. */
|
|
38
|
+
out: string;
|
|
39
|
+
connectorKey: string;
|
|
40
|
+
/** The project root the Python read as `Path.cwd()`. Defaults to the process's. */
|
|
41
|
+
cwd?: string;
|
|
42
|
+
/** `--source`: the one `.flow.ts` to repoint, settling any ambiguity. */
|
|
43
|
+
source?: string | null;
|
|
44
|
+
/** `--no-source-rewrite`: print the import line instead of editing the source. */
|
|
45
|
+
noSourceRewrite?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `_repoint_source`: repoint the author's import, or say why it was left
|
|
49
|
+
* alone. The lines come back unprefixed; the caller prints each as
|
|
50
|
+
* `prepare-connector: <line>`. An empty list means nothing to report — no
|
|
51
|
+
* prepared module, no candidate, or a second run that finds no import left to
|
|
52
|
+
* change.
|
|
53
|
+
*/
|
|
54
|
+
export declare function repointSource(options: RepointOptions): string[];
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repoint the author's `import … from './connectors/<key>.ts'` at the prepared
|
|
3
|
+
* descriptor, or explain why it was left alone.
|
|
4
|
+
*
|
|
5
|
+
* `prepare` already writes `bindings.json`, the overlay descriptor and
|
|
6
|
+
* `resolutions.json`; the one thing it did not do was update the import naming
|
|
7
|
+
* the descriptor it had just replaced. That left a mechanical edit — one
|
|
8
|
+
* specifier, one file, exactly one right answer — to the caller, who has
|
|
9
|
+
* strictly less information than this command does. Measured cost: an agent
|
|
10
|
+
* reaches for `sed 's|./connectors/|./connectors-local/|'`, which repoints
|
|
11
|
+
* connectors that were never prepared and breaks the flow with `Cannot find
|
|
12
|
+
* module` (#704).
|
|
13
|
+
*
|
|
14
|
+
* Imports are matched as RESOLVED PATHS, never as text, and only inside a real
|
|
15
|
+
* import STATEMENT — the same text in a comment, or in the import of a
|
|
16
|
+
* connector this run did not prepare, is left exactly as it was. Ambiguity
|
|
17
|
+
* keeps today's behaviour: zero candidates or several, and nothing is written.
|
|
18
|
+
*
|
|
19
|
+
* Ported from `_IMPORT_RE`, `_resolves_to`, `_sources_importing`,
|
|
20
|
+
* `_repoint_import` and `_repoint_source` in `prepare_connector.py`.
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
24
|
+
import { compareCodePoints } from "../compat/compare.js";
|
|
25
|
+
import { relativePosix } from "../compat/paths.js";
|
|
26
|
+
import { repointedLine, severalSourcesLine, sourceNotAFileLine } from "./messages.js";
|
|
27
|
+
/**
|
|
28
|
+
* `import … from "<specifier>"` — the specifier is group 3, quotes preserved.
|
|
29
|
+
* Anchored to a real import statement on purpose (see the module comment).
|
|
30
|
+
*/
|
|
31
|
+
export const IMPORT_PATTERN = /(^\s*import\s[^;'"]*?\sfrom\s*)(['"])([^'"]+)(\2)/gm;
|
|
32
|
+
/** Where a `.flow.ts` will not be: generated output and installed packages. */
|
|
33
|
+
export const SOURCE_SKIP_DIRS = new Set(["node_modules", "connectors", "connectors-local", ".git"]);
|
|
34
|
+
/**
|
|
35
|
+
* `Path(p).resolve()` with `strict=False`: absolute, `..` collapsed, and
|
|
36
|
+
* symlinks followed as far as the path exists. Both sides of every comparison
|
|
37
|
+
* go through this, so `/var/...` and `/private/var/...` name one file.
|
|
38
|
+
*/
|
|
39
|
+
export function canonicalPath(p) {
|
|
40
|
+
const absolute = resolve(p);
|
|
41
|
+
try {
|
|
42
|
+
return realpathSync.native(absolute);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
const parent = dirname(absolute);
|
|
46
|
+
if (parent === absolute)
|
|
47
|
+
return absolute;
|
|
48
|
+
return join(canonicalPath(parent), basename(absolute));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `_resolves_to`: does `specifier`, read from `source`, name the file `target`?
|
|
53
|
+
* A bare package specifier never does — it is not our overlay.
|
|
54
|
+
*/
|
|
55
|
+
export function resolvesTo(specifier, source, target) {
|
|
56
|
+
if (!specifier.startsWith("."))
|
|
57
|
+
return false;
|
|
58
|
+
return canonicalPath(resolve(dirname(source), specifier)) === canonicalPath(target);
|
|
59
|
+
}
|
|
60
|
+
/** `sorted(Path…)`: paths compare part by part, each part by code point. */
|
|
61
|
+
function comparePaths(a, b) {
|
|
62
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
63
|
+
const c = compareCodePoints(a[i], b[i]);
|
|
64
|
+
if (c !== 0)
|
|
65
|
+
return c;
|
|
66
|
+
}
|
|
67
|
+
return a.length - b.length;
|
|
68
|
+
}
|
|
69
|
+
/** Every `*.flow.ts` file under `root`, sorted, skipping the directories a source is never in. */
|
|
70
|
+
function flowSources(root) {
|
|
71
|
+
const found = [];
|
|
72
|
+
const walk = (dir, parts) => {
|
|
73
|
+
let children;
|
|
74
|
+
try {
|
|
75
|
+
children = readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const child of children) {
|
|
81
|
+
if (child.isDirectory()) {
|
|
82
|
+
if (!SOURCE_SKIP_DIRS.has(child.name))
|
|
83
|
+
walk(join(dir, child.name), [...parts, child.name]);
|
|
84
|
+
}
|
|
85
|
+
else if (child.isFile() && child.name.endsWith(".flow.ts")) {
|
|
86
|
+
found.push([...parts, child.name]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
walk(root, []);
|
|
91
|
+
found.sort(comparePaths);
|
|
92
|
+
return found.map((parts) => join(root, ...parts));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `_sources_importing`: every `*.flow.ts` under `root` whose import resolves
|
|
96
|
+
* to `baked`. Selection is by CONTENT rather than "the single flow in this
|
|
97
|
+
* directory": a workspace with three flows, one of which uses this connector,
|
|
98
|
+
* has one correct answer and this finds it.
|
|
99
|
+
*/
|
|
100
|
+
export function sourcesImporting(baked, root) {
|
|
101
|
+
const found = [];
|
|
102
|
+
for (const path of flowSources(root)) {
|
|
103
|
+
let text;
|
|
104
|
+
try {
|
|
105
|
+
text = readFileSync(path, "utf8");
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
for (const match of text.matchAll(IMPORT_PATTERN)) {
|
|
111
|
+
if (resolvesTo(match[3], path, baked)) {
|
|
112
|
+
found.push(path);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return found;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* `_repoint_import`: point `source`'s import at the prepared descriptor and
|
|
121
|
+
* return the new specifier, or `undefined` when nothing changed.
|
|
122
|
+
*
|
|
123
|
+
* Only the quoted path changes. The rest of the statement — the named
|
|
124
|
+
* bindings, an `as` alias, the quote style — is carried through untouched,
|
|
125
|
+
* which is the whole difference between this and the find-and-replace it
|
|
126
|
+
* replaces.
|
|
127
|
+
*/
|
|
128
|
+
export function repointImport(source, baked, prepared) {
|
|
129
|
+
let text;
|
|
130
|
+
try {
|
|
131
|
+
text = readFileSync(source, "utf8");
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
let specifier = relativePosix(dirname(source), prepared);
|
|
137
|
+
if (!specifier.startsWith("."))
|
|
138
|
+
specifier = `./${specifier}`;
|
|
139
|
+
let changed = false;
|
|
140
|
+
const rewritten = text.replace(IMPORT_PATTERN, (whole, head, quote, spec, tail) => {
|
|
141
|
+
if (!resolvesTo(spec, source, baked))
|
|
142
|
+
return whole;
|
|
143
|
+
changed = true;
|
|
144
|
+
return `${head}${quote}${specifier}${tail}`;
|
|
145
|
+
});
|
|
146
|
+
if (!changed || rewritten === text)
|
|
147
|
+
return undefined;
|
|
148
|
+
writeFileSync(source, rewritten, "utf8");
|
|
149
|
+
return specifier;
|
|
150
|
+
}
|
|
151
|
+
function isFile(path) {
|
|
152
|
+
try {
|
|
153
|
+
return statSync(path).isFile();
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* `_repoint_source`: repoint the author's import, or say why it was left
|
|
161
|
+
* alone. The lines come back unprefixed; the caller prints each as
|
|
162
|
+
* `prepare-connector: <line>`. An empty list means nothing to report — no
|
|
163
|
+
* prepared module, no candidate, or a second run that finds no import left to
|
|
164
|
+
* change.
|
|
165
|
+
*/
|
|
166
|
+
export function repointSource(options) {
|
|
167
|
+
const cwd = options.cwd ?? process.cwd();
|
|
168
|
+
const prepared = canonicalPath(resolve(cwd, options.out, `${options.connectorKey}.ts`));
|
|
169
|
+
if (options.noSourceRewrite || !isFile(prepared))
|
|
170
|
+
return [];
|
|
171
|
+
const root = canonicalPath(cwd);
|
|
172
|
+
const baked = canonicalPath(join(root, "connectors", `${options.connectorKey}.ts`));
|
|
173
|
+
let candidates;
|
|
174
|
+
if (options.source) {
|
|
175
|
+
candidates = [canonicalPath(resolve(cwd, options.source))];
|
|
176
|
+
if (!isFile(candidates[0]))
|
|
177
|
+
return [sourceNotAFileLine(options.source)];
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
candidates = sourcesImporting(baked, root);
|
|
181
|
+
}
|
|
182
|
+
if (candidates.length === 0)
|
|
183
|
+
return [];
|
|
184
|
+
if (candidates.length > 1) {
|
|
185
|
+
return [severalSourcesLine(candidates.map((c) => relativePosix(root, c)))];
|
|
186
|
+
}
|
|
187
|
+
const source = candidates[0];
|
|
188
|
+
const specifier = repointImport(source, baked, prepared);
|
|
189
|
+
if (specifier === undefined)
|
|
190
|
+
return [];
|
|
191
|
+
return [repointedLine(relativePosix(root, source), specifier)];
|
|
192
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
2
|
+
/** `bool(value)` for JSON-shaped values: `[]`, `{}`, `""`, `0`, `None` are false. */
|
|
3
|
+
export declare function pyTruthy(value: unknown): boolean;
|
|
4
|
+
/** `a or b`. */
|
|
5
|
+
export declare function pyOr<A, B>(a: A, b: B): A | B;
|
|
6
|
+
/** `mapping.get(key, default)`: the value when the key is PRESENT (even as `null`), else the default. */
|
|
7
|
+
export declare function pyGet<D>(mapping: Record<string, unknown>, key: string, dflt: D): unknown | D;
|
|
8
|
+
/** `type(value).__name__` for JSON-shaped values. */
|
|
9
|
+
export declare function pyTypeName(value: unknown): string;
|
|
10
|
+
/** `repr(str)`: single quotes unless the text has a `'` and no `"`; escapes as CPython does. */
|
|
11
|
+
export declare function pyReprString(s: string): string;
|
|
12
|
+
/** `repr(value)` for JSON-shaped values (`'x'`, `True`, `None`, `[1, 'a']`, `{'k': 1}`). */
|
|
13
|
+
export declare function pyRepr(value: unknown): string;
|
|
14
|
+
/** `str(value)` for any JSON-shaped value: `pyStr` for scalars, `repr` for containers, as Python does. */
|
|
15
|
+
export declare function pyStrAny(value: unknown): string;
|
|
16
|
+
/** `str.split()` with no argument: runs of whitespace, empties dropped. */
|
|
17
|
+
export declare function pySplitWhitespace(s: string): string[];
|
|
18
|
+
/**
|
|
19
|
+
* `os.path.join(a, b)` for a relative `b`: `a` + separator + `b`, with NO
|
|
20
|
+
* normalisation — `./connectors-local` stays `./connectors-local`.
|
|
21
|
+
*
|
|
22
|
+
* For the lines `prepare` prints, not for opening files. The Python printed
|
|
23
|
+
* every path as the author typed it (`args.out`, `args.bindings`), and
|
|
24
|
+
* `node:path`'s `join` would rewrite `./connectors-local/resolutions.json` as
|
|
25
|
+
* `connectors-local/resolutions.json`; the pipeline resolves a real path for
|
|
26
|
+
* the file system separately.
|
|
27
|
+
*/
|
|
28
|
+
export declare function pyPathJoin(a: string, b: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* `str(pathlib.Path(p))`: redundant separators and `.` components collapse,
|
|
31
|
+
* a trailing separator goes, `..` and a leading `/` stay, and an empty path
|
|
32
|
+
* is `.`. The descriptor generator printed its output directory this way.
|
|
33
|
+
*/
|
|
34
|
+
export declare function pyPathlibStr(p: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Why `stdout` is not the JSON envelope a caller wanted — the text the Python
|
|
37
|
+
* put after "was not JSON:". Python printed `json.JSONDecodeError`'s message
|
|
38
|
+
* (`Expecting value: line 1 column 1 (char 0)`); this prints V8's, or names the
|
|
39
|
+
* type when the text parsed but is not an object.
|
|
40
|
+
*/
|
|
41
|
+
export declare function notJsonReason(stdout: string): string;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python value semantics the prepare helpers lean on, beyond `compat/pystr`.
|
|
3
|
+
*
|
|
4
|
+
* `_connections`, `_objects` and `_resolve` call `str()`, `repr()` and `bool()`
|
|
5
|
+
* on whatever a `uip` listing handed them and format the result into a refusal
|
|
6
|
+
* an agent reads. `pyStr` refuses containers on purpose (the generators never
|
|
7
|
+
* stringify one deliberately); here a container is a fact about the tenant's
|
|
8
|
+
* data and must render the way Python rendered it, not crash the refusal that
|
|
9
|
+
* was about to explain it. Everything in this module is private to `prepare/`.
|
|
10
|
+
*/
|
|
11
|
+
import { sep } from "node:path";
|
|
12
|
+
import { pyStr } from "../compat/pystr.js";
|
|
13
|
+
export function isPlainObject(value) {
|
|
14
|
+
return (typeof value === "object" &&
|
|
15
|
+
value !== null &&
|
|
16
|
+
!Array.isArray(value) &&
|
|
17
|
+
!(value instanceof Map) &&
|
|
18
|
+
!(value instanceof Set));
|
|
19
|
+
}
|
|
20
|
+
/** `bool(value)` for JSON-shaped values: `[]`, `{}`, `""`, `0`, `None` are false. */
|
|
21
|
+
export function pyTruthy(value) {
|
|
22
|
+
if (value === null || value === undefined)
|
|
23
|
+
return false;
|
|
24
|
+
if (typeof value === "boolean")
|
|
25
|
+
return value;
|
|
26
|
+
if (typeof value === "number")
|
|
27
|
+
return value !== 0;
|
|
28
|
+
if (typeof value === "string")
|
|
29
|
+
return value.length > 0;
|
|
30
|
+
if (Array.isArray(value))
|
|
31
|
+
return value.length > 0;
|
|
32
|
+
if (value instanceof Map || value instanceof Set)
|
|
33
|
+
return value.size > 0;
|
|
34
|
+
if (typeof value === "object")
|
|
35
|
+
return Object.keys(value).length > 0;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/** `a or b`. */
|
|
39
|
+
export function pyOr(a, b) {
|
|
40
|
+
return pyTruthy(a) ? a : b;
|
|
41
|
+
}
|
|
42
|
+
/** `mapping.get(key, default)`: the value when the key is PRESENT (even as `null`), else the default. */
|
|
43
|
+
export function pyGet(mapping, key, dflt) {
|
|
44
|
+
return Object.prototype.hasOwnProperty.call(mapping, key) ? mapping[key] : dflt;
|
|
45
|
+
}
|
|
46
|
+
/** `type(value).__name__` for JSON-shaped values. */
|
|
47
|
+
export function pyTypeName(value) {
|
|
48
|
+
if (value === null || value === undefined)
|
|
49
|
+
return "NoneType";
|
|
50
|
+
if (Array.isArray(value))
|
|
51
|
+
return "list";
|
|
52
|
+
switch (typeof value) {
|
|
53
|
+
case "string":
|
|
54
|
+
return "str";
|
|
55
|
+
case "boolean":
|
|
56
|
+
return "bool";
|
|
57
|
+
case "number":
|
|
58
|
+
return Number.isInteger(value) ? "int" : "float";
|
|
59
|
+
case "bigint":
|
|
60
|
+
return "int";
|
|
61
|
+
case "object":
|
|
62
|
+
return "dict";
|
|
63
|
+
default:
|
|
64
|
+
return typeof value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `str.isprintable()` is false for these categories (and true for the ASCII
|
|
69
|
+
* space, which is in Zs). `repr` escapes exactly the non-printable ones.
|
|
70
|
+
*/
|
|
71
|
+
const NOT_PRINTABLE = /[\p{Cc}\p{Cf}\p{Cs}\p{Co}\p{Cn}\p{Zl}\p{Zp}\p{Zs}]/u;
|
|
72
|
+
function hex(codePoint, width) {
|
|
73
|
+
return codePoint.toString(16).padStart(width, "0");
|
|
74
|
+
}
|
|
75
|
+
/** `repr(str)`: single quotes unless the text has a `'` and no `"`; escapes as CPython does. */
|
|
76
|
+
export function pyReprString(s) {
|
|
77
|
+
const quote = s.includes("'") && !s.includes('"') ? '"' : "'";
|
|
78
|
+
let out = quote;
|
|
79
|
+
for (const ch of s) {
|
|
80
|
+
if (ch === quote || ch === "\\")
|
|
81
|
+
out += `\\${ch}`;
|
|
82
|
+
else if (ch === "\n")
|
|
83
|
+
out += "\\n";
|
|
84
|
+
else if (ch === "\r")
|
|
85
|
+
out += "\\r";
|
|
86
|
+
else if (ch === "\t")
|
|
87
|
+
out += "\\t";
|
|
88
|
+
else if (ch !== " " && NOT_PRINTABLE.test(ch)) {
|
|
89
|
+
const codePoint = ch.codePointAt(0);
|
|
90
|
+
out +=
|
|
91
|
+
codePoint < 0x100
|
|
92
|
+
? `\\x${hex(codePoint, 2)}`
|
|
93
|
+
: codePoint < 0x10000
|
|
94
|
+
? `\\u${hex(codePoint, 4)}`
|
|
95
|
+
: `\\U${hex(codePoint, 8)}`;
|
|
96
|
+
}
|
|
97
|
+
else
|
|
98
|
+
out += ch;
|
|
99
|
+
}
|
|
100
|
+
return out + quote;
|
|
101
|
+
}
|
|
102
|
+
/** `repr(value)` for JSON-shaped values (`'x'`, `True`, `None`, `[1, 'a']`, `{'k': 1}`). */
|
|
103
|
+
export function pyRepr(value) {
|
|
104
|
+
if (typeof value === "string")
|
|
105
|
+
return pyReprString(value);
|
|
106
|
+
if (Array.isArray(value))
|
|
107
|
+
return `[${value.map(pyRepr).join(", ")}]`;
|
|
108
|
+
if (value instanceof Map) {
|
|
109
|
+
return `{${[...value.entries()].map(([k, v]) => `${pyRepr(k)}: ${pyRepr(v)}`).join(", ")}}`;
|
|
110
|
+
}
|
|
111
|
+
if (isPlainObject(value)) {
|
|
112
|
+
return `{${Object.entries(value)
|
|
113
|
+
.map(([k, v]) => `${pyReprString(k)}: ${pyRepr(v)}`)
|
|
114
|
+
.join(", ")}}`;
|
|
115
|
+
}
|
|
116
|
+
return pyStr(value);
|
|
117
|
+
}
|
|
118
|
+
/** `str(value)` for any JSON-shaped value: `pyStr` for scalars, `repr` for containers, as Python does. */
|
|
119
|
+
export function pyStrAny(value) {
|
|
120
|
+
if (typeof value === "string")
|
|
121
|
+
return value;
|
|
122
|
+
if (Array.isArray(value) || value instanceof Map || isPlainObject(value))
|
|
123
|
+
return pyRepr(value);
|
|
124
|
+
return pyStr(value);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The characters `str.isspace()` is true for — the set `str.split()` splits
|
|
128
|
+
* on. Same table as `compat/pystr`'s `PY_SPACE`, which is not exported.
|
|
129
|
+
*/
|
|
130
|
+
const PY_WHITESPACE_RUN = /[\t\n\r\u000b\u000c\u001c\u001d\u001e\u001f \u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+/u;
|
|
131
|
+
/** `str.split()` with no argument: runs of whitespace, empties dropped. */
|
|
132
|
+
export function pySplitWhitespace(s) {
|
|
133
|
+
return s.split(PY_WHITESPACE_RUN).filter((token) => token !== "");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* `os.path.join(a, b)` for a relative `b`: `a` + separator + `b`, with NO
|
|
137
|
+
* normalisation — `./connectors-local` stays `./connectors-local`.
|
|
138
|
+
*
|
|
139
|
+
* For the lines `prepare` prints, not for opening files. The Python printed
|
|
140
|
+
* every path as the author typed it (`args.out`, `args.bindings`), and
|
|
141
|
+
* `node:path`'s `join` would rewrite `./connectors-local/resolutions.json` as
|
|
142
|
+
* `connectors-local/resolutions.json`; the pipeline resolves a real path for
|
|
143
|
+
* the file system separately.
|
|
144
|
+
*/
|
|
145
|
+
export function pyPathJoin(a, b) {
|
|
146
|
+
if (a === "")
|
|
147
|
+
return b;
|
|
148
|
+
return a.endsWith("/") || a.endsWith(sep) ? `${a}${b}` : `${a}${sep}${b}`;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* `str(pathlib.Path(p))`: redundant separators and `.` components collapse,
|
|
152
|
+
* a trailing separator goes, `..` and a leading `/` stay, and an empty path
|
|
153
|
+
* is `.`. The descriptor generator printed its output directory this way.
|
|
154
|
+
*/
|
|
155
|
+
export function pyPathlibStr(p) {
|
|
156
|
+
const absolute = p.startsWith("/");
|
|
157
|
+
const parts = p.split(/[\\/]+/).filter((part) => part !== "" && part !== ".");
|
|
158
|
+
const body = parts.join(sep);
|
|
159
|
+
if (absolute)
|
|
160
|
+
return `${sep}${body}`;
|
|
161
|
+
return body || ".";
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Why `stdout` is not the JSON envelope a caller wanted — the text the Python
|
|
165
|
+
* put after "was not JSON:". Python printed `json.JSONDecodeError`'s message
|
|
166
|
+
* (`Expecting value: line 1 column 1 (char 0)`); this prints V8's, or names the
|
|
167
|
+
* type when the text parsed but is not an object.
|
|
168
|
+
*/
|
|
169
|
+
export function notJsonReason(stdout) {
|
|
170
|
+
let parsed;
|
|
171
|
+
try {
|
|
172
|
+
parsed = JSON.parse(stdout);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return error instanceof Error ? error.message : String(error);
|
|
176
|
+
}
|
|
177
|
+
return `expected a JSON object, found ${pyTypeName(parsed)}`;
|
|
178
|
+
}
|