@flow-as-code/cdk 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.
@@ -0,0 +1,275 @@
1
+ /*
2
+ * Copyright 2026 The flow-as-code Authors
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ // FlowSet: a construct that turns a set of FlowDocs into deployable
6
+ // AWS::Connect resources.
7
+ //
8
+ // Flows become AWS::Connect::ContactFlow and modules become
9
+ // AWS::Connect::ContactFlowModule, with content produced by @flow-as-code/core's
10
+ // materializeWithBinder and serializeContent so the deployed JSON is
11
+ // byte-stable for the same inputs.
12
+ // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-connect-contactflow.html
13
+ // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-connect-contactflowmodule.html
14
+ //
15
+ // Versioning model:
16
+ // - Modules are versioned. Each module gets an AWS::Connect::ContactFlowModuleVersion
17
+ // whose logical ID embeds a hash of the module's canonical content, so a
18
+ // content change replaces the version resource and publishes a new immutable
19
+ // version, and an AWS::Connect::ContactFlowModuleAlias per alias name used by
20
+ // the doc set (default "live") whose logical ID is stable and which repoints
21
+ // to the new version. Flows reference the ALIAS ARN, so repointing an alias
22
+ // to a new version changes no flow content.
23
+ // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-connect-contactflowmoduleversion.html
24
+ // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-connect-contactflowmodulealias.html
25
+ // - Full flows are NOT versioned; their content updates in place. Connect's
26
+ // CreateContactFlowVersion API states: "This API only supports creating
27
+ // versions for flows of type Campaign."
28
+ // https://docs.aws.amazon.com/connect/latest/APIReference/API_CreateContactFlowVersion.html
29
+ // (verified 2026-08-31)
30
+
31
+ import { createHash } from "node:crypto";
32
+ import { readFileSync, readdirSync } from "node:fs";
33
+ import { join } from "node:path";
34
+
35
+ import {
36
+ allRules,
37
+ collectRefs,
38
+ lint,
39
+ materializeWithBinder,
40
+ serializeContent,
41
+ toText,
42
+ type FlowDoc,
43
+ type RefEntry,
44
+ } from "@flow-as-code/core";
45
+ import {
46
+ CfnContactFlow,
47
+ CfnContactFlowModule,
48
+ CfnContactFlowModuleAlias,
49
+ CfnContactFlowModuleVersion,
50
+ } from "aws-cdk-lib/aws-connect";
51
+ import { Construct } from "constructs";
52
+
53
+ import { bindRef, type TokenBinder } from "./binder.js";
54
+
55
+ /** Alias created for a module no flow pins to a named alias. */
56
+ export const DEFAULT_MODULE_ALIAS = "live";
57
+
58
+ export interface FlowSetProps {
59
+ /** ARN of the Connect instance the flows deploy into. */
60
+ instanceArn: string;
61
+ /**
62
+ * Either a directory containing `*.flowdoc.json` files (read at synth time,
63
+ * sorted by file name) or the documents themselves.
64
+ */
65
+ source: string | FlowDoc[];
66
+ /** Resolves non-module references to CloudFormation-token strings. */
67
+ binder: TokenBinder;
68
+ }
69
+
70
+ /** `name@alias` key for a module alias resource. */
71
+ const aliasKey = (name: string, alias: string | undefined): string =>
72
+ `${name}@${alias ?? DEFAULT_MODULE_ALIAS}`;
73
+
74
+ function loadDocs(source: string | FlowDoc[]): FlowDoc[] {
75
+ if (Array.isArray(source)) return [...source];
76
+ const files = readdirSync(source)
77
+ .filter((f) => f.endsWith(".flowdoc.json"))
78
+ .sort();
79
+ if (files.length === 0) {
80
+ throw new Error(`FlowSet source directory "${source}" contains no *.flowdoc.json files.`);
81
+ }
82
+ return files.map((f) => JSON.parse(readFileSync(join(source, f), "utf8")) as FlowDoc);
83
+ }
84
+
85
+ /**
86
+ * Modules ordered so every module is created after the modules it references
87
+ * ("up to five levels" of nesting, per the flow language contract). Stable:
88
+ * ties break on name. Throws on a reference cycle, which Connect could never
89
+ * execute anyway.
90
+ */
91
+ function topoSortModules(modules: FlowDoc[]): FlowDoc[] {
92
+ const byName = new Map(modules.map((m) => [m.name, m]));
93
+ const deps = new Map<string, string[]>(
94
+ modules.map((m) => [
95
+ m.name,
96
+ collectRefs(m.content)
97
+ .filter((r) => r.type === "module" && byName.has(r.name))
98
+ .map((r) => r.name)
99
+ .sort(),
100
+ ]),
101
+ );
102
+
103
+ const done = new Set<string>();
104
+ const visiting = new Set<string>();
105
+ const ordered: FlowDoc[] = [];
106
+ const visit = (name: string, path: string[]): void => {
107
+ if (done.has(name)) return;
108
+ if (visiting.has(name)) {
109
+ throw new Error(`Module reference cycle: ${[...path, name].join(" -> ")}.`);
110
+ }
111
+ visiting.add(name);
112
+ for (const dep of deps.get(name) ?? []) visit(dep, [...path, name]);
113
+ visiting.delete(name);
114
+ done.add(name);
115
+ ordered.push(byName.get(name) as FlowDoc);
116
+ };
117
+ for (const m of [...modules].sort((a, b) => a.name.localeCompare(b.name))) visit(m.name, []);
118
+ return ordered;
119
+ }
120
+
121
+ /**
122
+ * Creates every flow and module in a FlowDoc set on a Connect instance, with
123
+ * references resolved to CloudFormation tokens: non-module refs through the
124
+ * user's TokenBinder, module refs to the alias ARN of the module this
125
+ * construct manages.
126
+ */
127
+ export class FlowSet extends Construct {
128
+ /** Created contact flows, keyed by document name. */
129
+ readonly flows: ReadonlyMap<string, CfnContactFlow>;
130
+ /** Created modules, keyed by document name. */
131
+ readonly modules: ReadonlyMap<string, CfnContactFlowModule>;
132
+ /** Created module aliases, keyed by `name@alias`. */
133
+ readonly moduleAliases: ReadonlyMap<string, CfnContactFlowModuleAlias>;
134
+
135
+ constructor(scope: Construct, id: string, props: FlowSetProps) {
136
+ super(scope, id);
137
+
138
+ const docs = loadDocs(props.source);
139
+ this.validateDocs(docs);
140
+
141
+ const moduleDocs = docs.filter((d) => d.kind === "module");
142
+ const flowDocs = docs
143
+ .filter((d) => d.kind === "flow")
144
+ .sort((a, b) => a.name.localeCompare(b.name));
145
+ const moduleNames = new Set(moduleDocs.map((d) => d.name));
146
+
147
+ // Which aliases each module needs: every alias the doc set pins, plus the
148
+ // default so an unreferenced module still deploys with a usable alias.
149
+ const wanted = new Map<string, Set<string>>(moduleDocs.map((d) => [d.name, new Set()]));
150
+ for (const doc of docs) {
151
+ for (const ref of collectRefs(doc.content)) {
152
+ if (ref.type !== "module") continue;
153
+ if (!moduleNames.has(ref.name)) {
154
+ throw new Error(
155
+ `"${doc.name}" references ${ref.token}, but no module named "${ref.name}" is in this FlowSet.`,
156
+ );
157
+ }
158
+ (wanted.get(ref.name) as Set<string>).add(ref.alias ?? DEFAULT_MODULE_ALIAS);
159
+ }
160
+ }
161
+ for (const aliases of wanted.values()) {
162
+ if (aliases.size === 0) aliases.add(DEFAULT_MODULE_ALIAS);
163
+ }
164
+
165
+ const flows = new Map<string, CfnContactFlow>();
166
+ const modules = new Map<string, CfnContactFlowModule>();
167
+ const aliases = new Map<string, CfnContactFlowModuleAlias>();
168
+
169
+ const materializeDoc = (
170
+ doc: FlowDoc,
171
+ ): { content: string; used: CfnContactFlowModuleAlias[] } => {
172
+ const used: CfnContactFlowModuleAlias[] = [];
173
+ const content = materializeWithBinder(doc, (ref: RefEntry): string => {
174
+ if (ref.type !== "module") return bindRef(props.binder, ref, doc.name);
175
+ const alias = aliases.get(aliasKey(ref.name, ref.alias));
176
+ if (alias === undefined) {
177
+ // Unreachable for modules (topological order) and flows (created
178
+ // after every module); kept as a guard with a real message.
179
+ throw new Error(`Internal: alias for ${ref.token} not yet created (doc "${doc.name}").`);
180
+ }
181
+ used.push(alias);
182
+ return alias.attrContactFlowModuleAliasArn;
183
+ });
184
+ return { content: serializeContent(content), used };
185
+ };
186
+
187
+ // Modules first, dependency-ordered, each with its version and aliases.
188
+ for (const doc of topoSortModules(moduleDocs)) {
189
+ const { content, used } = materializeDoc(doc);
190
+ const module = new CfnContactFlowModule(this, `Module-${doc.name}`, {
191
+ instanceArn: props.instanceArn,
192
+ name: doc.name,
193
+ content,
194
+ });
195
+ for (const dep of used) module.addResourceDependency(dep);
196
+ modules.set(doc.name, module);
197
+
198
+ // The version's logical ID embeds the canonical content hash (computed
199
+ // over the doc with its own tokens left in place, so it is stable across
200
+ // processes): a content change replaces the resource, publishing a new
201
+ // immutable version; the alias below keeps a stable logical ID and
202
+ // repoints. See the header comment for the versioning model.
203
+ const discriminator = createHash("sha256")
204
+ .update(serializeContent(materializeWithBinder(doc, (r) => r.token)))
205
+ .digest("hex")
206
+ .slice(0, 8);
207
+ const version = new CfnContactFlowModuleVersion(
208
+ this,
209
+ `Module-${doc.name}-Version-${discriminator}`,
210
+ {
211
+ contactFlowModuleId: module.attrContactFlowModuleArn,
212
+ },
213
+ );
214
+ version.addResourceDependency(module);
215
+
216
+ for (const aliasName of [...(wanted.get(doc.name) as Set<string>)].sort()) {
217
+ const alias = new CfnContactFlowModuleAlias(this, `Module-${doc.name}-Alias-${aliasName}`, {
218
+ contactFlowModuleId: module.attrContactFlowModuleArn,
219
+ contactFlowModuleVersion: version.attrVersion,
220
+ name: aliasName,
221
+ });
222
+ alias.addResourceDependency(version);
223
+ aliases.set(aliasKey(doc.name, aliasName), alias);
224
+ }
225
+ }
226
+
227
+ // Then flows. Alias references inside content already imply the ordering;
228
+ // the explicit dependency keeps it true even if content is later composed
229
+ // differently.
230
+ for (const doc of flowDocs) {
231
+ const { content, used } = materializeDoc(doc);
232
+ const flow = new CfnContactFlow(this, `Flow-${doc.name}`, {
233
+ instanceArn: props.instanceArn,
234
+ name: doc.name,
235
+ type: doc.connectType,
236
+ content,
237
+ });
238
+ for (const dep of used) flow.addResourceDependency(dep);
239
+ flows.set(doc.name, flow);
240
+ }
241
+
242
+ this.flows = flows;
243
+ this.modules = modules;
244
+ this.moduleAliases = aliases;
245
+ }
246
+
247
+ /** Refuses duplicate names, kind mismatches, and hard lint findings. */
248
+ private validateDocs(docs: FlowDoc[]): void {
249
+ const seen = new Set<string>();
250
+ const duplicates = new Set<string>();
251
+ for (const d of docs) {
252
+ if (seen.has(d.name)) duplicates.add(d.name);
253
+ seen.add(d.name);
254
+ if (d.kind === "module" && d.connectType !== "MODULE") {
255
+ throw new Error(`Module "${d.name}" must have connectType MODULE, got ${d.connectType}.`);
256
+ }
257
+ if (d.kind === "flow" && d.connectType === "MODULE") {
258
+ throw new Error(`Flow "${d.name}" cannot have connectType MODULE.`);
259
+ }
260
+ }
261
+ if (duplicates.size > 0) {
262
+ throw new Error(
263
+ `Duplicate document name(s) in FlowSet: ${[...duplicates].sort().join(", ")}.`,
264
+ );
265
+ }
266
+
267
+ // Hard rules block deployment exactly as they block a studio save:
268
+ // no-literal-arn and no-unresolved-token (@flow-as-code/core lint).
269
+ const hard = new Set(allRules.filter((r) => r.hard).map((r) => r.id));
270
+ const blocking = lint(docs).filter((f) => hard.has(f.rule));
271
+ if (blocking.length > 0) {
272
+ throw new Error(`FlowSet refused ${blocking.length} document(s):\n${toText(blocking)}`);
273
+ }
274
+ }
275
+ }
package/src/index.ts ADDED
@@ -0,0 +1,51 @@
1
+ /*
2
+ * Copyright 2026 The flow-as-code Authors
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ // CDK binding for @flow-as-code/core. See README.md.
6
+ //
7
+ // TokenBinder maps FlowDoc references to CloudFormation-token strings;
8
+ // FlowSet turns a set of FlowDocs into AWS::Connect resources with module
9
+ // version publishing and alias repointing.
10
+ //
11
+ // aws-cdk-lib and constructs are OPTIONAL peers, so that installing @flow-as-code/cli
12
+ // (which only reaches this package through the ./scaffold subpath, and that
13
+ // subpath imports no CDK) does not drag 168 MB of CDK in. The cost is that npm
14
+ // no longer warns at install time, so a consumer who forgets them used to meet
15
+ // a bare ERR_MODULE_NOT_FOUND for aws-cdk-lib raised from flow-set.js. ESM
16
+ // resolves the whole graph before any module body runs, so the only way to
17
+ // front-run that is to keep flow-set.js off the static import graph: this
18
+ // module checks the peer itself and then loads flow-set through a dynamic
19
+ // import. The type-only imports below are erased and pull in nothing.
20
+
21
+ import { createRequire } from "node:module";
22
+
23
+ import { PACKAGE_NAMES } from "@flow-as-code/core";
24
+
25
+ import type { FlowSet as FlowSetClass, FlowSetProps } from "./flow-set.js";
26
+
27
+ export { bindRef, type TokenBinder } from "./binder.js";
28
+ export type { FlowSetProps };
29
+
30
+ /** Peer floor, kept in step with peerDependencies in package.json. */
31
+ const CDK_PEERS = "aws-cdk-lib@^2.267.0 constructs@^10.8.1";
32
+
33
+ function assertCdkPeersInstalled(): void {
34
+ try {
35
+ createRequire(import.meta.url).resolve("aws-cdk-lib");
36
+ } catch {
37
+ throw new Error(
38
+ `${PACKAGE_NAMES.cdk} needs the CDK at runtime, but aws-cdk-lib is not installed. ` +
39
+ "It and constructs are optional peers so that a CLI-only install stays small, " +
40
+ `which means npm does not warn about them at install time. Install them with: npm install ${CDK_PEERS}`,
41
+ );
42
+ }
43
+ }
44
+
45
+ assertCdkPeersInstalled();
46
+
47
+ const flowSet = await import("./flow-set.js");
48
+
49
+ export const DEFAULT_MODULE_ALIAS: string = flowSet.DEFAULT_MODULE_ALIAS;
50
+ export const FlowSet: typeof FlowSetClass = flowSet.FlowSet;
51
+ export type FlowSet = FlowSetClass;
@@ -0,0 +1,193 @@
1
+ /*
2
+ * Copyright 2026 The flow-as-code Authors
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ // The CDK stack scaffold, shared by `flow-cli emit --target cdk` and the
6
+ // studio's CDK export button.
7
+ //
8
+ // @flow-as-code/cdk is a library, not a code generator: FlowSet reads the FlowDoc
9
+ // directory at synth time, so there is nothing per-document to emit. What a
10
+ // user actually has to write by hand is the stack that instantiates it and,
11
+ // above all, the TokenBinder, whose method set depends on which reference types
12
+ // their documents use. That is what this generates: a valid, compiling starting
13
+ // point with a TODO on exactly the binder methods the document set needs.
14
+ //
15
+ // It lives in the package whose API it writes, and is published as
16
+ // `@flow-as-code/cdk/scaffold`, so the CLI and the studio emit the
17
+ // same bytes instead of two implementations that drift. The studio cannot
18
+ // import the CLI (the CLI depends on the studio for its built assets) and
19
+ // parity between the two is the point of the feature, so a shared module is
20
+ // the only arrangement that holds.
21
+ //
22
+ // Pure by contract: no node builtins, no filesystem, no clock, no randomness,
23
+ // because this module is bundled into a browser. The one calculation that
24
+ // needs a path, the FLOW_DOCS expression, is passed in as `source`: the CLI
25
+ // computes it with node:path and the studio with `sourceForDepth` below.
26
+ //
27
+ // Deterministic: same documents in, byte-identical TypeScript out. Reference
28
+ // types and names are sorted, and nothing in the output depends on the order
29
+ // files were read.
30
+ //
31
+ // FlowSet resolves `${cdref:module:...}` itself against the modules it manages,
32
+ // so `module` never appears on a binder (packages/cdk/src/binder.ts).
33
+
34
+ import { type FlowDoc, PACKAGE_NAMES, type RefType, collectRefs } from "@flow-as-code/core";
35
+
36
+ /** Basename of the file the scaffold is written to. */
37
+ export const CDK_SCAFFOLD_FILE = "flow-stack.ts";
38
+
39
+ /** Required TokenBinder methods, in the order they are emitted. */
40
+ const REQUIRED_TYPES = ["hours", "lambda", "lex", "prompt", "queue"] as const;
41
+
42
+ /** What each binder method must return, echoing packages/cdk/src/binder.ts. */
43
+ const RETURNS: Record<string, string> = {
44
+ flow: "ARN of a contact flow not managed by this FlowSet",
45
+ hours: "ARN of the hours of operation, e.g. hours.attrHoursOfOperationArn",
46
+ lambda: "ARN of the Lambda function, e.g. fn.functionArn",
47
+ lex: "ARN of the Lex bot alias",
48
+ prompt: "ARN of the prompt",
49
+ queue: "ARN of the queue, e.g. queue.attrQueueArn",
50
+ };
51
+
52
+ /** What each reference type names, for the comment on an unused method. */
53
+ const NOUNS: Record<string, string> = {
54
+ flow: "contact flow",
55
+ hours: "hours of operation",
56
+ lambda: "Lambda function",
57
+ lex: "Lex bot alias",
58
+ prompt: "prompt",
59
+ queue: "queue",
60
+ };
61
+
62
+ /**
63
+ * Reference names per type across the whole set, sorted and deduplicated.
64
+ *
65
+ * A document's `refs` index is the authored answer, but it is derived data and
66
+ * nothing in the schema forces it to be current, so the content is scanned too
67
+ * and the two are unioned. A stale index can therefore only add a binder method
68
+ * the set does not strictly need, never drop one it does.
69
+ */
70
+ export function referencedNames(docs: readonly FlowDoc[]): Map<RefType, string[]> {
71
+ const byType = new Map<RefType, Set<string>>();
72
+ for (const doc of docs) {
73
+ for (const ref of [...(doc.refs ?? []), ...collectRefs(doc.content)]) {
74
+ if (ref.type === "module") continue;
75
+ const names = byType.get(ref.type) ?? new Set<string>();
76
+ names.add(ref.name);
77
+ byType.set(ref.type, names);
78
+ }
79
+ }
80
+ return new Map([...byType].map(([type, names]) => [type, [...names].sort()]));
81
+ }
82
+
83
+ /**
84
+ * The FLOW_DOCS expression for an output directory `depth` levels below the
85
+ * directory holding the FlowDocs: `.`, `..`, `../..`. This is what node's
86
+ * `path.relative` produces for the same pair, which is what the CLI passes;
87
+ * packages/studio/tests/exportParity.test.ts compares the two byte for
88
+ * byte on a real `flow-cli emit --target cdk --out` run.
89
+ */
90
+ export function sourceForDepth(depth: number): string {
91
+ if (depth <= 0) return ".";
92
+ return Array.from({ length: depth }, () => "..").join("/");
93
+ }
94
+
95
+ function binderMethod(type: string, names: readonly string[]): string[] {
96
+ const returns = RETURNS[type] ?? "ARN of the referenced resource";
97
+ const noun = NOUNS[type] ?? type;
98
+ const head =
99
+ names.length === 0
100
+ ? [` // No document in this set references a ${noun}.`]
101
+ : [` // TODO: return the ${returns}.`, ` // Names referenced: ${names.join(", ")}.`];
102
+ return [
103
+ ...head,
104
+ ` ${type}: (name) => {`,
105
+ names.length === 0
106
+ ? ` throw new Error(\`Unexpected ${type} reference "\${name}".\`);`
107
+ : ` throw new Error(\`TODO: bind ${type} "\${name}" to an ARN.\`);`,
108
+ " },",
109
+ ];
110
+ }
111
+
112
+ export interface CdkScaffoldInput {
113
+ docs: readonly FlowDoc[];
114
+ /**
115
+ * Where FlowSet reads the `*.flowdoc.json` files, relative to the scaffold:
116
+ * `.` when they sit beside it, `..` from a subdirectory. See sourceForDepth.
117
+ */
118
+ source: string;
119
+ }
120
+
121
+ /** The scaffold source. Pure: no filesystem access, no clock, no randomness. */
122
+ export function cdkScaffold({ docs, source }: CdkScaffoldInput): string {
123
+ const referenced = referencedNames(docs);
124
+ const types: RefType[] = [...REQUIRED_TYPES];
125
+ // `flow` is optional on TokenBinder, so it is emitted only when it is used.
126
+ if ((referenced.get("flow") ?? []).length > 0) types.unshift("flow");
127
+
128
+ const used = [...referenced]
129
+ .filter(([, names]) => names.length > 0)
130
+ .map(([type]) => type)
131
+ .sort();
132
+ const names = docs.map((d) => `${d.kind} ${d.name}`).sort();
133
+
134
+ // No licence header: this file lands in a user's project and is theirs, not
135
+ // ours. Same reasoning as codegen's banner (packages/core/src/codegen.ts).
136
+ const lines = [
137
+ "// Generated by `flow-cli emit --target cdk` as a starting point, then yours to",
138
+ "// keep: re-running the command overwrites this file, so move it or rename it",
139
+ "// once you have edited it.",
140
+ "//",
141
+ `// Documents in the set: ${names.join(", ")}.`,
142
+ "//",
143
+ "// FlowSet reads every *.flowdoc.json in the source directory at synth time and",
144
+ "// creates AWS::Connect::ContactFlow and AWS::Connect::ContactFlowModule",
145
+ "// resources, publishing a module version and repointing its alias whenever the",
146
+ `// module content changes. See the ${PACKAGE_NAMES.cdk} README.`,
147
+ "",
148
+ 'import { fileURLToPath } from "node:url";',
149
+ "",
150
+ 'import { Stack, type StackProps } from "aws-cdk-lib";',
151
+ `import { FlowSet, type TokenBinder } from "${PACKAGE_NAMES.cdk}";`,
152
+ 'import type { Construct } from "constructs";',
153
+ "",
154
+ "/**",
155
+ " * Directory holding the FlowDocs. Resolved against this file rather than the",
156
+ " * working directory, so `cdk synth` works from the project root, from here, or",
157
+ " * from anywhere else.",
158
+ " */",
159
+ `const FLOW_DOCS = fileURLToPath(new URL(${JSON.stringify(source)}, import.meta.url));`,
160
+ "",
161
+ "/**",
162
+ " * Resolves each reference token in the documents to a CloudFormation token,",
163
+ ` * normally a construct attribute. ${PACKAGE_NAMES.cdk} inserts what you`,
164
+ " * return byte for byte, so CDK tokens pass through and CloudFormation",
165
+ " * resolves them at deploy time. Never return a literal ARN here.",
166
+ " *",
167
+ used.length === 0
168
+ ? " * These documents contain no references."
169
+ : ` * These documents reference: ${used.join(", ")}.`,
170
+ " */",
171
+ "const binder: TokenBinder = {",
172
+ ...types.flatMap((type) => binderMethod(type, referenced.get(type) ?? [])),
173
+ "};",
174
+ "",
175
+ "export interface FlowStackProps extends StackProps {",
176
+ " /** ARN of the Amazon Connect instance these flows deploy into. */",
177
+ " instanceArn: string;",
178
+ "}",
179
+ "",
180
+ "export class FlowStack extends Stack {",
181
+ " constructor(scope: Construct, id: string, props: FlowStackProps) {",
182
+ " super(scope, id, props);",
183
+ "",
184
+ ' new FlowSet(this, "Flows", {',
185
+ " instanceArn: props.instanceArn,",
186
+ " source: FLOW_DOCS,",
187
+ " binder,",
188
+ " });",
189
+ " }",
190
+ "}",
191
+ ];
192
+ return lines.join("\n") + "\n";
193
+ }