@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.
- package/LICENSE +202 -0
- package/README.md +67 -0
- package/dist/emit.d.ts +45 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +350 -0
- package/dist/emit.js.map +1 -0
- package/dist/hcl.d.ts +49 -0
- package/dist/hcl.d.ts.map +1 -0
- package/dist/hcl.js +95 -0
- package/dist/hcl.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/template.d.ts +31 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +83 -0
- package/dist/template.js.map +1 -0
- package/dist/write.d.ts +12 -0
- package/dist/write.d.ts.map +1 -0
- package/dist/write.js +27 -0
- package/dist/write.js.map +1 -0
- package/package.json +56 -0
- package/src/emit.ts +532 -0
- package/src/hcl.ts +107 -0
- package/src/index.ts +11 -0
- package/src/template.ts +88 -0
- package/src/write.ts +33 -0
package/src/template.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
|
|
22
|
+
import { type FlowDoc, materializeWithMap, serializeContent } from "@flow-as-code/core";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Doubles the leading character of every HCL template introducer so the text
|
|
26
|
+
* renders literally. `${` becomes `$${` and `%{` becomes `%%{`.
|
|
27
|
+
*
|
|
28
|
+
* The two patterns are disjoint and the expansion of one can never produce the
|
|
29
|
+
* other, so a single pass in either order is correct. It is also correct for
|
|
30
|
+
* already-doubled input: HCL scans left to right and takes the longest match,
|
|
31
|
+
* so escaping `$${` to `$$${` renders back to `$${` (a literal `$` followed by
|
|
32
|
+
* the escape for a literal `${`).
|
|
33
|
+
*/
|
|
34
|
+
export function escapeTemplateText(text: string): string {
|
|
35
|
+
// The replacements are functions on purpose. A string replacement gives `$`
|
|
36
|
+
// its substitution meaning, so the literal "$${" would be inserted as "${"
|
|
37
|
+
// and every escape would silently undo itself.
|
|
38
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll#specifying_a_string_as_the_replacement
|
|
39
|
+
return text.replaceAll("${", () => "$${").replaceAll("%{", () => "%%{");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Root of the sentinel names used while substituting reference tokens. */
|
|
43
|
+
const SENTINEL_BASE = "CDREF_TF_SENTINEL";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A sentinel prefix that does not occur in `haystack`. Authored text is allowed
|
|
47
|
+
* to contain anything, sentinel lookalikes included, so the prefix grows until
|
|
48
|
+
* it is absent from the document being rendered. It terminates: every extra
|
|
49
|
+
* character makes the prefix longer, and a string cannot contain a substring
|
|
50
|
+
* longer than itself.
|
|
51
|
+
*/
|
|
52
|
+
export function sentinelPrefix(haystack: string): string {
|
|
53
|
+
let prefix = SENTINEL_BASE;
|
|
54
|
+
while (haystack.includes(prefix)) prefix += "X";
|
|
55
|
+
return `${prefix}_`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The `.tftpl` body for one document: deployable content JSON with every
|
|
60
|
+
* `${cdref:...}` token replaced by a `${var}` interpolation naming the entry in
|
|
61
|
+
* `local.flow_refs`, and every other template introducer escaped.
|
|
62
|
+
*
|
|
63
|
+
* `variableNames` maps token to template variable name and must cover every
|
|
64
|
+
* token in the document; @flow-as-code/core's strict materializer raises a
|
|
65
|
+
* MaterializeError listing all missing tokens at once if it does not.
|
|
66
|
+
*/
|
|
67
|
+
export function renderTemplate(doc: FlowDoc, variableNames: ReadonlyMap<string, string>): string {
|
|
68
|
+
// Sentinels are chosen against the authored document, before materialization,
|
|
69
|
+
// so a value that looks like a sentinel cannot be smuggled in through a token.
|
|
70
|
+
const prefix = sentinelPrefix(JSON.stringify(doc.content));
|
|
71
|
+
|
|
72
|
+
const sentinels = new Map<string, string>();
|
|
73
|
+
const substitutions = new Map<string, string>();
|
|
74
|
+
let index = 0;
|
|
75
|
+
for (const [token, variable] of [...variableNames].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
|
|
76
|
+
const sentinel = `${prefix}${String(index)}_`;
|
|
77
|
+
index += 1;
|
|
78
|
+
sentinels.set(token, sentinel);
|
|
79
|
+
substitutions.set(sentinel, `\${${variable}}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const content = materializeWithMap(doc, Object.fromEntries(sentinels));
|
|
83
|
+
let body = escapeTemplateText(serializeContent(content));
|
|
84
|
+
for (const [sentinel, interpolation] of substitutions) {
|
|
85
|
+
body = body.replaceAll(sentinel, interpolation);
|
|
86
|
+
}
|
|
87
|
+
return body;
|
|
88
|
+
}
|
package/src/write.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
|
|
8
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import type { FlowDoc } from "@flow-as-code/core";
|
|
11
|
+
import { type EmitTfOptions, type EmitTfResult, emitTf } from "./emit.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Emits `docs` and writes the result under `outDir`, creating directories as
|
|
15
|
+
* needed. Returns the emitted files so a caller can report or re-check them.
|
|
16
|
+
*
|
|
17
|
+
* Existing files are overwritten and nothing else in `outDir` is touched: the
|
|
18
|
+
* user owns their provider, backend, and resource files, and this emitter must
|
|
19
|
+
* never remove them.
|
|
20
|
+
*/
|
|
21
|
+
export function writeTf(
|
|
22
|
+
docs: readonly FlowDoc[],
|
|
23
|
+
outDir: string,
|
|
24
|
+
options: EmitTfOptions = {},
|
|
25
|
+
): EmitTfResult {
|
|
26
|
+
const result = emitTf(docs, options);
|
|
27
|
+
for (const [relative, content] of Object.entries(result.files)) {
|
|
28
|
+
const target = join(outDir, relative);
|
|
29
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
30
|
+
writeFileSync(target, content, "utf8");
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|