react-hcl 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/dist/index.js ADDED
@@ -0,0 +1,320 @@
1
+ // src/hcl-serializer.ts
2
+ var RAW_HCL_SYMBOL = /* @__PURE__ */ Symbol.for("react-hcl:RawHCL");
3
+ function raw(value) {
4
+ return {
5
+ [RAW_HCL_SYMBOL]: true,
6
+ value,
7
+ toString() {
8
+ return value;
9
+ }
10
+ };
11
+ }
12
+ var BLOCK_HCL_SYMBOL = /* @__PURE__ */ Symbol.for("react-hcl:BlockHCL");
13
+ function block(value) {
14
+ return { [BLOCK_HCL_SYMBOL]: true, value };
15
+ }
16
+ var ATTRIBUTE_HCL_SYMBOL = /* @__PURE__ */ Symbol.for("react-hcl:AttributeHCL");
17
+ function attribute(value) {
18
+ return { [ATTRIBUTE_HCL_SYMBOL]: true, value };
19
+ }
20
+ function adjustIndent(text, targetIndent = 2) {
21
+ const rawLines = text.split("\n");
22
+ let start = 0;
23
+ while (start < rawLines.length && rawLines[start].trim() === "") start++;
24
+ let end = rawLines.length - 1;
25
+ while (end > start && rawLines[end].trim() === "") end--;
26
+ const lines = rawLines.slice(start, end + 1);
27
+ if (lines.length === 0) return "";
28
+ let minIndent = Infinity;
29
+ for (const line of lines) {
30
+ if (line.trim() === "") continue;
31
+ const leadingSpaces = line.search(/\S|$/);
32
+ if (leadingSpaces < minIndent) minIndent = leadingSpaces;
33
+ }
34
+ if (minIndent === Infinity) minIndent = 0;
35
+ const pad = " ".repeat(targetIndent);
36
+ return lines.map((line) => {
37
+ if (line.trim() === "") return "";
38
+ return pad + line.slice(minIndent);
39
+ }).join("\n");
40
+ }
41
+
42
+ // src/components/data-source.ts
43
+ function DataSource(props) {
44
+ const { type, name, ref, children, attributes: extraAttrs, ...rest } = props;
45
+ const attributes = { ...rest, ...extraAttrs };
46
+ if (ref) {
47
+ ref.__refMeta = { blockType: "data", type, name };
48
+ }
49
+ if (attributes.provider?.__refMeta) {
50
+ const meta = attributes.provider.__refMeta;
51
+ attributes.provider = raw(`${meta.type}.${meta.alias || meta.name}`);
52
+ }
53
+ if (attributes.depends_on && Array.isArray(attributes.depends_on)) {
54
+ attributes.depends_on = attributes.depends_on.map((dep) => {
55
+ if (dep.__refMeta) {
56
+ const meta = dep.__refMeta;
57
+ if (meta.blockType === "data") {
58
+ return raw(`data.${meta.type}.${meta.name}`);
59
+ }
60
+ return raw(`${meta.type}.${meta.name}`);
61
+ }
62
+ return dep;
63
+ });
64
+ }
65
+ const rawChildren = Array.isArray(children) ? children[0] : children;
66
+ const hasInnerText = typeof rawChildren === "string";
67
+ return {
68
+ blockType: "data",
69
+ type,
70
+ name,
71
+ attributes: hasInnerText ? {} : attributes,
72
+ ...hasInnerText ? { innerText: adjustIndent(rawChildren, 2) } : {}
73
+ };
74
+ }
75
+
76
+ // src/components/locals.ts
77
+ function Locals(props) {
78
+ const { children, ...attributes } = props;
79
+ return { blockType: "locals", attributes };
80
+ }
81
+
82
+ // src/components/module.ts
83
+ function Module(props) {
84
+ const { name, ref, children, attributes: extraAttrs, ...rest } = props;
85
+ const attributes = { ...rest, ...extraAttrs };
86
+ if (ref) {
87
+ ref.__refMeta = { blockType: "module", type: "module", name };
88
+ }
89
+ if (attributes.depends_on && Array.isArray(attributes.depends_on)) {
90
+ attributes.depends_on = attributes.depends_on.map((dep) => {
91
+ if (dep.__refMeta) {
92
+ const meta = dep.__refMeta;
93
+ if (meta.blockType === "module") {
94
+ return raw(`module.${meta.name}`);
95
+ }
96
+ if (meta.blockType === "data") {
97
+ return raw(`data.${meta.type}.${meta.name}`);
98
+ }
99
+ return raw(`${meta.type}.${meta.name}`);
100
+ }
101
+ return dep;
102
+ });
103
+ }
104
+ if (attributes.providers && typeof attributes.providers === "object" && !Array.isArray(attributes.providers)) {
105
+ const resolved = {};
106
+ for (const [key, val] of Object.entries(attributes.providers)) {
107
+ if (val?.__refMeta) {
108
+ const meta = val.__refMeta;
109
+ resolved[key] = raw(`${meta.type}.${meta.alias || meta.name}`);
110
+ } else {
111
+ resolved[key] = val;
112
+ }
113
+ }
114
+ attributes.providers = attribute(resolved);
115
+ }
116
+ const rawChildren = Array.isArray(children) ? children[0] : children;
117
+ const hasInnerText = typeof rawChildren === "string";
118
+ return {
119
+ blockType: "module",
120
+ name,
121
+ attributes: hasInnerText ? {} : attributes,
122
+ ...hasInnerText ? { innerText: adjustIndent(rawChildren, 2) } : {}
123
+ };
124
+ }
125
+
126
+ // src/components/output.ts
127
+ function Output(props) {
128
+ const { name, children, ...attributes } = props;
129
+ return { blockType: "output", name, attributes };
130
+ }
131
+
132
+ // src/components/provider.ts
133
+ function Provider(props) {
134
+ const { type, ref, alias, children, ...rest } = props;
135
+ if (ref) {
136
+ ref.__refMeta = {
137
+ blockType: "provider",
138
+ type,
139
+ name: alias || type,
140
+ ...alias ? { alias } : {}
141
+ };
142
+ }
143
+ const attributes = alias ? { alias, ...rest } : rest;
144
+ return { blockType: "provider", type, attributes };
145
+ }
146
+
147
+ // src/components/resource.ts
148
+ function Resource(props) {
149
+ const { type, name, ref, children, attributes: extraAttrs, ...rest } = props;
150
+ const attributes = { ...rest, ...extraAttrs };
151
+ if (ref) {
152
+ ref.__refMeta = { blockType: "resource", type, name };
153
+ }
154
+ if (attributes.provider?.__refMeta) {
155
+ const meta = attributes.provider.__refMeta;
156
+ attributes.provider = raw(`${meta.type}.${meta.alias || meta.name}`);
157
+ }
158
+ if (attributes.depends_on && Array.isArray(attributes.depends_on)) {
159
+ attributes.depends_on = attributes.depends_on.map((dep) => {
160
+ if (dep.__refMeta) {
161
+ const meta = dep.__refMeta;
162
+ if (meta.blockType === "data") {
163
+ return raw(`data.${meta.type}.${meta.name}`);
164
+ }
165
+ return raw(`${meta.type}.${meta.name}`);
166
+ }
167
+ return dep;
168
+ });
169
+ }
170
+ const rawChildren = Array.isArray(children) ? children[0] : children;
171
+ const hasInnerText = typeof rawChildren === "string";
172
+ return {
173
+ blockType: "resource",
174
+ type,
175
+ name,
176
+ attributes: hasInnerText ? {} : attributes,
177
+ ...hasInnerText ? { innerText: adjustIndent(rawChildren, 2) } : {}
178
+ };
179
+ }
180
+
181
+ // src/components/terraform.ts
182
+ function Terraform(props) {
183
+ const { children, ...attributes } = props;
184
+ const terraformAttributes = { ...attributes };
185
+ const requiredProviders = terraformAttributes.required_providers;
186
+ if (typeof requiredProviders === "object" && requiredProviders !== null && !Array.isArray(requiredProviders)) {
187
+ terraformAttributes.required_providers = block(requiredProviders);
188
+ }
189
+ const rawChildren = Array.isArray(children) ? children[0] : children;
190
+ const hasInnerText = typeof rawChildren === "string";
191
+ return {
192
+ blockType: "terraform",
193
+ attributes: hasInnerText ? {} : terraformAttributes,
194
+ ...hasInnerText ? { innerText: rawChildren } : {}
195
+ };
196
+ }
197
+
198
+ // src/components/variable.ts
199
+ function Variable(props) {
200
+ const { name, type, children, ...rest } = props;
201
+ const attributes = type ? { type: raw(type), ...rest } : rest;
202
+ return { blockType: "variable", name, attributes };
203
+ }
204
+
205
+ // src/helpers/tf.ts
206
+ var tf = {
207
+ /** tf.var("name") → var.name */
208
+ var(name) {
209
+ return raw(`var.${name}`);
210
+ },
211
+ /** tf.local("name") → local.name */
212
+ local(name) {
213
+ return raw(`local.${name}`);
214
+ }
215
+ };
216
+
217
+ // src/hooks/use-ref.ts
218
+ var RAW_HCL_SYMBOL2 = /* @__PURE__ */ Symbol.for("react-hcl:RawHCL");
219
+ var HOOK_KEY = /* @__PURE__ */ Symbol.for("react-hcl:hookState");
220
+ function getState() {
221
+ if (!globalThis[HOOK_KEY]) {
222
+ globalThis[HOOK_KEY] = { hookIndex: 0, hookStore: [] };
223
+ }
224
+ return globalThis[HOOK_KEY];
225
+ }
226
+ function buildPrefix(meta) {
227
+ if (meta.blockType === "module") {
228
+ return `module.${meta.name}`;
229
+ }
230
+ if (meta.blockType === "data") {
231
+ return `data.${meta.type}.${meta.name}`;
232
+ }
233
+ return `${meta.type}.${meta.name}`;
234
+ }
235
+ var UNRESOLVED_PLACEHOLDER = "__UNRESOLVED_REF__";
236
+ function createLazyRawHCL(resolvePath) {
237
+ const base = {
238
+ [RAW_HCL_SYMBOL2]: true,
239
+ get value() {
240
+ return resolvePath();
241
+ },
242
+ toString() {
243
+ return resolvePath();
244
+ }
245
+ };
246
+ return new Proxy(base, {
247
+ get(target, prop) {
248
+ if (typeof prop === "symbol") {
249
+ return target[prop];
250
+ }
251
+ if (prop === "value" || prop === "toString") {
252
+ return target[prop];
253
+ }
254
+ return createLazyRawHCL(() => `${resolvePath()}.${prop}`);
255
+ }
256
+ });
257
+ }
258
+ function useRef() {
259
+ const state = getState();
260
+ const idx = state.hookIndex++;
261
+ if (idx < state.hookStore.length) {
262
+ return state.hookStore[idx];
263
+ }
264
+ let meta;
265
+ const getMeta = () => {
266
+ if (!meta) {
267
+ return {
268
+ blockType: "resource",
269
+ type: UNRESOLVED_PLACEHOLDER,
270
+ name: UNRESOLVED_PLACEHOLDER
271
+ };
272
+ }
273
+ return meta;
274
+ };
275
+ const proxy = new Proxy({}, {
276
+ get(_target, prop) {
277
+ if (typeof prop === "symbol") return void 0;
278
+ if (prop === "__refMeta") {
279
+ return meta;
280
+ }
281
+ if (prop === "__dependsOnValue") {
282
+ return createLazyRawHCL(() => buildPrefix(getMeta()));
283
+ }
284
+ if (prop === "__providerValue") {
285
+ return createLazyRawHCL(() => {
286
+ const m = getMeta();
287
+ return `${m.type}.${m.alias || m.name}`;
288
+ });
289
+ }
290
+ return createLazyRawHCL(() => `${buildPrefix(getMeta())}.${prop}`);
291
+ },
292
+ set(_target, prop, value) {
293
+ if (prop === "__refMeta") {
294
+ meta = value;
295
+ return true;
296
+ }
297
+ return true;
298
+ }
299
+ });
300
+ state.hookStore.push(proxy);
301
+ return proxy;
302
+ }
303
+
304
+ // src/jsx-runtime.ts
305
+ var Fragment = "Fragment";
306
+ export {
307
+ DataSource,
308
+ Fragment,
309
+ Locals,
310
+ Module,
311
+ Output,
312
+ Provider,
313
+ Resource,
314
+ Terraform,
315
+ Variable,
316
+ block,
317
+ raw,
318
+ tf,
319
+ useRef
320
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Custom JSX runtime for react-hcl.
3
+ *
4
+ * This is NOT React — it is a minimal JSX runtime that produces plain JSXElement objects
5
+ * (a simple { type, props, children } structure) instead of React elements.
6
+ *
7
+ * How it integrates:
8
+ * - esbuild's `jsx: "automatic"` + `jsxImportSource: "react-hcl"` causes
9
+ * TSX files to import jsx/jsxs from "react-hcl/jsx-runtime" automatically.
10
+ * - When users write <Resource type="aws_vpc" name="main" />, esbuild transforms it to
11
+ * jsx(Resource, { type: "aws_vpc", name: "main" }) which returns a JSXElement.
12
+ * - The CLI (cli.ts) then renders the JSXElement tree by calling component functions
13
+ * and collecting their output.
14
+ *
15
+ * JSX automatic runtime contract:
16
+ * - `jsx(type, props)` is called for elements with 0 or 1 child
17
+ * - `jsxs(type, props)` is called for elements with 2+ children
18
+ * - `children` is passed inside `props` (not as a separate argument)
19
+ * - `Fragment` is used for <> ... </> syntax
20
+ */
21
+ type ComponentFunction = Function;
22
+ /** The runtime representation of a JSX element, produced by jsx() and jsxs(). */
23
+ export type JSXElement = {
24
+ type: string | ComponentFunction;
25
+ props: Record<string, any>;
26
+ children: any[];
27
+ };
28
+ /**
29
+ * Called by the JSX automatic transform for elements with 0 or 1 child.
30
+ * Separates `children` from props and wraps a single child in an array.
31
+ */
32
+ export declare function jsx(type: string | ComponentFunction, props: Record<string, any>): JSXElement;
33
+ /**
34
+ * Called by the JSX automatic transform for elements with 2+ children.
35
+ * Children are already an array from the transform; normalizes edge cases.
36
+ */
37
+ export declare function jsxs(type: string | ComponentFunction, props: Record<string, any>): JSXElement;
38
+ /** Fragment component — used for <> ... </> grouping without a wrapper element. */
39
+ export declare const Fragment = "Fragment";
40
+ export {};
@@ -0,0 +1,23 @@
1
+ // src/jsx-runtime.ts
2
+ function jsx(type, props) {
3
+ const { children, ...restProps } = props;
4
+ return {
5
+ type,
6
+ props: restProps,
7
+ children: children != null ? [children] : []
8
+ };
9
+ }
10
+ function jsxs(type, props) {
11
+ const { children, ...restProps } = props;
12
+ return {
13
+ type,
14
+ props: restProps,
15
+ children: Array.isArray(children) ? children : children != null ? [children] : []
16
+ };
17
+ }
18
+ var Fragment = "Fragment";
19
+ export {
20
+ Fragment,
21
+ jsx,
22
+ jsxs
23
+ };
@@ -0,0 +1,12 @@
1
+ import type { Block } from "./blocks";
2
+ import type { JSXElement } from "./jsx-runtime";
3
+ type Renderable = JSXElement | JSXElement[] | string | null;
4
+ type RootRenderable = Renderable | (() => Renderable);
5
+ /**
6
+ * 2-pass rendering:
7
+ * Pass 1: Clear hook store, render tree to collect ref metadata (result discarded).
8
+ * Pass 2: Reset hook index only (reuse proxies with metadata), render tree (result kept).
9
+ * Validation: Check that all refs in hookStore have __refMeta set.
10
+ */
11
+ export declare function render(element: RootRenderable): Block[];
12
+ export {};
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "react-hcl",
3
+ "version": "0.1.0",
4
+ "description": "TSX to Terraform (.tf) transpiler with a custom JSX runtime",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./jsx-runtime": {
14
+ "types": "./dist/jsx-runtime.d.ts",
15
+ "import": "./dist/jsx-runtime.js"
16
+ }
17
+ },
18
+ "bin": {
19
+ "react-hcl": "dist/cli.js"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "build": "bun run build:js && bun run build:types",
30
+ "build:js": "bun run build:js:cli && bun run build:js:index && bun run build:js:jsx-runtime",
31
+ "build:js:cli": "bun x esbuild src/cli.ts --outfile=dist/cli.js --format=esm --platform=node --external:esbuild --bundle --banner:js='#!/usr/bin/env node'",
32
+ "build:js:index": "bun x esbuild src/index.ts --outfile=dist/index.js --format=esm --platform=node --external:esbuild --bundle",
33
+ "build:js:jsx-runtime": "bun x esbuild src/jsx-runtime.ts --outfile=dist/jsx-runtime.js --format=esm --platform=node --bundle",
34
+ "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly",
35
+ "lint": "bunx biome check .",
36
+ "lint:fix": "bunx biome check --fix .",
37
+ "test": "bun test",
38
+ "tf:validate:samples": "bash scripts/terraform-validate-samples.sh",
39
+ "prepack": "bun run build"
40
+ },
41
+ "devDependencies": {
42
+ "@biomejs/biome": "^2.3.15",
43
+ "@types/bun": "^1.3.9",
44
+ "hcl2-parser": "^1.0.3"
45
+ },
46
+ "peerDependencies": {
47
+ "typescript": "^5"
48
+ },
49
+ "dependencies": {
50
+ "esbuild": "^0.27.3"
51
+ }
52
+ }