@flow-as-code/tf 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,83 @@
1
+ /*
2
+ * Copyright 2026 The flow-as-code Authors
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ // Rendering FlowDoc content into a `templatefile` template (.tftpl).
6
+ //
7
+ // The body of a .tftpl is Connect Flow language JSON, and that JSON legitimately
8
+ // contains prompt text a caller wrote, which may include the two sequences HCL
9
+ // treats as template introducers: `${` (interpolation) and `%{` (directive).
10
+ // Both must survive rendering byte for byte, so both are escaped by doubling
11
+ // their leading character, per the HCL string template grammar:
12
+ // https://developer.hashicorp.com/terraform/language/expressions/strings#escape-sequences
13
+ // https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#template-expressions
14
+ //
15
+ // Escaping is applied to the WHOLE serialized document, then the reference
16
+ // interpolations this emitter owns are substituted in. Doing it the other way
17
+ // round would escape our own interpolations. The intermediate values are unique
18
+ // sentinels chosen so they cannot appear in the document (see sentinelPrefix),
19
+ // which keeps the substitution from colliding with authored text that happens to
20
+ // look like a sentinel.
21
+ import { materializeWithMap, serializeContent } from "@flow-as-code/core";
22
+ /**
23
+ * Doubles the leading character of every HCL template introducer so the text
24
+ * renders literally. `${` becomes `$${` and `%{` becomes `%%{`.
25
+ *
26
+ * The two patterns are disjoint and the expansion of one can never produce the
27
+ * other, so a single pass in either order is correct. It is also correct for
28
+ * already-doubled input: HCL scans left to right and takes the longest match,
29
+ * so escaping `$${` to `$$${` renders back to `$${` (a literal `$` followed by
30
+ * the escape for a literal `${`).
31
+ */
32
+ export function escapeTemplateText(text) {
33
+ // The replacements are functions on purpose. A string replacement gives `$`
34
+ // its substitution meaning, so the literal "$${" would be inserted as "${"
35
+ // and every escape would silently undo itself.
36
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll#specifying_a_string_as_the_replacement
37
+ return text.replaceAll("${", () => "$${").replaceAll("%{", () => "%%{");
38
+ }
39
+ /** Root of the sentinel names used while substituting reference tokens. */
40
+ const SENTINEL_BASE = "CDREF_TF_SENTINEL";
41
+ /**
42
+ * A sentinel prefix that does not occur in `haystack`. Authored text is allowed
43
+ * to contain anything, sentinel lookalikes included, so the prefix grows until
44
+ * it is absent from the document being rendered. It terminates: every extra
45
+ * character makes the prefix longer, and a string cannot contain a substring
46
+ * longer than itself.
47
+ */
48
+ export function sentinelPrefix(haystack) {
49
+ let prefix = SENTINEL_BASE;
50
+ while (haystack.includes(prefix))
51
+ prefix += "X";
52
+ return `${prefix}_`;
53
+ }
54
+ /**
55
+ * The `.tftpl` body for one document: deployable content JSON with every
56
+ * `${cdref:...}` token replaced by a `${var}` interpolation naming the entry in
57
+ * `local.flow_refs`, and every other template introducer escaped.
58
+ *
59
+ * `variableNames` maps token to template variable name and must cover every
60
+ * token in the document; @flow-as-code/core's strict materializer raises a
61
+ * MaterializeError listing all missing tokens at once if it does not.
62
+ */
63
+ export function renderTemplate(doc, variableNames) {
64
+ // Sentinels are chosen against the authored document, before materialization,
65
+ // so a value that looks like a sentinel cannot be smuggled in through a token.
66
+ const prefix = sentinelPrefix(JSON.stringify(doc.content));
67
+ const sentinels = new Map();
68
+ const substitutions = new Map();
69
+ let index = 0;
70
+ for (const [token, variable] of [...variableNames].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
71
+ const sentinel = `${prefix}${String(index)}_`;
72
+ index += 1;
73
+ sentinels.set(token, sentinel);
74
+ substitutions.set(sentinel, `\${${variable}}`);
75
+ }
76
+ const content = materializeWithMap(doc, Object.fromEntries(sentinels));
77
+ let body = escapeTemplateText(serializeContent(content));
78
+ for (const [sentinel, interpolation] of substitutions) {
79
+ body = body.replaceAll(sentinel, interpolation);
80
+ }
81
+ return body;
82
+ }
83
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qEAAqE;AACrE,EAAE;AACF,iFAAiF;AACjF,+EAA+E;AAC/E,6EAA6E;AAC7E,6EAA6E;AAC7E,gEAAgE;AAChE,0FAA0F;AAC1F,oFAAoF;AACpF,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,iFAAiF;AACjF,wBAAwB;AAExB,OAAO,EAAgB,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAExF;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,4EAA4E;IAC5E,2EAA2E;IAC3E,+CAA+C;IAC/C,4IAA4I;IAC5I,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;AAC1E,CAAC;AAED,2EAA2E;AAC3E,MAAM,aAAa,GAAG,mBAAmB,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,IAAI,MAAM,GAAG,aAAa,CAAC;IAC3B,OAAO,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,GAAG,CAAC;IAChD,OAAO,GAAG,MAAM,GAAG,CAAC;AACtB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,GAAY,EAAE,aAA0C;IACrF,8EAA8E;IAC9E,+EAA+E;IAC/E,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IAE3D,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IAChD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1F,MAAM,QAAQ,GAAG,GAAG,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;QAC9C,KAAK,IAAI,CAAC,CAAC;QACX,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAC/B,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,QAAQ,GAAG,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,OAAO,GAAG,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC;IACvE,IAAI,IAAI,GAAG,kBAAkB,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC;IACzD,KAAK,MAAM,CAAC,QAAQ,EAAE,aAAa,CAAC,IAAI,aAAa,EAAE,CAAC;QACtD,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { FlowDoc } from "@flow-as-code/core";
2
+ import { type EmitTfOptions, type EmitTfResult } from "./emit.js";
3
+ /**
4
+ * Emits `docs` and writes the result under `outDir`, creating directories as
5
+ * needed. Returns the emitted files so a caller can report or re-check them.
6
+ *
7
+ * Existing files are overwritten and nothing else in `outDir` is touched: the
8
+ * user owns their provider, backend, and resource files, and this emitter must
9
+ * never remove them.
10
+ */
11
+ export declare function writeTf(docs: readonly FlowDoc[], outDir: string, options?: EmitTfOptions): EmitTfResult;
12
+ //# sourceMappingURL=write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../src/write.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAU,MAAM,WAAW,CAAC;AAE1E;;;;;;;GAOG;AACH,wBAAgB,OAAO,CACrB,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,aAAkB,GAC1B,YAAY,CAQd"}
package/dist/write.js ADDED
@@ -0,0 +1,27 @@
1
+ /*
2
+ * Copyright 2026 The flow-as-code Authors
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ // The filesystem half of the emitter, kept apart from emit.ts so the emitter
6
+ // itself stays pure and the goldens compare bytes with no disk involved.
7
+ import { mkdirSync, writeFileSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+ import { emitTf } from "./emit.js";
10
+ /**
11
+ * Emits `docs` and writes the result under `outDir`, creating directories as
12
+ * needed. Returns the emitted files so a caller can report or re-check them.
13
+ *
14
+ * Existing files are overwritten and nothing else in `outDir` is touched: the
15
+ * user owns their provider, backend, and resource files, and this emitter must
16
+ * never remove them.
17
+ */
18
+ export function writeTf(docs, outDir, options = {}) {
19
+ const result = emitTf(docs, options);
20
+ for (const [relative, content] of Object.entries(result.files)) {
21
+ const target = join(outDir, relative);
22
+ mkdirSync(dirname(target), { recursive: true });
23
+ writeFileSync(target, content, "utf8");
24
+ }
25
+ return result;
26
+ }
27
+ //# sourceMappingURL=write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.js","sourceRoot":"","sources":["../src/write.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,6EAA6E;AAC7E,yEAAyE;AAEzE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAyC,MAAM,EAAE,MAAM,WAAW,CAAC;AAE1E;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CACrB,IAAwB,EACxB,MAAc,EACd,UAAyB,EAAE;IAE3B,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACrC,KAAK,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACtC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@flow-as-code/tf",
3
+ "version": "0.1.0",
4
+ "description": "Terraform/OpenTofu emitter: FlowDoc to aws_connect_* resources, .tftpl content, and an awscc version/alias scaffold. One-way in v1.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./emit": {
15
+ "types": "./dist/emit.d.ts",
16
+ "import": "./dist/emit.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src",
22
+ "README.md",
23
+ "LICENSE",
24
+ "!**/*.test.ts",
25
+ "!**/*.test.tsx",
26
+ "!**/__fixtures__",
27
+ "!**/__generated__",
28
+ "!**/*.tsbuildinfo",
29
+ "!**/__snapshots__",
30
+ "!**/*.snap",
31
+ "!**/test-helpers.ts",
32
+ "!**/test-helpers.tsx"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "engines": {
38
+ "node": ">=22.12"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "test": "vitest run"
43
+ },
44
+ "dependencies": {
45
+ "@flow-as-code/core": "0.1.0"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/flow-as-code/flow-as-code.git",
50
+ "directory": "packages/tf"
51
+ },
52
+ "homepage": "https://github.com/flow-as-code/flow-as-code/tree/main/packages/tf#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/flow-as-code/flow-as-code/issues"
55
+ }
56
+ }