@flow-as-code/core 0.1.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/LICENSE +202 -0
- package/README.md +30 -0
- package/SPEC.md +92 -0
- package/dist/actions.d.ts +87 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +204 -0
- package/dist/actions.js.map +1 -0
- package/dist/aws.d.ts +27 -0
- package/dist/aws.d.ts.map +1 -0
- package/dist/aws.js +32 -0
- package/dist/aws.js.map +1 -0
- package/dist/blocks.d.ts +287 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/blocks.js +352 -0
- package/dist/blocks.js.map +1 -0
- package/dist/codegen.d.ts +21 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +1027 -0
- package/dist/codegen.js.map +1 -0
- package/dist/export.d.ts +256 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +799 -0
- package/dist/export.js.map +1 -0
- package/dist/flow.d.ts +41 -0
- package/dist/flow.d.ts.map +1 -0
- package/dist/flow.js +85 -0
- package/dist/flow.js.map +1 -0
- package/dist/flowdoc.d.ts +111 -0
- package/dist/flowdoc.d.ts.map +1 -0
- package/dist/flowdoc.js +139 -0
- package/dist/flowdoc.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +11 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +49 -0
- package/dist/layout.js.map +1 -0
- package/dist/lint/engine.d.ts +16 -0
- package/dist/lint/engine.d.ts.map +1 -0
- package/dist/lint/engine.js +37 -0
- package/dist/lint/engine.js.map +1 -0
- package/dist/lint/graph.d.ts +34 -0
- package/dist/lint/graph.d.ts.map +1 -0
- package/dist/lint/graph.js +79 -0
- package/dist/lint/graph.js.map +1 -0
- package/dist/lint/index.d.ts +7 -0
- package/dist/lint/index.d.ts.map +1 -0
- package/dist/lint/index.js +5 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/lint/reporters.d.ts +6 -0
- package/dist/lint/reporters.d.ts.map +1 -0
- package/dist/lint/reporters.js +30 -0
- package/dist/lint/reporters.js.map +1 -0
- package/dist/lint/rules/action-allowed-in-flow-type.d.ts +11 -0
- package/dist/lint/rules/action-allowed-in-flow-type.d.ts.map +1 -0
- package/dist/lint/rules/action-allowed-in-flow-type.js +32 -0
- package/dist/lint/rules/action-allowed-in-flow-type.js.map +1 -0
- package/dist/lint/rules/action-count.d.ts +16 -0
- package/dist/lint/rules/action-count.d.ts.map +1 -0
- package/dist/lint/rules/action-count.js +32 -0
- package/dist/lint/rules/action-count.js.map +1 -0
- package/dist/lint/rules/error-branches.d.ts +10 -0
- package/dist/lint/rules/error-branches.d.ts.map +1 -0
- package/dist/lint/rules/error-branches.js +39 -0
- package/dist/lint/rules/error-branches.js.map +1 -0
- package/dist/lint/rules/index.d.ts +17 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/index.js +36 -0
- package/dist/lint/rules/index.js.map +1 -0
- package/dist/lint/rules/module-depth-5.d.ts +14 -0
- package/dist/lint/rules/module-depth-5.d.ts.map +1 -0
- package/dist/lint/rules/module-depth-5.js +67 -0
- package/dist/lint/rules/module-depth-5.js.map +1 -0
- package/dist/lint/rules/no-literal-arn.d.ts +21 -0
- package/dist/lint/rules/no-literal-arn.d.ts.map +1 -0
- package/dist/lint/rules/no-literal-arn.js +58 -0
- package/dist/lint/rules/no-literal-arn.js.map +1 -0
- package/dist/lint/rules/no-unresolved-token.d.ts +8 -0
- package/dist/lint/rules/no-unresolved-token.d.ts.map +1 -0
- package/dist/lint/rules/no-unresolved-token.js +71 -0
- package/dist/lint/rules/no-unresolved-token.js.map +1 -0
- package/dist/lint/rules/prompt-length-3000.d.ts +16 -0
- package/dist/lint/rules/prompt-length-3000.d.ts.map +1 -0
- package/dist/lint/rules/prompt-length-3000.js +61 -0
- package/dist/lint/rules/prompt-length-3000.js.map +1 -0
- package/dist/lint/rules/reachable-blocks.d.ts +7 -0
- package/dist/lint/rules/reachable-blocks.d.ts.map +1 -0
- package/dist/lint/rules/reachable-blocks.js +45 -0
- package/dist/lint/rules/reachable-blocks.js.map +1 -0
- package/dist/lint/rules/recording-consent-before-record.d.ts +3 -0
- package/dist/lint/rules/recording-consent-before-record.d.ts.map +1 -0
- package/dist/lint/rules/recording-consent-before-record.js +87 -0
- package/dist/lint/rules/recording-consent-before-record.js.map +1 -0
- package/dist/lint/rules/terminal-blocks.d.ts +8 -0
- package/dist/lint/rules/terminal-blocks.d.ts.map +1 -0
- package/dist/lint/rules/terminal-blocks.js +40 -0
- package/dist/lint/rules/terminal-blocks.js.map +1 -0
- package/dist/lint/rules/unique-names.d.ts +7 -0
- package/dist/lint/rules/unique-names.d.ts.map +1 -0
- package/dist/lint/rules/unique-names.js +40 -0
- package/dist/lint/rules/unique-names.js.map +1 -0
- package/dist/lint/types.d.ts +30 -0
- package/dist/lint/types.d.ts.map +1 -0
- package/dist/lint/types.js +11 -0
- package/dist/lint/types.js.map +1 -0
- package/dist/materialize.d.ts +31 -0
- package/dist/materialize.d.ts.map +1 -0
- package/dist/materialize.js +192 -0
- package/dist/materialize.js.map +1 -0
- package/dist/package-names.d.ts +11 -0
- package/dist/package-names.d.ts.map +1 -0
- package/dist/package-names.js +25 -0
- package/dist/package-names.js.map +1 -0
- package/dist/refs.d.ts +35 -0
- package/dist/refs.d.ts.map +1 -0
- package/dist/refs.js +69 -0
- package/dist/refs.js.map +1 -0
- package/dist/serialize.d.ts +13 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +86 -0
- package/dist/serialize.js.map +1 -0
- package/dist/simulate.d.ts +446 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +1115 -0
- package/dist/simulate.js.map +1 -0
- package/dist/synth.d.ts +25 -0
- package/dist/synth.d.ts.map +1 -0
- package/dist/synth.js +93 -0
- package/dist/synth.js.map +1 -0
- package/package.json +73 -0
- package/src/actions.ts +221 -0
- package/src/aws.ts +55 -0
- package/src/blocks.ts +619 -0
- package/src/codegen.ts +1144 -0
- package/src/export.ts +1190 -0
- package/src/flow.ts +108 -0
- package/src/flowdoc.ts +261 -0
- package/src/index.ts +31 -0
- package/src/layout.ts +54 -0
- package/src/lint/engine.ts +56 -0
- package/src/lint/graph.ts +86 -0
- package/src/lint/index.ts +10 -0
- package/src/lint/reporters.ts +44 -0
- package/src/lint/rules/action-allowed-in-flow-type.ts +32 -0
- package/src/lint/rules/action-count.ts +35 -0
- package/src/lint/rules/error-branches.ts +45 -0
- package/src/lint/rules/index.ts +53 -0
- package/src/lint/rules/module-depth-5.ts +70 -0
- package/src/lint/rules/no-literal-arn.ts +64 -0
- package/src/lint/rules/no-unresolved-token.ts +80 -0
- package/src/lint/rules/prompt-length-3000.ts +66 -0
- package/src/lint/rules/reachable-blocks.ts +50 -0
- package/src/lint/rules/recording-consent-before-record.ts +90 -0
- package/src/lint/rules/terminal-blocks.ts +45 -0
- package/src/lint/rules/unique-names.ts +44 -0
- package/src/lint/types.ts +43 -0
- package/src/materialize.ts +230 -0
- package/src/package-names.ts +26 -0
- package/src/refs.ts +93 -0
- package/src/serialize.ts +97 -0
- package/src/simulate.ts +1610 -0
- package/src/synth.ts +110 -0
package/src/codegen.ts
ADDED
|
@@ -0,0 +1,1144 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 The flow-as-code Authors
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
// codegen: FlowDoc in, idiomatic TypeScript out.
|
|
6
|
+
//
|
|
7
|
+
// The output is what a person would have written against the builder API: one
|
|
8
|
+
// exported zero-arg factory returning a Flow (or FlowModule), with one inline
|
|
9
|
+
// block construction per Action, in doc order. Synth preserves declaration
|
|
10
|
+
// order (docs/adr/0003-action-ordering.md), so emitting doc order makes
|
|
11
|
+
// synth(codegen(doc)) reproduce the doc with no reordering step.
|
|
12
|
+
//
|
|
13
|
+
// Invariants:
|
|
14
|
+
// - Pure and byte-stable: the same FlowDoc yields the identical string.
|
|
15
|
+
// - Prettier-clean as written: the printer mirrors the repo Prettier config
|
|
16
|
+
// (2-space indent, double quotes, trailing commas, printWidth 100), and a
|
|
17
|
+
// test formats the output to prove it is a fixed point.
|
|
18
|
+
// - Lossless: an Action of a modeled Type that the block class cannot
|
|
19
|
+
// reproduce exactly falls back to GenericBlock, verified by re-synthesizing
|
|
20
|
+
// every candidate block and comparing against the source Action.
|
|
21
|
+
// - Reference tokens in modeled reference-bearing fields become Refs.* calls;
|
|
22
|
+
// JSONPath strings become jsonPath(...); tokens inside GenericBlock
|
|
23
|
+
// parameters stay verbatim strings and are never rewritten.
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
ActionType,
|
|
27
|
+
DTMF_DIGITS,
|
|
28
|
+
INPUT_TIME_LIMIT_EXCEEDED,
|
|
29
|
+
NO_MATCHING_CONDITION,
|
|
30
|
+
NO_MATCHING_ERROR,
|
|
31
|
+
REFERENCE_FIELDS,
|
|
32
|
+
} from "./actions.js";
|
|
33
|
+
import type { DtmfDigit } from "./actions.js";
|
|
34
|
+
import type { Block, DtmfBranch, GenericBlockConfig, MessageBody } from "./blocks.js";
|
|
35
|
+
import {
|
|
36
|
+
CheckHoursOfOperation,
|
|
37
|
+
Compare,
|
|
38
|
+
DisconnectParticipant,
|
|
39
|
+
EndFlowExecution,
|
|
40
|
+
EndFlowModuleExecution,
|
|
41
|
+
GenericBlock,
|
|
42
|
+
GetParticipantInput,
|
|
43
|
+
InvokeFlowModule,
|
|
44
|
+
InvokeLambdaFunction,
|
|
45
|
+
MessageParticipant,
|
|
46
|
+
TransferContactToQueue,
|
|
47
|
+
TransferToFlow,
|
|
48
|
+
UpdateContactAttributes,
|
|
49
|
+
UpdateContactRecordingBehavior,
|
|
50
|
+
UpdateContactTargetQueue,
|
|
51
|
+
} from "./blocks.js";
|
|
52
|
+
import type {
|
|
53
|
+
ConditionOperator,
|
|
54
|
+
ErrorTransition,
|
|
55
|
+
FlowAction,
|
|
56
|
+
FlowDoc,
|
|
57
|
+
Point,
|
|
58
|
+
RefType,
|
|
59
|
+
Transitions,
|
|
60
|
+
} from "./flowdoc.js";
|
|
61
|
+
import { assertFlowDoc } from "./flowdoc.js";
|
|
62
|
+
import { autoLayout } from "./layout.js";
|
|
63
|
+
import { PACKAGE_NAMES } from "./package-names.js";
|
|
64
|
+
import { parseToken } from "./refs.js";
|
|
65
|
+
|
|
66
|
+
export interface CodegenOptions {
|
|
67
|
+
/** Import path for the builder API. Defaults to the published package. */
|
|
68
|
+
moduleSpecifier?: string;
|
|
69
|
+
/**
|
|
70
|
+
* A previously generated source for the same flow. Comment lines containing
|
|
71
|
+
* `@keep` that immediately precede a block construction re-attach to the
|
|
72
|
+
* block with the same id; ones immediately preceding the export function
|
|
73
|
+
* re-attach there. Everything else regenerates.
|
|
74
|
+
*/
|
|
75
|
+
previous?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const DEFAULT_MODULE_SPECIFIER = PACKAGE_NAMES.core;
|
|
79
|
+
const PRINT_WIDTH = 100;
|
|
80
|
+
|
|
81
|
+
// The banner on generated files. No licence header: the output lands in a
|
|
82
|
+
// user's project and is their file, not ours, so stamping this repo's
|
|
83
|
+
// copyright on it would be wrong. The Terraform emitter's banner works the
|
|
84
|
+
// same way (packages/tf/src/emit.ts).
|
|
85
|
+
//
|
|
86
|
+
// Naming the document it came from makes a file found on its own explain
|
|
87
|
+
// itself, and `.flowdoc.json` is the name `flow-cli codegen` reads.
|
|
88
|
+
function generatedMarker(name: string): string[] {
|
|
89
|
+
return [
|
|
90
|
+
`// Generated by ${PACKAGE_NAMES.core} codegen from ${name}.flowdoc.json.`,
|
|
91
|
+
"// Edits to structure regenerate; comments marked @keep survive.",
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Value model for the printer
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/** A code fragment emitted verbatim, e.g. `Refs.queue("front-desk")`. */
|
|
100
|
+
class Raw {
|
|
101
|
+
constructor(readonly code: string) {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** An object literal with a fixed entry order. */
|
|
105
|
+
class ObjV {
|
|
106
|
+
constructor(readonly entries: [string, V][]) {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
class ArrV {
|
|
110
|
+
constructor(readonly items: V[]) {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type V = string | number | boolean | null | Raw | ObjV | ArrV;
|
|
114
|
+
|
|
115
|
+
/** Arbitrary JSON (GenericBlock parameters) to the value model, keys sorted. */
|
|
116
|
+
function toV(value: unknown): V {
|
|
117
|
+
if (value === null || typeof value === "string" || typeof value === "number") return value;
|
|
118
|
+
if (typeof value === "boolean") return value;
|
|
119
|
+
if (Array.isArray(value)) return new ArrV(value.map(toV));
|
|
120
|
+
if (typeof value === "object") {
|
|
121
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
122
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
123
|
+
.map(([k, v]) => [k, toV(v)] as [string, V]);
|
|
124
|
+
return new ObjV(entries);
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`Cannot emit a ${typeof value} value.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Printer. Mirrors the repo Prettier config so the output is a Prettier fixed
|
|
131
|
+
// point: double quotes unless the string holds more double than single quotes,
|
|
132
|
+
// keys quoted only when not valid identifiers, objects kept expanded once
|
|
133
|
+
// broken (Prettier objectWrap: preserve), arrays force-broken when every
|
|
134
|
+
// element is an object with two or more properties.
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
138
|
+
|
|
139
|
+
function quoteString(s: string): string {
|
|
140
|
+
const doubles = (s.match(/"/g) ?? []).length;
|
|
141
|
+
const singles = (s.match(/'/g) ?? []).length;
|
|
142
|
+
const q = doubles > singles ? "'" : '"';
|
|
143
|
+
let out = q;
|
|
144
|
+
for (const ch of s) {
|
|
145
|
+
if (ch === "\\") out += "\\\\";
|
|
146
|
+
else if (ch === q) out += `\\${q}`;
|
|
147
|
+
else if (ch === "\n") out += "\\n";
|
|
148
|
+
else if (ch === "\r") out += "\\r";
|
|
149
|
+
else if (ch === "\t") out += "\\t";
|
|
150
|
+
else if (
|
|
151
|
+
ch < " " ||
|
|
152
|
+
ch === "\u2028" ||
|
|
153
|
+
ch === "\u2029" ||
|
|
154
|
+
(ch.codePointAt(0)! >= 0xd800 && ch.codePointAt(0)! <= 0xdfff)
|
|
155
|
+
) {
|
|
156
|
+
// Control characters, JS line separators, and lone surrogates must be
|
|
157
|
+
// escaped: an unpaired surrogate written to a utf8 file becomes U+FFFD,
|
|
158
|
+
// which would break the file-based round trip.
|
|
159
|
+
out += `\\u${ch.codePointAt(0)!.toString(16).padStart(4, "0")}`;
|
|
160
|
+
} else out += ch;
|
|
161
|
+
}
|
|
162
|
+
return out + q;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function keySource(key: string): string {
|
|
166
|
+
return IDENTIFIER.test(key) ? key : quoteString(key);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Prettier breaks such arrays regardless of width. */
|
|
170
|
+
function arrayForcesBreak(a: ArrV): boolean {
|
|
171
|
+
return a.items.length > 1 && a.items.every((i) => i instanceof ObjV && i.entries.length >= 2);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Single-line rendering, or undefined when the value must break. */
|
|
175
|
+
function inlineV(v: V): string | undefined {
|
|
176
|
+
if (v instanceof Raw) return v.code;
|
|
177
|
+
if (typeof v === "string") return quoteString(v);
|
|
178
|
+
if (typeof v === "number" || typeof v === "boolean" || v === null) return String(v);
|
|
179
|
+
if (v instanceof ArrV) {
|
|
180
|
+
if (arrayForcesBreak(v)) return undefined;
|
|
181
|
+
const items = v.items.map(inlineV);
|
|
182
|
+
if (items.some((i) => i === undefined)) return undefined;
|
|
183
|
+
return `[${items.join(", ")}]`;
|
|
184
|
+
}
|
|
185
|
+
if (v.entries.length === 0) return "{}";
|
|
186
|
+
const parts = v.entries.map(([k, val]) => {
|
|
187
|
+
const inner = inlineV(val);
|
|
188
|
+
return inner === undefined ? undefined : `${keySource(k)}: ${inner}`;
|
|
189
|
+
});
|
|
190
|
+
if (parts.some((p) => p === undefined)) return undefined;
|
|
191
|
+
return `{ ${parts.join(", ")} }`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Renders a value starting at column `col` on a line indented by `indent`.
|
|
196
|
+
* Continuation lines carry their own indentation; the first line does not.
|
|
197
|
+
* One column is reserved for the trailing comma the caller appends.
|
|
198
|
+
*/
|
|
199
|
+
function printV(v: V, col: number, indent: string): string {
|
|
200
|
+
const flat = inlineV(v);
|
|
201
|
+
if (flat !== undefined && col + flat.length + 1 <= PRINT_WIDTH) return flat;
|
|
202
|
+
const inner = `${indent} `;
|
|
203
|
+
if (v instanceof ObjV && v.entries.length > 0) return printBrokenObject(v.entries, indent);
|
|
204
|
+
if (v instanceof ArrV && v.items.length > 0) {
|
|
205
|
+
const lines = v.items.map((i) => `${inner}${printV(i, inner.length, inner)},`);
|
|
206
|
+
return `[\n${lines.join("\n")}\n${indent}]`;
|
|
207
|
+
}
|
|
208
|
+
// Primitives and Raw fragments cannot break; emit even when over width.
|
|
209
|
+
return flat ?? "";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function printBrokenObject(entries: [string, V][], indent: string): string {
|
|
213
|
+
const inner = `${indent} `;
|
|
214
|
+
const lines = entries.map(([k, val]) => {
|
|
215
|
+
const prefix = `${inner}${keySource(k)}: `;
|
|
216
|
+
return `${prefix}${printV(val, prefix.length, inner)},`;
|
|
217
|
+
});
|
|
218
|
+
return `{\n${lines.join("\n")}\n${indent}}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Inversion: FlowAction back to a block construction.
|
|
223
|
+
//
|
|
224
|
+
// Every candidate is verified by constructing the real block and comparing its
|
|
225
|
+
// toAction() output against the source Action; on any mismatch the Action is
|
|
226
|
+
// emitted as a GenericBlock instead, so the round-trip stays lossless.
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
interface Inversion {
|
|
230
|
+
/** Block class name, also the import to emit. */
|
|
231
|
+
cls: string;
|
|
232
|
+
/** Config object entries in emission order. */
|
|
233
|
+
entries: [string, V][];
|
|
234
|
+
/** The constructed block, used to verify the inversion is exact. */
|
|
235
|
+
block: Block;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface Ctx {
|
|
239
|
+
refs: boolean;
|
|
240
|
+
jsonPath: boolean;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function stableJson(value: unknown): string {
|
|
244
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
245
|
+
if (value !== null && typeof value === "object") {
|
|
246
|
+
const keys = Object.keys(value as Record<string, unknown>).sort();
|
|
247
|
+
const body = keys
|
|
248
|
+
.map((k) => `${JSON.stringify(k)}:${stableJson((value as Record<string, unknown>)[k])}`)
|
|
249
|
+
.join(",");
|
|
250
|
+
return `{${body}}`;
|
|
251
|
+
}
|
|
252
|
+
return JSON.stringify(value);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const cast = <T>(value: unknown): T => value as T;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* A reference-bearing field value as builder source. Well-formed tokens of the
|
|
259
|
+
* expected type become Refs.* calls, JSONPath strings become jsonPath(...),
|
|
260
|
+
* anything else is inexpressible and forces the GenericBlock fallback.
|
|
261
|
+
* https://docs.aws.amazon.com/connect/latest/devguide/flow-language-actions.html
|
|
262
|
+
* documents these fields as fully static or a single JSONPath identifier.
|
|
263
|
+
*/
|
|
264
|
+
function refSource(value: unknown, type: RefType, ctx: Ctx): Raw | undefined {
|
|
265
|
+
if (typeof value !== "string") return undefined;
|
|
266
|
+
const entry = parseToken(value);
|
|
267
|
+
if (entry !== undefined) {
|
|
268
|
+
if (entry.type !== type) return undefined;
|
|
269
|
+
if (type === "module") {
|
|
270
|
+
if (entry.alias === undefined) return undefined; // Refs.module requires an alias
|
|
271
|
+
ctx.refs = true;
|
|
272
|
+
return new Raw(`Refs.module(${quoteString(entry.name)}, ${quoteString(entry.alias)})`);
|
|
273
|
+
}
|
|
274
|
+
if (entry.alias !== undefined) return undefined; // only module refs carry aliases
|
|
275
|
+
ctx.refs = true;
|
|
276
|
+
return new Raw(`Refs.${type}(${quoteString(entry.name)})`);
|
|
277
|
+
}
|
|
278
|
+
if (/^\$\.[A-Za-z0-9_$.[\]'-]+$/.test(value)) {
|
|
279
|
+
ctx.jsonPath = true;
|
|
280
|
+
return new Raw(`jsonPath(${quoteString(value)})`);
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function paramKeysAre(
|
|
286
|
+
params: Record<string, unknown>,
|
|
287
|
+
required: string[],
|
|
288
|
+
optional: string[] = [],
|
|
289
|
+
): boolean {
|
|
290
|
+
const keys = Object.keys(params);
|
|
291
|
+
return (
|
|
292
|
+
required.every((k) => keys.includes(k)) &&
|
|
293
|
+
keys.every((k) => required.includes(k) || optional.includes(k))
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** NextAction plus exactly one catch-all error and no conditions. */
|
|
298
|
+
function wiredTransitions(
|
|
299
|
+
t: Transitions,
|
|
300
|
+
errorType: string,
|
|
301
|
+
): { next: string; onError: string } | undefined {
|
|
302
|
+
if (t.NextAction === undefined) return undefined;
|
|
303
|
+
const errors = t.Errors ?? [];
|
|
304
|
+
if (errors.length !== 1 || errors[0]!.ErrorType !== errorType) return undefined;
|
|
305
|
+
if ((t.Conditions ?? []).length !== 0) return undefined;
|
|
306
|
+
return { next: t.NextAction, onError: errors[0]!.NextAction };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isStringMap(value: unknown): value is Record<string, string> {
|
|
310
|
+
return (
|
|
311
|
+
value !== null &&
|
|
312
|
+
typeof value === "object" &&
|
|
313
|
+
!Array.isArray(value) &&
|
|
314
|
+
Object.values(value as Record<string, unknown>).every((v) => typeof v === "string")
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const OPERATORS: readonly string[] = [
|
|
319
|
+
"Equals",
|
|
320
|
+
"TextStartsWith",
|
|
321
|
+
"TextEndsWith",
|
|
322
|
+
"TextContains",
|
|
323
|
+
"NumberGreaterThan",
|
|
324
|
+
"NumberGreaterOrEqualTo",
|
|
325
|
+
"NumberLessThan",
|
|
326
|
+
"NumberLessOrEqualTo",
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
function isCondition(
|
|
330
|
+
value: unknown,
|
|
331
|
+
): value is { NextAction: string; Condition: { Operator: ConditionOperator; Operands: string[] } } {
|
|
332
|
+
const c = value as {
|
|
333
|
+
NextAction?: unknown;
|
|
334
|
+
Condition?: { Operator?: unknown; Operands?: unknown };
|
|
335
|
+
};
|
|
336
|
+
return (
|
|
337
|
+
typeof c.NextAction === "string" &&
|
|
338
|
+
typeof c.Condition === "object" &&
|
|
339
|
+
c.Condition !== null &&
|
|
340
|
+
typeof c.Condition.Operator === "string" &&
|
|
341
|
+
OPERATORS.includes(c.Condition.Operator) &&
|
|
342
|
+
Array.isArray(c.Condition.Operands) &&
|
|
343
|
+
c.Condition.Operands.every((o) => typeof o === "string")
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function terminal(a: FlowAction, make: () => Block): Inversion | undefined {
|
|
348
|
+
if (Object.keys(a.Parameters).length !== 0) return undefined;
|
|
349
|
+
if (Object.keys(a.Transitions).length !== 0) return undefined;
|
|
350
|
+
return { cls: a.Type, entries: [["id", a.Identifier]], block: make() };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* One inverter per modeled action type; undefined means "not expressible".
|
|
355
|
+
*
|
|
356
|
+
* The correctness gate is not in here. invertAction constructs the candidate
|
|
357
|
+
* block, compares its synthesized Action with the source byte for byte, and
|
|
358
|
+
* falls back to GenericBlock on any difference or constructor throw. The early
|
|
359
|
+
* returns below are readable statements of each class's shape and a cheap way
|
|
360
|
+
* out before a constructor runs; most of them are redundant with that
|
|
361
|
+
* comparison, so neutering one does not change the output. The exception is
|
|
362
|
+
* the refSource check: the block would still verify with an unresolvable
|
|
363
|
+
* PromptId, but the emitted entries would carry `prompt: undefined`, so that
|
|
364
|
+
* return is load-bearing and is the one a test pins. The typeof checks on
|
|
365
|
+
* string parameters exist for the type checker; the comparison catches the
|
|
366
|
+
* JSON number spelling either way.
|
|
367
|
+
*/
|
|
368
|
+
const INVERTERS: Record<string, (a: FlowAction, ctx: Ctx) => Inversion | undefined> = {
|
|
369
|
+
[ActionType.MessageParticipant]: (a, ctx) => {
|
|
370
|
+
const w = wiredTransitions(a.Transitions, NO_MATCHING_ERROR);
|
|
371
|
+
if (w === undefined) return undefined;
|
|
372
|
+
const p = a.Parameters;
|
|
373
|
+
const keys = Object.keys(p);
|
|
374
|
+
if (keys.length !== 1) return undefined;
|
|
375
|
+
const entries: [string, V][] = [["id", a.Identifier]];
|
|
376
|
+
if (keys[0] === "Text" && typeof p.Text === "string") entries.push(["text", p.Text]);
|
|
377
|
+
else if (keys[0] === "SSML" && typeof p.SSML === "string") entries.push(["ssml", p.SSML]);
|
|
378
|
+
else if (keys[0] === "PromptId") {
|
|
379
|
+
const ref = refSource(
|
|
380
|
+
p.PromptId,
|
|
381
|
+
REFERENCE_FIELDS[ActionType.MessageParticipant]!.PromptId!,
|
|
382
|
+
ctx,
|
|
383
|
+
);
|
|
384
|
+
if (ref === undefined) return undefined;
|
|
385
|
+
entries.push(["prompt", ref]);
|
|
386
|
+
} else return undefined;
|
|
387
|
+
entries.push(["next", w.next], ["onError", w.onError]);
|
|
388
|
+
const body =
|
|
389
|
+
keys[0] === "Text"
|
|
390
|
+
? { text: cast<string>(p.Text) }
|
|
391
|
+
: keys[0] === "SSML"
|
|
392
|
+
? { ssml: cast<string>(p.SSML) }
|
|
393
|
+
: { prompt: cast<never>(p.PromptId) };
|
|
394
|
+
return {
|
|
395
|
+
cls: "MessageParticipant",
|
|
396
|
+
entries,
|
|
397
|
+
block: new MessageParticipant({
|
|
398
|
+
id: a.Identifier,
|
|
399
|
+
...body,
|
|
400
|
+
next: w.next,
|
|
401
|
+
onError: w.onError,
|
|
402
|
+
}),
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
[ActionType.GetParticipantInput]: (a, ctx) => {
|
|
407
|
+
const t = a.Transitions;
|
|
408
|
+
const errors = t.Errors ?? [];
|
|
409
|
+
// The class emits the menu form's three errors in this order.
|
|
410
|
+
const expected = [INPUT_TIME_LIMIT_EXCEEDED, NO_MATCHING_CONDITION, NO_MATCHING_ERROR];
|
|
411
|
+
if (errors.length !== expected.length) return undefined;
|
|
412
|
+
if (errors.some((e, i) => e.ErrorType !== expected[i])) return undefined;
|
|
413
|
+
const [onTimeout, onNoMatch, onError] = errors.map((e) => e.NextAction) as [
|
|
414
|
+
string,
|
|
415
|
+
string,
|
|
416
|
+
string,
|
|
417
|
+
];
|
|
418
|
+
// The class mirrors NextAction onto the no-match path.
|
|
419
|
+
if (t.NextAction !== onNoMatch) return undefined;
|
|
420
|
+
const conditions = t.Conditions ?? [];
|
|
421
|
+
if (!conditions.every(isCondition)) return undefined;
|
|
422
|
+
const branches: DtmfBranch[] = [];
|
|
423
|
+
for (const c of conditions) {
|
|
424
|
+
if (c.Condition.Operator !== "Equals" || c.Condition.Operands.length !== 1) return undefined;
|
|
425
|
+
const digit = c.Condition.Operands[0]!;
|
|
426
|
+
if (!(DTMF_DIGITS as readonly string[]).includes(digit)) return undefined;
|
|
427
|
+
branches.push({ digit: digit as DtmfDigit, target: c.NextAction });
|
|
428
|
+
}
|
|
429
|
+
const p = a.Parameters;
|
|
430
|
+
if (!paramKeysAre(p, ["InputTimeLimitSeconds", "StoreInput"], ["Text", "SSML", "PromptId"])) {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
// Only the menu form is modeled; stored input has no conditions and needs
|
|
434
|
+
// InputValidation, which the class does not emit.
|
|
435
|
+
if (p.StoreInput !== "False") return undefined;
|
|
436
|
+
// The class writes the timeout as a plain decimal string, so only that
|
|
437
|
+
// spelling inverts: "5", never 5, "05", or "5.0". The constructor holds
|
|
438
|
+
// the value to its documented range.
|
|
439
|
+
if (typeof p.InputTimeLimitSeconds !== "string") return undefined;
|
|
440
|
+
if (!/^[1-9][0-9]*$/.test(p.InputTimeLimitSeconds)) return undefined;
|
|
441
|
+
const timeoutSeconds = Number(p.InputTimeLimitSeconds);
|
|
442
|
+
const bodyKeys = ["Text", "SSML", "PromptId"].filter((k) => k in p);
|
|
443
|
+
if (bodyKeys.length > 1) return undefined;
|
|
444
|
+
const entries: [string, V][] = [["id", a.Identifier]];
|
|
445
|
+
let body: MessageBody | { text?: never; ssml?: never; prompt?: never } = {};
|
|
446
|
+
if (bodyKeys[0] === "Text" && typeof p.Text === "string") {
|
|
447
|
+
entries.push(["text", p.Text]);
|
|
448
|
+
body = { text: p.Text };
|
|
449
|
+
} else if (bodyKeys[0] === "SSML" && typeof p.SSML === "string") {
|
|
450
|
+
entries.push(["ssml", p.SSML]);
|
|
451
|
+
body = { ssml: p.SSML };
|
|
452
|
+
} else if (bodyKeys[0] === "PromptId") {
|
|
453
|
+
const ref = refSource(
|
|
454
|
+
p.PromptId,
|
|
455
|
+
REFERENCE_FIELDS[ActionType.GetParticipantInput]!.PromptId!,
|
|
456
|
+
ctx,
|
|
457
|
+
);
|
|
458
|
+
if (ref === undefined) return undefined;
|
|
459
|
+
entries.push(["prompt", ref]);
|
|
460
|
+
body = { prompt: cast<never>(p.PromptId) };
|
|
461
|
+
} else if (bodyKeys.length === 1) return undefined;
|
|
462
|
+
entries.push(
|
|
463
|
+
["timeoutSeconds", timeoutSeconds],
|
|
464
|
+
[
|
|
465
|
+
"branches",
|
|
466
|
+
new ArrV(
|
|
467
|
+
branches.map(
|
|
468
|
+
(b) =>
|
|
469
|
+
new ObjV([
|
|
470
|
+
["digit", b.digit],
|
|
471
|
+
["target", b.target as string],
|
|
472
|
+
]),
|
|
473
|
+
),
|
|
474
|
+
),
|
|
475
|
+
],
|
|
476
|
+
["onTimeout", onTimeout],
|
|
477
|
+
["onNoMatch", onNoMatch],
|
|
478
|
+
["onError", onError],
|
|
479
|
+
);
|
|
480
|
+
return {
|
|
481
|
+
cls: "GetParticipantInput",
|
|
482
|
+
entries,
|
|
483
|
+
block: new GetParticipantInput({
|
|
484
|
+
id: a.Identifier,
|
|
485
|
+
...body,
|
|
486
|
+
timeoutSeconds,
|
|
487
|
+
branches,
|
|
488
|
+
onTimeout,
|
|
489
|
+
onNoMatch,
|
|
490
|
+
onError,
|
|
491
|
+
}),
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
[ActionType.DisconnectParticipant]: (a) =>
|
|
496
|
+
terminal(a, () => new DisconnectParticipant({ id: a.Identifier })),
|
|
497
|
+
[ActionType.EndFlowExecution]: (a) =>
|
|
498
|
+
terminal(a, () => new EndFlowExecution({ id: a.Identifier })),
|
|
499
|
+
[ActionType.EndFlowModuleExecution]: (a) =>
|
|
500
|
+
terminal(a, () => new EndFlowModuleExecution({ id: a.Identifier })),
|
|
501
|
+
|
|
502
|
+
[ActionType.CheckHoursOfOperation]: (a, ctx) => {
|
|
503
|
+
const t = a.Transitions;
|
|
504
|
+
const errors = t.Errors ?? [];
|
|
505
|
+
const conditions = t.Conditions ?? [];
|
|
506
|
+
if (errors.length !== 1 || errors[0]!.ErrorType !== NO_MATCHING_ERROR) return undefined;
|
|
507
|
+
if (conditions.length !== 2) return undefined;
|
|
508
|
+
const [inHours, outOfHours] = conditions;
|
|
509
|
+
if (stableJson(inHours!.Condition) !== stableJson({ Operator: "Equals", Operands: ["True"] })) {
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
if (
|
|
513
|
+
stableJson(outOfHours!.Condition) !== stableJson({ Operator: "Equals", Operands: ["False"] })
|
|
514
|
+
) {
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
// The class mirrors NextAction onto the out-of-hours path.
|
|
518
|
+
if (t.NextAction !== outOfHours!.NextAction) return undefined;
|
|
519
|
+
if (!paramKeysAre(a.Parameters, [], ["HoursOfOperationId"])) return undefined;
|
|
520
|
+
const entries: [string, V][] = [["id", a.Identifier]];
|
|
521
|
+
let hours: string | undefined;
|
|
522
|
+
if ("HoursOfOperationId" in a.Parameters) {
|
|
523
|
+
const ref = refSource(a.Parameters.HoursOfOperationId, "hours", ctx);
|
|
524
|
+
if (ref === undefined) return undefined;
|
|
525
|
+
entries.push(["hours", ref]);
|
|
526
|
+
hours = cast<string>(a.Parameters.HoursOfOperationId);
|
|
527
|
+
}
|
|
528
|
+
entries.push(
|
|
529
|
+
["onInHours", inHours!.NextAction],
|
|
530
|
+
["onOutOfHours", outOfHours!.NextAction],
|
|
531
|
+
["onError", errors[0]!.NextAction],
|
|
532
|
+
);
|
|
533
|
+
return {
|
|
534
|
+
cls: "CheckHoursOfOperation",
|
|
535
|
+
entries,
|
|
536
|
+
block: new CheckHoursOfOperation({
|
|
537
|
+
id: a.Identifier,
|
|
538
|
+
...(hours === undefined ? {} : { hours: cast<never>(hours) }),
|
|
539
|
+
onInHours: inHours!.NextAction,
|
|
540
|
+
onOutOfHours: outOfHours!.NextAction,
|
|
541
|
+
onError: errors[0]!.NextAction,
|
|
542
|
+
}),
|
|
543
|
+
};
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
[ActionType.Compare]: (a, ctx) => {
|
|
547
|
+
const t = a.Transitions;
|
|
548
|
+
if (t.NextAction !== undefined) return undefined;
|
|
549
|
+
const errors = t.Errors ?? [];
|
|
550
|
+
if (errors.length !== 1 || errors[0]!.ErrorType !== NO_MATCHING_CONDITION) return undefined;
|
|
551
|
+
const conditions = t.Conditions ?? [];
|
|
552
|
+
if (conditions.length === 0 || !conditions.every(isCondition)) return undefined;
|
|
553
|
+
if (!paramKeysAre(a.Parameters, ["ComparisonValue"])) return undefined;
|
|
554
|
+
const value = a.Parameters.ComparisonValue;
|
|
555
|
+
if (typeof value !== "string" || !/^\$\.[A-Za-z0-9_$.[\]'-]+$/.test(value)) return undefined;
|
|
556
|
+
// The emitted config calls jsonPath(); record it so the import is present
|
|
557
|
+
// even when no other action pulls it in through refSource.
|
|
558
|
+
ctx.jsonPath = true;
|
|
559
|
+
const branches = conditions.map((c) => ({
|
|
560
|
+
operator: c.Condition.Operator,
|
|
561
|
+
operands: c.Condition.Operands,
|
|
562
|
+
target: c.NextAction,
|
|
563
|
+
}));
|
|
564
|
+
const entries: [string, V][] = [
|
|
565
|
+
["id", a.Identifier],
|
|
566
|
+
["value", new Raw(`jsonPath(${quoteString(value)})`)],
|
|
567
|
+
[
|
|
568
|
+
"branches",
|
|
569
|
+
new ArrV(
|
|
570
|
+
branches.map(
|
|
571
|
+
(b) =>
|
|
572
|
+
new ObjV([
|
|
573
|
+
["operator", b.operator],
|
|
574
|
+
["operands", new ArrV([...b.operands])],
|
|
575
|
+
["target", b.target],
|
|
576
|
+
]),
|
|
577
|
+
),
|
|
578
|
+
),
|
|
579
|
+
],
|
|
580
|
+
["onNoMatch", errors[0]!.NextAction],
|
|
581
|
+
];
|
|
582
|
+
return {
|
|
583
|
+
cls: "Compare",
|
|
584
|
+
entries,
|
|
585
|
+
block: new Compare({
|
|
586
|
+
id: a.Identifier,
|
|
587
|
+
value: cast<never>(value),
|
|
588
|
+
branches,
|
|
589
|
+
onNoMatch: errors[0]!.NextAction,
|
|
590
|
+
}),
|
|
591
|
+
};
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
[ActionType.TransferToFlow]: (a, ctx) => {
|
|
595
|
+
const w = wiredTransitions(a.Transitions, NO_MATCHING_ERROR);
|
|
596
|
+
if (w === undefined || !paramKeysAre(a.Parameters, ["ContactFlowId"])) return undefined;
|
|
597
|
+
const ref = refSource(a.Parameters.ContactFlowId, "flow", ctx);
|
|
598
|
+
if (ref === undefined) return undefined;
|
|
599
|
+
return {
|
|
600
|
+
cls: "TransferToFlow",
|
|
601
|
+
entries: [
|
|
602
|
+
["id", a.Identifier],
|
|
603
|
+
["flow", ref],
|
|
604
|
+
["next", w.next],
|
|
605
|
+
["onError", w.onError],
|
|
606
|
+
],
|
|
607
|
+
block: new TransferToFlow({
|
|
608
|
+
id: a.Identifier,
|
|
609
|
+
flow: cast<never>(a.Parameters.ContactFlowId),
|
|
610
|
+
next: w.next,
|
|
611
|
+
onError: w.onError,
|
|
612
|
+
}),
|
|
613
|
+
};
|
|
614
|
+
},
|
|
615
|
+
|
|
616
|
+
[ActionType.UpdateContactTargetQueue]: (a, ctx) => {
|
|
617
|
+
const w = wiredTransitions(a.Transitions, NO_MATCHING_ERROR);
|
|
618
|
+
if (w === undefined) return undefined;
|
|
619
|
+
const keys = Object.keys(a.Parameters);
|
|
620
|
+
if (keys.length !== 1 || (keys[0] !== "QueueId" && keys[0] !== "AgentId")) return undefined;
|
|
621
|
+
const field = keys[0];
|
|
622
|
+
const ref = refSource(a.Parameters[field], "queue", ctx);
|
|
623
|
+
if (ref === undefined) return undefined;
|
|
624
|
+
const prop = field === "QueueId" ? "queue" : "agent";
|
|
625
|
+
return {
|
|
626
|
+
cls: "UpdateContactTargetQueue",
|
|
627
|
+
entries: [
|
|
628
|
+
["id", a.Identifier],
|
|
629
|
+
[prop, ref],
|
|
630
|
+
["next", w.next],
|
|
631
|
+
["onError", w.onError],
|
|
632
|
+
],
|
|
633
|
+
block: new UpdateContactTargetQueue(
|
|
634
|
+
cast<never>({
|
|
635
|
+
id: a.Identifier,
|
|
636
|
+
[prop]: a.Parameters[field],
|
|
637
|
+
next: w.next,
|
|
638
|
+
onError: w.onError,
|
|
639
|
+
}),
|
|
640
|
+
),
|
|
641
|
+
};
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
[ActionType.TransferContactToQueue]: (a) => {
|
|
645
|
+
const t = a.Transitions;
|
|
646
|
+
if (Object.keys(a.Parameters).length !== 0) return undefined;
|
|
647
|
+
if (t.NextAction === undefined || (t.Conditions ?? []).length !== 0) return undefined;
|
|
648
|
+
const errors = t.Errors ?? [];
|
|
649
|
+
if (
|
|
650
|
+
errors.length !== 2 ||
|
|
651
|
+
errors[0]!.ErrorType !== "QueueAtCapacity" ||
|
|
652
|
+
errors[1]!.ErrorType !== NO_MATCHING_ERROR
|
|
653
|
+
) {
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
cls: "TransferContactToQueue",
|
|
658
|
+
entries: [
|
|
659
|
+
["id", a.Identifier],
|
|
660
|
+
["next", t.NextAction],
|
|
661
|
+
["onQueueAtCapacity", errors[0]!.NextAction],
|
|
662
|
+
["onError", errors[1]!.NextAction],
|
|
663
|
+
],
|
|
664
|
+
block: new TransferContactToQueue({
|
|
665
|
+
id: a.Identifier,
|
|
666
|
+
next: t.NextAction,
|
|
667
|
+
onQueueAtCapacity: errors[0]!.NextAction,
|
|
668
|
+
onError: errors[1]!.NextAction,
|
|
669
|
+
}),
|
|
670
|
+
};
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
[ActionType.UpdateContactAttributes]: (a) => {
|
|
674
|
+
const w = wiredTransitions(a.Transitions, NO_MATCHING_ERROR);
|
|
675
|
+
if (w === undefined) return undefined;
|
|
676
|
+
if (!paramKeysAre(a.Parameters, ["Attributes", "TargetContact"])) return undefined;
|
|
677
|
+
const attributes = a.Parameters.Attributes;
|
|
678
|
+
const target = a.Parameters.TargetContact;
|
|
679
|
+
if (!isStringMap(attributes)) return undefined;
|
|
680
|
+
if (target !== "Current" && target !== "Related") return undefined;
|
|
681
|
+
const entries: [string, V][] = [
|
|
682
|
+
["id", a.Identifier],
|
|
683
|
+
["attributes", toV(attributes)],
|
|
684
|
+
];
|
|
685
|
+
if (target === "Related") entries.push(["targetContact", "Related"]);
|
|
686
|
+
entries.push(["next", w.next], ["onError", w.onError]);
|
|
687
|
+
return {
|
|
688
|
+
cls: "UpdateContactAttributes",
|
|
689
|
+
entries,
|
|
690
|
+
block: new UpdateContactAttributes({
|
|
691
|
+
id: a.Identifier,
|
|
692
|
+
attributes,
|
|
693
|
+
...(target === "Related" ? { targetContact: target } : {}),
|
|
694
|
+
next: w.next,
|
|
695
|
+
onError: w.onError,
|
|
696
|
+
}),
|
|
697
|
+
};
|
|
698
|
+
},
|
|
699
|
+
|
|
700
|
+
[ActionType.UpdateContactRecordingBehavior]: (a) => {
|
|
701
|
+
const w = wiredTransitions(a.Transitions, NO_MATCHING_ERROR);
|
|
702
|
+
if (w === undefined || !paramKeysAre(a.Parameters, ["RecordingBehavior"])) return undefined;
|
|
703
|
+
const rb = a.Parameters.RecordingBehavior as Record<string, unknown> | null;
|
|
704
|
+
if (rb === null || typeof rb !== "object" || Array.isArray(rb)) return undefined;
|
|
705
|
+
if (
|
|
706
|
+
!paramKeysAre(
|
|
707
|
+
rb,
|
|
708
|
+
["RecordedParticipants"],
|
|
709
|
+
["ScreenRecordedParticipants", "IVRRecordingBehavior"],
|
|
710
|
+
)
|
|
711
|
+
) {
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
const recorded = rb.RecordedParticipants;
|
|
715
|
+
if (!Array.isArray(recorded) || !recorded.every((p) => p === "Agent" || p === "Customer")) {
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
718
|
+
const entries: [string, V][] = [
|
|
719
|
+
["id", a.Identifier],
|
|
720
|
+
["recordedParticipants", new ArrV([...recorded])],
|
|
721
|
+
];
|
|
722
|
+
const screen = rb.ScreenRecordedParticipants;
|
|
723
|
+
if (screen !== undefined) {
|
|
724
|
+
if (!Array.isArray(screen) || !screen.every((p) => p === "Agent")) return undefined;
|
|
725
|
+
entries.push(["screenRecordedParticipants", new ArrV([...screen])]);
|
|
726
|
+
}
|
|
727
|
+
const ivr = rb.IVRRecordingBehavior;
|
|
728
|
+
if (ivr !== undefined) {
|
|
729
|
+
if (ivr !== "Enabled" && ivr !== "Disabled") return undefined;
|
|
730
|
+
entries.push(["ivrRecordingBehavior", ivr]);
|
|
731
|
+
}
|
|
732
|
+
entries.push(["next", w.next], ["onError", w.onError]);
|
|
733
|
+
return {
|
|
734
|
+
cls: "UpdateContactRecordingBehavior",
|
|
735
|
+
entries,
|
|
736
|
+
block: new UpdateContactRecordingBehavior({
|
|
737
|
+
id: a.Identifier,
|
|
738
|
+
recordedParticipants: cast<never>(recorded),
|
|
739
|
+
...(screen === undefined ? {} : { screenRecordedParticipants: cast<never>(screen) }),
|
|
740
|
+
...(ivr === undefined ? {} : { ivrRecordingBehavior: ivr }),
|
|
741
|
+
next: w.next,
|
|
742
|
+
onError: w.onError,
|
|
743
|
+
}),
|
|
744
|
+
};
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
[ActionType.InvokeFlowModule]: (a, ctx) => {
|
|
748
|
+
const w = wiredTransitions(a.Transitions, NO_MATCHING_ERROR);
|
|
749
|
+
if (w === undefined || !paramKeysAre(a.Parameters, ["FlowModuleId"])) return undefined;
|
|
750
|
+
const ref = refSource(a.Parameters.FlowModuleId, "module", ctx);
|
|
751
|
+
if (ref === undefined) return undefined;
|
|
752
|
+
return {
|
|
753
|
+
cls: "InvokeFlowModule",
|
|
754
|
+
entries: [
|
|
755
|
+
["id", a.Identifier],
|
|
756
|
+
["module", ref],
|
|
757
|
+
["next", w.next],
|
|
758
|
+
["onError", w.onError],
|
|
759
|
+
],
|
|
760
|
+
block: new InvokeFlowModule({
|
|
761
|
+
id: a.Identifier,
|
|
762
|
+
module: cast<never>(a.Parameters.FlowModuleId),
|
|
763
|
+
next: w.next,
|
|
764
|
+
onError: w.onError,
|
|
765
|
+
}),
|
|
766
|
+
};
|
|
767
|
+
},
|
|
768
|
+
|
|
769
|
+
[ActionType.InvokeLambdaFunction]: (a, ctx) => {
|
|
770
|
+
const w = wiredTransitions(a.Transitions, NO_MATCHING_ERROR);
|
|
771
|
+
if (w === undefined) return undefined;
|
|
772
|
+
const p = a.Parameters;
|
|
773
|
+
if (
|
|
774
|
+
!paramKeysAre(
|
|
775
|
+
p,
|
|
776
|
+
["LambdaFunctionARN", "InvocationTimeLimitSeconds", "InvocationType"],
|
|
777
|
+
["LambdaInvocationAttributes", "ResponseValidation"],
|
|
778
|
+
)
|
|
779
|
+
) {
|
|
780
|
+
return undefined;
|
|
781
|
+
}
|
|
782
|
+
const ref = refSource(p.LambdaFunctionARN, "lambda", ctx);
|
|
783
|
+
if (ref === undefined) return undefined;
|
|
784
|
+
const timeout = p.InvocationTimeLimitSeconds;
|
|
785
|
+
if (typeof timeout !== "number") return undefined;
|
|
786
|
+
const invocationType = p.InvocationType;
|
|
787
|
+
if (invocationType !== "SYNCHRONOUS" && invocationType !== "ASYNCHRONOUS") return undefined;
|
|
788
|
+
const entries: [string, V][] = [
|
|
789
|
+
["id", a.Identifier],
|
|
790
|
+
["lambda", ref],
|
|
791
|
+
["timeoutSeconds", timeout],
|
|
792
|
+
];
|
|
793
|
+
if (invocationType === "ASYNCHRONOUS") entries.push(["invocationType", invocationType]);
|
|
794
|
+
const attributes = p.LambdaInvocationAttributes;
|
|
795
|
+
if (attributes !== undefined) {
|
|
796
|
+
if (!isStringMap(attributes)) return undefined;
|
|
797
|
+
entries.push(["attributes", toV(attributes)]);
|
|
798
|
+
}
|
|
799
|
+
let responseType: "STRING_MAP" | "JSON" | undefined;
|
|
800
|
+
if (p.ResponseValidation !== undefined) {
|
|
801
|
+
const rv = p.ResponseValidation as Record<string, unknown> | null;
|
|
802
|
+
if (rv === null || typeof rv !== "object" || !paramKeysAre(rv, ["ResponseType"])) {
|
|
803
|
+
return undefined;
|
|
804
|
+
}
|
|
805
|
+
if (rv.ResponseType !== "STRING_MAP" && rv.ResponseType !== "JSON") return undefined;
|
|
806
|
+
responseType = rv.ResponseType;
|
|
807
|
+
entries.push(["responseType", responseType]);
|
|
808
|
+
}
|
|
809
|
+
entries.push(["next", w.next], ["onError", w.onError]);
|
|
810
|
+
return {
|
|
811
|
+
cls: "InvokeLambdaFunction",
|
|
812
|
+
entries,
|
|
813
|
+
block: new InvokeLambdaFunction({
|
|
814
|
+
id: a.Identifier,
|
|
815
|
+
lambda: cast<never>(p.LambdaFunctionARN),
|
|
816
|
+
timeoutSeconds: timeout,
|
|
817
|
+
...(invocationType === "ASYNCHRONOUS" ? { invocationType } : {}),
|
|
818
|
+
...(attributes === undefined
|
|
819
|
+
? {}
|
|
820
|
+
: { attributes: cast<Record<string, string>>(attributes) }),
|
|
821
|
+
...(responseType === undefined ? {} : { responseType }),
|
|
822
|
+
next: w.next,
|
|
823
|
+
onError: w.onError,
|
|
824
|
+
}),
|
|
825
|
+
};
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
function invertGeneric(a: FlowAction): Inversion {
|
|
830
|
+
const entries: [string, V][] = [
|
|
831
|
+
["id", a.Identifier],
|
|
832
|
+
["type", a.Type],
|
|
833
|
+
];
|
|
834
|
+
const cfg: GenericBlockConfig = { id: a.Identifier, type: a.Type };
|
|
835
|
+
// Connect omits Parameters and could omit Transitions on an action that has
|
|
836
|
+
// none, and a document read off disk is not guaranteed to have been through
|
|
837
|
+
// export's normalization. GenericBlock itself already defaults both, so
|
|
838
|
+
// matching it here keeps an unmodeled action degrading rather than throwing
|
|
839
|
+
// "Cannot convert undefined or null to object".
|
|
840
|
+
const parameters = a.Parameters ?? {};
|
|
841
|
+
if (Object.keys(parameters).length > 0) {
|
|
842
|
+
entries.push(["parameters", toV(parameters)]);
|
|
843
|
+
cfg.parameters = parameters;
|
|
844
|
+
}
|
|
845
|
+
const t = a.Transitions ?? {};
|
|
846
|
+
if (t.NextAction !== undefined) {
|
|
847
|
+
entries.push(["next", t.NextAction]);
|
|
848
|
+
cfg.next = t.NextAction;
|
|
849
|
+
}
|
|
850
|
+
const errors = t.Errors ?? [];
|
|
851
|
+
if (errors.length > 0) {
|
|
852
|
+
entries.push([
|
|
853
|
+
"errors",
|
|
854
|
+
new ArrV(
|
|
855
|
+
errors.map(
|
|
856
|
+
(e: ErrorTransition) =>
|
|
857
|
+
new ObjV([
|
|
858
|
+
["errorType", e.ErrorType],
|
|
859
|
+
["target", e.NextAction],
|
|
860
|
+
]),
|
|
861
|
+
),
|
|
862
|
+
),
|
|
863
|
+
]);
|
|
864
|
+
cfg.errors = errors.map((e) => ({ errorType: e.ErrorType, target: e.NextAction }));
|
|
865
|
+
}
|
|
866
|
+
const conditions = t.Conditions ?? [];
|
|
867
|
+
if (conditions.length > 0) {
|
|
868
|
+
entries.push([
|
|
869
|
+
"conditions",
|
|
870
|
+
new ArrV(
|
|
871
|
+
conditions.map(
|
|
872
|
+
(c) =>
|
|
873
|
+
new ObjV([
|
|
874
|
+
["target", c.NextAction],
|
|
875
|
+
["operator", c.Condition.Operator],
|
|
876
|
+
["operands", new ArrV([...c.Condition.Operands])],
|
|
877
|
+
]),
|
|
878
|
+
),
|
|
879
|
+
),
|
|
880
|
+
]);
|
|
881
|
+
cfg.conditions = conditions.map((c) => ({
|
|
882
|
+
target: c.NextAction,
|
|
883
|
+
operator: c.Condition.Operator,
|
|
884
|
+
operands: c.Condition.Operands,
|
|
885
|
+
}));
|
|
886
|
+
}
|
|
887
|
+
return { cls: "GenericBlock", entries, block: new GenericBlock(cfg) };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/** Exact inversion or the GenericBlock fallback; never lossy. */
|
|
891
|
+
function invertAction(a: FlowAction, ctx: Ctx): Inversion {
|
|
892
|
+
const inverter = INVERTERS[a.Type];
|
|
893
|
+
if (inverter !== undefined) {
|
|
894
|
+
try {
|
|
895
|
+
const scratch: Ctx = { refs: false, jsonPath: false };
|
|
896
|
+
const candidate = inverter(a, scratch);
|
|
897
|
+
if (candidate !== undefined && stableJson(candidate.block.toAction()) === stableJson(a)) {
|
|
898
|
+
ctx.refs = ctx.refs || scratch.refs;
|
|
899
|
+
ctx.jsonPath = ctx.jsonPath || scratch.jsonPath;
|
|
900
|
+
return candidate;
|
|
901
|
+
}
|
|
902
|
+
} catch {
|
|
903
|
+
// Constructor validation rejected the shape; fall through to generic.
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return invertGeneric(a);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// ---------------------------------------------------------------------------
|
|
910
|
+
// @keep comments
|
|
911
|
+
// ---------------------------------------------------------------------------
|
|
912
|
+
|
|
913
|
+
interface KeepComments {
|
|
914
|
+
beforeExport: string[];
|
|
915
|
+
beforeBlock: Map<string, string[]>;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const NEW_BLOCK = /^new [A-Za-z_$][A-Za-z0-9_$]*\(\{/;
|
|
919
|
+
const BLOCK_ID = /\bid: (["'])((?:[^"'\\]|\\.)*)\1/;
|
|
920
|
+
|
|
921
|
+
function extractKeepComments(previous: string): KeepComments {
|
|
922
|
+
const out: KeepComments = { beforeExport: [], beforeBlock: new Map() };
|
|
923
|
+
const lines = previous.split("\n");
|
|
924
|
+
let pending: string[] = [];
|
|
925
|
+
for (let i = 0; i < lines.length; i++) {
|
|
926
|
+
const trimmed = lines[i]!.trim();
|
|
927
|
+
if (trimmed.startsWith("//")) {
|
|
928
|
+
pending.push(trimmed);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
const keeps = pending.filter((l) => l.includes("@keep"));
|
|
932
|
+
pending = [];
|
|
933
|
+
if (keeps.length === 0) continue;
|
|
934
|
+
if (trimmed.startsWith("export function ")) {
|
|
935
|
+
out.beforeExport = keeps;
|
|
936
|
+
} else if (NEW_BLOCK.test(trimmed)) {
|
|
937
|
+
// The id property is on this line or within the next few.
|
|
938
|
+
for (let j = i; j < Math.min(i + 5, lines.length); j++) {
|
|
939
|
+
const m = BLOCK_ID.exec(lines[j]!);
|
|
940
|
+
if (m !== null) {
|
|
941
|
+
out.beforeBlock.set(m[2]!, keeps);
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return out;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// ---------------------------------------------------------------------------
|
|
951
|
+
// Naming
|
|
952
|
+
// ---------------------------------------------------------------------------
|
|
953
|
+
|
|
954
|
+
const RESERVED_WORDS = new Set([
|
|
955
|
+
"await",
|
|
956
|
+
"break",
|
|
957
|
+
"case",
|
|
958
|
+
"catch",
|
|
959
|
+
"class",
|
|
960
|
+
"const",
|
|
961
|
+
"continue",
|
|
962
|
+
"debugger",
|
|
963
|
+
"default",
|
|
964
|
+
"delete",
|
|
965
|
+
"do",
|
|
966
|
+
"else",
|
|
967
|
+
"enum",
|
|
968
|
+
"export",
|
|
969
|
+
"extends",
|
|
970
|
+
"false",
|
|
971
|
+
"finally",
|
|
972
|
+
"for",
|
|
973
|
+
"function",
|
|
974
|
+
"if",
|
|
975
|
+
"implements",
|
|
976
|
+
"import",
|
|
977
|
+
"in",
|
|
978
|
+
"instanceof",
|
|
979
|
+
"interface",
|
|
980
|
+
"let",
|
|
981
|
+
"new",
|
|
982
|
+
"null",
|
|
983
|
+
"package",
|
|
984
|
+
"private",
|
|
985
|
+
"protected",
|
|
986
|
+
"public",
|
|
987
|
+
"return",
|
|
988
|
+
"static",
|
|
989
|
+
"super",
|
|
990
|
+
"switch",
|
|
991
|
+
"this",
|
|
992
|
+
"throw",
|
|
993
|
+
"true",
|
|
994
|
+
"try",
|
|
995
|
+
"typeof",
|
|
996
|
+
"var",
|
|
997
|
+
"void",
|
|
998
|
+
"while",
|
|
999
|
+
"with",
|
|
1000
|
+
"yield",
|
|
1001
|
+
]);
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* The exported factory's name: camelCase of the flow name, prefixed with
|
|
1005
|
+
* "flow" when it would start with a digit, hit a reserved word, or collide
|
|
1006
|
+
* with an identifier the file imports (a flow named "json-path" must not emit
|
|
1007
|
+
* `export function jsonPath` next to `import { jsonPath }`).
|
|
1008
|
+
*/
|
|
1009
|
+
export function factoryName(name: string, taken: ReadonlySet<string> = new Set()): string {
|
|
1010
|
+
const parts = name.split(/[^A-Za-z0-9]+/).filter((p) => p.length > 0);
|
|
1011
|
+
if (parts.length === 0) {
|
|
1012
|
+
throw new Error(`Cannot derive a factory name from flow name "${name}".`);
|
|
1013
|
+
}
|
|
1014
|
+
let candidate = parts.map((p, i) => (i === 0 ? p : p[0]!.toUpperCase() + p.slice(1))).join("");
|
|
1015
|
+
while (/^[0-9]/.test(candidate) || RESERVED_WORDS.has(candidate) || taken.has(candidate)) {
|
|
1016
|
+
candidate = `flow${candidate[0]!.toUpperCase()}${candidate.slice(1)}`;
|
|
1017
|
+
}
|
|
1018
|
+
return candidate;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// ---------------------------------------------------------------------------
|
|
1022
|
+
// Assembly
|
|
1023
|
+
// ---------------------------------------------------------------------------
|
|
1024
|
+
|
|
1025
|
+
function flowConfigEntries(doc: FlowDoc): [string, V][] {
|
|
1026
|
+
const entries: [string, V][] = [["name", doc.name]];
|
|
1027
|
+
if (doc.kind !== "module") entries.push(["connectType", doc.connectType]);
|
|
1028
|
+
// A module's empty Settings is synth's default, so it is not worth emitting;
|
|
1029
|
+
// a non-empty one must be, or synth would re-default it and the round trip
|
|
1030
|
+
// would silently lose module configuration.
|
|
1031
|
+
const settings = doc.content.Settings;
|
|
1032
|
+
if (doc.kind === "module" && settings !== undefined && Object.keys(settings).length > 0) {
|
|
1033
|
+
entries.push(["settings", toV(settings)]);
|
|
1034
|
+
}
|
|
1035
|
+
const first = doc.content.Actions[0];
|
|
1036
|
+
if (first !== undefined && doc.content.StartAction !== first.Identifier) {
|
|
1037
|
+
entries.push(["start", doc.content.StartAction]);
|
|
1038
|
+
}
|
|
1039
|
+
// Layout equal to deterministic auto-layout is a derivable default; only
|
|
1040
|
+
// hand-placed positions are worth carrying in source. Either way
|
|
1041
|
+
// synth(codegen(doc)) reproduces doc.layout.
|
|
1042
|
+
if (doc.layout !== undefined) {
|
|
1043
|
+
const auto = autoLayout(doc.content.Actions);
|
|
1044
|
+
if (stableJson(doc.layout) !== stableJson(auto)) {
|
|
1045
|
+
const layoutEntries = Object.keys(doc.layout)
|
|
1046
|
+
.sort()
|
|
1047
|
+
.map((id): [string, V] => {
|
|
1048
|
+
const p = doc.layout![id] as Point;
|
|
1049
|
+
return [
|
|
1050
|
+
id,
|
|
1051
|
+
new ObjV([
|
|
1052
|
+
["x", p.x],
|
|
1053
|
+
["y", p.y],
|
|
1054
|
+
]),
|
|
1055
|
+
];
|
|
1056
|
+
});
|
|
1057
|
+
entries.push(["layout", new ObjV(layoutEntries)]);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return entries;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function renderBlock(inv: Inversion, indent: string): string {
|
|
1064
|
+
const head = `new ${inv.cls}(`;
|
|
1065
|
+
if (inv.entries.length === 1) {
|
|
1066
|
+
const flat = inlineV(new ObjV(inv.entries));
|
|
1067
|
+
if (flat !== undefined && indent.length + head.length + flat.length + 2 <= PRINT_WIDTH) {
|
|
1068
|
+
return `${head}${flat})`;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return `${head}${printBrokenObject(inv.entries, indent)})`;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** A block's single-line form, or undefined when it must break. */
|
|
1075
|
+
function inlineBlock(inv: Inversion): string | undefined {
|
|
1076
|
+
if (inv.entries.length !== 1) return undefined;
|
|
1077
|
+
const flat = inlineV(new ObjV(inv.entries));
|
|
1078
|
+
return flat === undefined ? undefined : `new ${inv.cls}(${flat})`;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
export function codegen(doc: FlowDoc, options: CodegenOptions = {}): string {
|
|
1082
|
+
assertFlowDoc(doc, "codegen");
|
|
1083
|
+
if (doc.kind === "module" && doc.connectType !== "MODULE") {
|
|
1084
|
+
throw new Error(`FlowDoc kind "module" requires connectType MODULE, got ${doc.connectType}.`);
|
|
1085
|
+
}
|
|
1086
|
+
if (doc.content.Actions.length === 0) {
|
|
1087
|
+
throw new Error(`Flow "${doc.name}" has no actions; nothing to generate.`);
|
|
1088
|
+
}
|
|
1089
|
+
const spec = options.moduleSpecifier ?? DEFAULT_MODULE_SPECIFIER;
|
|
1090
|
+
const keep =
|
|
1091
|
+
options.previous === undefined
|
|
1092
|
+
? { beforeExport: [], beforeBlock: new Map<string, string[]>() }
|
|
1093
|
+
: extractKeepComments(options.previous);
|
|
1094
|
+
|
|
1095
|
+
const ctx: Ctx = { refs: false, jsonPath: false };
|
|
1096
|
+
const inversions = doc.content.Actions.map((a) => invertAction(a, ctx));
|
|
1097
|
+
|
|
1098
|
+
const flowClass = doc.kind === "module" ? "FlowModule" : "Flow";
|
|
1099
|
+
const imports = new Set<string>([flowClass, ...inversions.map((i) => i.cls)]);
|
|
1100
|
+
if (ctx.refs) imports.add("Refs");
|
|
1101
|
+
if (ctx.jsonPath) imports.add("jsonPath");
|
|
1102
|
+
const names = [...imports].sort();
|
|
1103
|
+
|
|
1104
|
+
const importInline = `import { ${names.join(", ")} } from ${quoteString(spec)};`;
|
|
1105
|
+
const importSource =
|
|
1106
|
+
importInline.length <= PRINT_WIDTH
|
|
1107
|
+
? importInline
|
|
1108
|
+
: `import {\n${names.map((n) => ` ${n},`).join("\n")}\n} from ${quoteString(spec)};`;
|
|
1109
|
+
|
|
1110
|
+
const fn = factoryName(doc.name, imports);
|
|
1111
|
+
const cfgEntries = flowConfigEntries(doc);
|
|
1112
|
+
|
|
1113
|
+
// The whole return statement collapses to one line when Prettier would:
|
|
1114
|
+
// every argument fits inline and the line stays within the print width.
|
|
1115
|
+
const cfgInline = inlineV(new ObjV(cfgEntries));
|
|
1116
|
+
const argInlines = inversions.map(inlineBlock);
|
|
1117
|
+
const hasBlockComments = inversions.some((i) => keep.beforeBlock.has(i.block.id));
|
|
1118
|
+
let body: string;
|
|
1119
|
+
const oneLine =
|
|
1120
|
+
cfgInline === undefined || argInlines.some((a) => a === undefined)
|
|
1121
|
+
? undefined
|
|
1122
|
+
: ` return new ${flowClass}(${cfgInline}).add(${argInlines.join(", ")});`;
|
|
1123
|
+
if (oneLine !== undefined && oneLine.length <= PRINT_WIDTH && !hasBlockComments) {
|
|
1124
|
+
body = oneLine;
|
|
1125
|
+
} else {
|
|
1126
|
+
const openInline =
|
|
1127
|
+
cfgInline === undefined ? undefined : ` return new ${flowClass}(${cfgInline}).add(`;
|
|
1128
|
+
const open =
|
|
1129
|
+
openInline !== undefined && openInline.length <= PRINT_WIDTH
|
|
1130
|
+
? openInline
|
|
1131
|
+
: ` return new ${flowClass}(${printBrokenObject(cfgEntries, " ")}).add(`;
|
|
1132
|
+
const args: string[] = [];
|
|
1133
|
+
for (const inv of inversions) {
|
|
1134
|
+
for (const c of keep.beforeBlock.get(inv.block.id) ?? []) args.push(` ${c}`);
|
|
1135
|
+
args.push(` ${renderBlock(inv, " ")},`);
|
|
1136
|
+
}
|
|
1137
|
+
body = `${open}\n${args.join("\n")}\n );`;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const lines: string[] = [...generatedMarker(doc.name), "", importSource, ""];
|
|
1141
|
+
for (const c of keep.beforeExport) lines.push(c);
|
|
1142
|
+
lines.push(`export function ${fn}(): ${flowClass} {`, body, "}");
|
|
1143
|
+
return lines.join("\n") + "\n";
|
|
1144
|
+
}
|