local-web-services-javascript-sdk 0.1.1 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-web-services-javascript-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "JavaScript testing SDK for local-web-services — subprocess-based AWS service fixtures for testing",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Minimal Terraform HCL parser for discovering AWS resources in .tf files.
5
+ *
6
+ * Supports the subset of HCL needed for common AWS resource types, including
7
+ * heredoc strings (<<MARKER ... MARKER) used for multi-line JSON definitions.
8
+ */
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+
13
+ const RESOURCE_HEADER = /resource\s+"(?<type>[^"]+)"\s+"(?<logical>[^"]+)"\s*\{/;
14
+ const ATTR_STR = /^\s*(?<key>\w+)\s*=\s*"(?<value>[^"]*)"/;
15
+ const ATTR_BARE = /^\s*(?<key>\w+)\s*=\s*(?<value>\S+)/;
16
+ const ATTR_HEREDOC = /^\s*(?<key>\w+)\s*=\s*<<(?<marker>\w+)\s*$/;
17
+
18
+ /**
19
+ * Parse all .tf files in `projectDir` and return a resource spec object with
20
+ * discovered AWS resources ready for use with LwsSession.
21
+ *
22
+ * @param {string} projectDir
23
+ * @returns {{ stateMachines?: Array<{ name: string, definition?: string, roleArn?: string }> }}
24
+ */
25
+ function discoverHcl(projectDir) {
26
+ const tfFiles = [];
27
+
28
+ function findTfFiles(dir) {
29
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
30
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
31
+ findTfFiles(path.join(dir, entry.name));
32
+ } else if (entry.name.endsWith(".tf")) {
33
+ tfFiles.push(path.join(dir, entry.name));
34
+ }
35
+ }
36
+ }
37
+
38
+ findTfFiles(projectDir);
39
+
40
+ if (tfFiles.length === 0) {
41
+ throw new Error(
42
+ `No .tf files found in ${projectDir}. ` +
43
+ "Make sure you point to the directory containing your Terraform files."
44
+ );
45
+ }
46
+
47
+ const resources = [];
48
+ for (const tfFile of tfFiles) {
49
+ resources.push(...parseTfFile(tfFile));
50
+ }
51
+
52
+ return classifyResources(resources);
53
+ }
54
+
55
+ function parseTfFile(filePath) {
56
+ const text = fs.readFileSync(filePath, "utf-8");
57
+ const lines = text.split("\n");
58
+ const results = [];
59
+ let i = 0;
60
+
61
+ while (i < lines.length) {
62
+ const m = RESOURCE_HEADER.exec(lines[i]);
63
+ if (m?.groups) {
64
+ const { type, logical } = m.groups;
65
+ const [attrs, nextI] = collectBlock(lines, i + 1);
66
+ results.push({ type, logical, attrs });
67
+ i = nextI;
68
+ } else {
69
+ i++;
70
+ }
71
+ }
72
+
73
+ return results;
74
+ }
75
+
76
+ function collectBlock(lines, start) {
77
+ const attrs = {};
78
+ let depth = 1;
79
+ let i = start;
80
+
81
+ while (i < lines.length && depth > 0) {
82
+ const line = lines[i];
83
+
84
+ // Handle heredoc (key = <<MARKER ... MARKER) before adjusting depth so
85
+ // JSON content inside the heredoc does not affect brace counting.
86
+ if (depth === 1) {
87
+ const mHeredoc = ATTR_HEREDOC.exec(line);
88
+ if (mHeredoc?.groups) {
89
+ const { key, marker } = mHeredoc.groups;
90
+ i++;
91
+ const heredocLines = [];
92
+ while (i < lines.length && lines[i].trimEnd() !== marker) {
93
+ heredocLines.push(lines[i]);
94
+ i++;
95
+ }
96
+ attrs[key] = heredocLines.join("\n");
97
+ i++; // skip the closing marker line
98
+ continue;
99
+ }
100
+ }
101
+
102
+ const opens = (line.match(/\{/g) ?? []).length;
103
+ const closes = (line.match(/\}/g) ?? []).length;
104
+ depth += opens - closes;
105
+
106
+ if (depth > 1) {
107
+ i++;
108
+ continue;
109
+ }
110
+ if (depth === 0) break;
111
+
112
+ const mStr = ATTR_STR.exec(line);
113
+ if (mStr?.groups) {
114
+ attrs[mStr.groups.key] = mStr.groups.value;
115
+ } else {
116
+ const mBare = ATTR_BARE.exec(line);
117
+ if (mBare?.groups) {
118
+ attrs[mBare.groups.key] = mBare.groups.value;
119
+ }
120
+ }
121
+ i++;
122
+ }
123
+
124
+ return [attrs, i + 1];
125
+ }
126
+
127
+ function classifyResources(resources) {
128
+ const spec = { stateMachines: [] };
129
+
130
+ for (const { type, attrs } of resources) {
131
+ if (type === "aws_sfn_state_machine") {
132
+ const sm = buildStateMachine(attrs);
133
+ if (sm) spec.stateMachines.push(sm);
134
+ }
135
+ }
136
+
137
+ return spec;
138
+ }
139
+
140
+ function buildStateMachine(attrs) {
141
+ if (!attrs["name"]) return null;
142
+ return {
143
+ name: attrs["name"],
144
+ definition: attrs["definition"] ?? "{}",
145
+ roleArn: attrs["role_arn"],
146
+ };
147
+ }
148
+
149
+ module.exports = { discoverHcl };
package/src/session.js CHANGED
@@ -181,13 +181,49 @@ class LwsSession {
181
181
  /**
182
182
  * Create a session by discovering resources from a Terraform / HCL project.
183
183
  *
184
+ * Reads `.tf` files in `projectDir`, discovers AWS resources, starts ldk,
185
+ * and pre-creates the discovered resources so they are immediately available.
186
+ *
184
187
  * @param {string} [projectDir="."]
185
188
  * @returns {Promise<LwsSession>}
186
189
  */
187
190
  static async fromHcl(projectDir = ".") {
191
+ const { discoverHcl } = require("./discovery/hcl");
192
+
193
+ const resolvedDir = path.resolve(projectDir);
194
+ const spec = discoverHcl(resolvedDir);
195
+
188
196
  const basePort = await freePort();
189
- const session = new LwsSession(basePort, projectDir, {});
190
- await session._start("terraform");
197
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "lws-testing-"));
198
+ await generateTerraformConfig(tempDir, spec);
199
+
200
+ const session = new LwsSession(basePort, tempDir, spec);
201
+ session._tempDir = tempDir;
202
+ await session._start();
203
+
204
+ // Pre-create state machines discovered from the HCL files.
205
+ const sfnPort = basePort + SERVICE_OFFSETS["stepfunctions"];
206
+ for (const sm of spec.stateMachines ?? []) {
207
+ const definition =
208
+ typeof sm.definition === "object"
209
+ ? JSON.stringify(sm.definition)
210
+ : (sm.definition ?? "{}");
211
+ await fetch(`http://127.0.0.1:${sfnPort}`, {
212
+ method: "POST",
213
+ headers: {
214
+ "Content-Type": "application/x-amz-json-1.0",
215
+ "X-Amz-Target": "AWSStepFunctions.CreateStateMachine",
216
+ },
217
+ body: JSON.stringify({
218
+ name: sm.name,
219
+ definition,
220
+ roleArn:
221
+ sm.roleArn ?? "arn:aws:iam::000000000000:role/StepFunctionsRole",
222
+ type: "STANDARD",
223
+ }),
224
+ });
225
+ }
226
+
191
227
  return session;
192
228
  }
193
229