@flow-as-code/tf
npm i @flow-as-code/tf
Apache-2.0, Node 22.12 or newer.
Site and docs: https://flow-as-code.dev/. This package on npm: https://www.npmjs.com/package/@flow-as-code/tf.
Implements docs/03-tf-emitter.md. Deterministic HCL emission, terraform validate-clean when the address map is complete, loud TODO placeholders when it is not. Never emits literal ARNs, provider blocks, or backend config.
One-way in v1: FlowDoc in, HCL out. Reading HCL back into FlowDoc needs HCL evaluation and is deferred to the provider era.
Use
import { emitTf, writeTf } from "@flow-as-code/tf";
const { files } = emitTf([doc], {
addressMap: { "queue:front-desk": "aws_connect_queue.front_desk.arn" },
});
writeTf([doc], "infra/flows", { addressMap });
emitTf is pure: documents in, a relative path to content map out, sorted by path and byte-identical across runs. writeTf is the filesystem wrapper; it creates directories, overwrites the files it emits, and touches nothing else in the target directory.
@flow-as-code/tf/emit is the same emitter without the filesystem half. The package index re-exports writeTf, which imports node:fs, so a browser bundle (@flow-as-code/studio's Terraform export) imports the subpath instead.
Output
| File | What it holds |
|---|---|
flows/<name>.flow.tftpl |
Deployable Flow language JSON, each reference replaced by a templatefile interpolation |
flows.tf |
aws_connect_contact_flow / aws_connect_contact_flow_module, plus awscc module version and alias resources |
flow_refs.tf |
locals holding the terraform address behind every reference |
variables.tf |
The Connect instance id variable, unless instanceIdExpression supplies one |
versions.tf.example |
Required providers and versions, as an example; not loaded by either tool |
Provider configuration, credentials, and backend configuration are yours. The emitter writes none of them, by rule.
References
Every ${cdref:type:name} token becomes a template variable named <type>_<name>_arn, hyphens as underscores, with a module alias folded in: queue_front_desk_arn, module_survey_prod_arn. Addresses come from three places, in this order:
- The document set itself. A
${cdref:module:x@prod}whose modulexis emitted here resolves toawscc_connect_contact_flow_module_alias.x_prod.contact_flow_module_alias_arn; a${cdref:flow:y}whose flowyis emitted here resolves to that flow. An address map entry for one of these is ignored, and the emitted file says so on the line above. - The address map. Keys are a token (
${cdref:queue:front-desk}), the token body (queue:front-desk,module:survey@prod), or the variable name (queue_front_desk_arn). Values are HCL address expressions. A literal ARN is refused, as is anything that would comment out or overrun its line. Entries matching no reference in the set are ignored, so one shared map can serve several flow sets. - Nothing. The entry becomes
TODO_MISSING_ADDRESS_<type>_<name>with a# TODOnaming the reference. That is an undeclared reference, soterraform validateandtofu validatefail loudly rather than an apply writing a broken flow.
Addresses of resources the set emits itself live in a per-document local (merge(local.flow_refs, { ... })) rather than in the shared map. Putting them in the shared map makes the map depend on the resources rendered from it, and both tools report that as a dependency cycle. Two flows that reference each other are still a cycle, because it is one.
Escaping
The .tftpl body is JSON containing caller text, and caller text legitimately contains ${ and %{, the two sequences HCL reads as template introducers. Every one of them is escaped ($${, %%{) and only the emitter's own reference interpolations are left live. The substitution runs through unique sentinels chosen so they cannot collide with the document, so text that looks like a sentinel is caller text and stays caller text. conformance/emit-tf/hostile-text holds the fixture, and the gated tests render it with a real tofu and compare against a reference implementation of the template grammar.
Versions and aliases
Flow versioning is not available: CreateContactFlowVersion "only supports creating versions for flows of type Campaign" (API reference). Flows update their content in place. Modules get awscc_connect_contact_flow_module_version and awscc_connect_contact_flow_module_alias, which is how a caller pins the module content it invokes. A version snapshots content at create time, so a module content change needs the version resource replaced for aliases to serve it.
Tests
npm test runs the goldens in conformance/emit-tf and the unit tests. The tests that run a real tofu init, tofu validate, tofu fmt, and tofu console need the network and a few minutes on a cold provider cache, so they are gated behind RUN_TOFU_VALIDATE=1:
RUN_TOFU_VALIDATE=1 npx vitest run --project @flow-as-code/tf
CI runs them in the emit-tf job, and an ungated test asserts that the job still sets the variable. Regenerate goldens deliberately with UPDATE_GOLDENS=1 and read the diff.