puls-dev 0.2.7 → 0.2.9

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.
Files changed (117) hide show
  1. package/dist/core/checker.js +71 -0
  2. package/dist/core/config.d.ts +6 -0
  3. package/dist/core/config.js +11 -1
  4. package/dist/core/context.d.ts +14 -0
  5. package/dist/core/context.js +2 -0
  6. package/dist/core/decorators.d.ts +4 -0
  7. package/dist/core/decorators.js +56 -30
  8. package/dist/core/hooks.d.ts +21 -0
  9. package/dist/core/hooks.js +116 -0
  10. package/dist/core/hooks.test.d.ts +1 -0
  11. package/dist/core/hooks.test.js +194 -0
  12. package/dist/core/multiregion.test.d.ts +1 -0
  13. package/dist/core/multiregion.test.js +87 -0
  14. package/dist/core/output.d.ts +2 -0
  15. package/dist/core/output.js +9 -2
  16. package/dist/core/parallel.test.d.ts +1 -0
  17. package/dist/core/parallel.test.js +215 -0
  18. package/dist/core/parser.d.ts +10 -0
  19. package/dist/core/parser.js +140 -0
  20. package/dist/core/parser.test.d.ts +1 -0
  21. package/dist/core/parser.test.js +117 -0
  22. package/dist/core/production.test.d.ts +1 -0
  23. package/dist/core/production.test.js +189 -0
  24. package/dist/core/provisioner.d.ts +4 -0
  25. package/dist/core/provisioner.js +123 -0
  26. package/dist/core/resource.d.ts +23 -0
  27. package/dist/core/resource.js +54 -0
  28. package/dist/core/retry.d.ts +9 -0
  29. package/dist/core/retry.js +28 -0
  30. package/dist/core/retry.test.d.ts +1 -0
  31. package/dist/core/retry.test.js +66 -0
  32. package/dist/core/secret.d.ts +41 -0
  33. package/dist/core/secret.js +105 -0
  34. package/dist/core/secret.test.d.ts +1 -0
  35. package/dist/core/secret.test.js +166 -0
  36. package/dist/core/stack.d.ts +4 -3
  37. package/dist/core/stack.js +322 -48
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.js +3 -0
  40. package/dist/providers/aws/api.js +97 -17
  41. package/dist/providers/aws/ec2.d.ts +51 -0
  42. package/dist/providers/aws/ec2.js +331 -0
  43. package/dist/providers/aws/ec2.test.d.ts +1 -0
  44. package/dist/providers/aws/ec2.test.js +281 -0
  45. package/dist/providers/aws/index.d.ts +4 -0
  46. package/dist/providers/aws/index.js +4 -0
  47. package/dist/providers/aws/route53.d.ts +1 -0
  48. package/dist/providers/aws/route53.js +15 -2
  49. package/dist/providers/aws/route53.test.js +47 -0
  50. package/dist/providers/aws/template.d.ts +34 -0
  51. package/dist/providers/aws/template.js +252 -0
  52. package/dist/providers/aws/template.test.d.ts +1 -0
  53. package/dist/providers/aws/template.test.js +208 -0
  54. package/dist/providers/do/api.d.ts +3 -1
  55. package/dist/providers/do/api.js +126 -27
  56. package/dist/providers/do/app.d.ts +26 -0
  57. package/dist/providers/do/app.js +124 -0
  58. package/dist/providers/do/app.test.d.ts +1 -0
  59. package/dist/providers/do/app.test.js +268 -0
  60. package/dist/providers/do/database.d.ts +44 -0
  61. package/dist/providers/do/database.js +208 -0
  62. package/dist/providers/do/database.test.d.ts +1 -0
  63. package/dist/providers/do/database.test.js +293 -0
  64. package/dist/providers/do/domain.d.ts +2 -0
  65. package/dist/providers/do/domain.js +30 -0
  66. package/dist/providers/do/domain.test.js +49 -0
  67. package/dist/providers/do/droplet.d.ts +9 -0
  68. package/dist/providers/do/droplet.js +146 -8
  69. package/dist/providers/do/droplet.test.js +228 -1
  70. package/dist/providers/do/firewall.d.ts +2 -1
  71. package/dist/providers/do/firewall.js +23 -9
  72. package/dist/providers/do/firewall.test.js +54 -0
  73. package/dist/providers/do/index.d.ts +11 -0
  74. package/dist/providers/do/index.js +8 -0
  75. package/dist/providers/do/spaces.d.ts +27 -0
  76. package/dist/providers/do/spaces.js +142 -0
  77. package/dist/providers/do/spaces.test.d.ts +1 -0
  78. package/dist/providers/do/spaces.test.js +180 -0
  79. package/dist/providers/do/spaces_api.d.ts +2 -0
  80. package/dist/providers/do/spaces_api.js +20 -0
  81. package/dist/providers/do/vpc.d.ts +30 -0
  82. package/dist/providers/do/vpc.js +128 -0
  83. package/dist/providers/do/vpc.test.d.ts +1 -0
  84. package/dist/providers/do/vpc.test.js +258 -0
  85. package/dist/providers/firebase/api.js +92 -29
  86. package/dist/providers/firebase/list.d.ts +2 -0
  87. package/dist/providers/firebase/list.js +25 -0
  88. package/dist/providers/gcp/api.js +88 -14
  89. package/dist/providers/gcp/clouddns.d.ts +1 -0
  90. package/dist/providers/gcp/clouddns.js +15 -2
  91. package/dist/providers/gcp/clouddns.test.js +45 -0
  92. package/dist/providers/gcp/index.d.ts +5 -1
  93. package/dist/providers/gcp/index.js +5 -1
  94. package/dist/providers/gcp/list.d.ts +2 -0
  95. package/dist/providers/gcp/list.js +55 -0
  96. package/dist/providers/gcp/secrets.js +1 -1
  97. package/dist/providers/gcp/template.d.ts +32 -0
  98. package/dist/providers/gcp/template.js +252 -0
  99. package/dist/providers/gcp/template.test.d.ts +1 -0
  100. package/dist/providers/gcp/template.test.js +227 -0
  101. package/dist/providers/gcp/vm.d.ts +48 -0
  102. package/dist/providers/gcp/vm.js +375 -0
  103. package/dist/providers/gcp/vm.test.d.ts +1 -0
  104. package/dist/providers/gcp/vm.test.js +321 -0
  105. package/dist/providers/proxmox/api.d.ts +1 -0
  106. package/dist/providers/proxmox/api.js +72 -16
  107. package/dist/providers/proxmox/index.d.ts +2 -0
  108. package/dist/providers/proxmox/index.js +2 -0
  109. package/dist/providers/proxmox/template.d.ts +44 -0
  110. package/dist/providers/proxmox/template.js +349 -0
  111. package/dist/providers/proxmox/template.test.d.ts +1 -0
  112. package/dist/providers/proxmox/template.test.js +179 -0
  113. package/dist/providers/proxmox/vm.d.ts +7 -4
  114. package/dist/providers/proxmox/vm.js +57 -102
  115. package/dist/providers/proxmox/vm.test.js +77 -0
  116. package/dist/types/inventory.d.ts +44 -1
  117. package/package.json +3 -1
@@ -0,0 +1,87 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { test, describe, beforeEach } from "node:test";
8
+ import assert from "node:assert";
9
+ import { Stack } from "./stack.js";
10
+ import { Deploy, Destroy } from "./decorators.js";
11
+ import { BaseBuilder } from "./resource.js";
12
+ import { Config } from "./config.js";
13
+ import { Output } from "./output.js";
14
+ class RegionResource extends BaseBuilder {
15
+ out = {
16
+ region: new Output(),
17
+ };
18
+ async deploy() {
19
+ const activeRegion = Config.get().providers.aws?.region ?? "unknown";
20
+ this.out.region.resolve(activeRegion);
21
+ return { name: this.name, region: activeRegion };
22
+ }
23
+ async destroy() {
24
+ const activeRegion = Config.get().providers.aws?.region ?? "unknown";
25
+ return { name: this.name, destroyed: true, region: activeRegion };
26
+ }
27
+ }
28
+ describe("Multi-Region Deployments Unit Tests", () => {
29
+ let executionLogs = [];
30
+ beforeEach(() => {
31
+ Config.set({
32
+ dryRun: false,
33
+ providers: {},
34
+ });
35
+ executionLogs = [];
36
+ });
37
+ test("runs sequential deployments across multiple regions and stores instances in registry", async () => {
38
+ let MultiStack = class MultiStack extends Stack {
39
+ res = new RegionResource("my-region-res").afterDeploy((result) => {
40
+ executionLogs.push(`deploy:${result.region}`);
41
+ });
42
+ };
43
+ MultiStack = __decorate([
44
+ Deploy({
45
+ regions: ["us-east-1", "eu-central-1", "ap-northeast-1"],
46
+ dryRun: false,
47
+ })
48
+ ], MultiStack);
49
+ // Wait for the asynchronous macro/microtask queue to resolve Deploy runs
50
+ await new Promise((resolve) => setTimeout(resolve, 100));
51
+ // Verify sequential region changes occurred in correct order
52
+ assert.deepStrictEqual(executionLogs, [
53
+ "deploy:us-east-1",
54
+ "deploy:eu-central-1",
55
+ "deploy:ap-northeast-1",
56
+ ]);
57
+ // Retrieve specific region stack outputs via Stack.from(cls, region)
58
+ const usStack = Stack.from(MultiStack, "us-east-1");
59
+ const euStack = Stack.from(MultiStack, "eu-central-1");
60
+ const apStack = Stack.from(MultiStack, "ap-northeast-1");
61
+ assert.ok(usStack);
62
+ assert.ok(euStack);
63
+ assert.ok(apStack);
64
+ assert.strictEqual(await usStack.res.out.region.get(), "us-east-1");
65
+ assert.strictEqual(await euStack.res.out.region.get(), "eu-central-1");
66
+ assert.strictEqual(await apStack.res.out.region.get(), "ap-northeast-1");
67
+ });
68
+ test("runs sequential teardowns across multiple regions on @Destroy", async () => {
69
+ let CleanStack = class CleanStack extends Stack {
70
+ res = new RegionResource("my-teardown-res").afterDestroy((result) => {
71
+ executionLogs.push(`destroy:${result.region}`);
72
+ });
73
+ };
74
+ CleanStack = __decorate([
75
+ Destroy({
76
+ regions: ["us-east-1", "eu-central-1"],
77
+ dryRun: false,
78
+ })
79
+ ], CleanStack);
80
+ // Wait for microtask queue to run Destroy
81
+ await new Promise((resolve) => setTimeout(resolve, 100));
82
+ assert.deepStrictEqual(executionLogs, [
83
+ "destroy:us-east-1",
84
+ "destroy:eu-central-1",
85
+ ]);
86
+ });
87
+ });
@@ -1,8 +1,10 @@
1
1
  export declare class Output<T> {
2
2
  private _promise;
3
3
  private _resolve;
4
+ private _reject;
4
5
  constructor();
5
6
  resolve(value: T): void;
7
+ reject(reason: any): void;
6
8
  get(): Promise<T>;
7
9
  apply<U>(fn: (val: T) => U): Output<U>;
8
10
  }
@@ -1,19 +1,26 @@
1
1
  export class Output {
2
2
  _promise;
3
3
  _resolve;
4
+ _reject;
4
5
  constructor() {
5
- this._promise = new Promise(resolve => (this._resolve = resolve));
6
+ this._promise = new Promise((resolve, reject) => {
7
+ this._resolve = resolve;
8
+ this._reject = reject;
9
+ });
6
10
  }
7
11
  resolve(value) {
8
12
  this._resolve(value);
9
13
  }
14
+ reject(reason) {
15
+ this._reject(reason);
16
+ }
10
17
  get() {
11
18
  return this._promise;
12
19
  }
13
20
  // Transform this output into a new Output<U> without awaiting it yourself.
14
21
  apply(fn) {
15
22
  const out = new Output();
16
- this._promise.then(v => out.resolve(fn(v)));
23
+ this._promise.then(v => out.resolve(fn(v)), err => out.reject(err));
17
24
  return out;
18
25
  }
19
26
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,215 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { test, describe, beforeEach } from "node:test";
8
+ import assert from "node:assert";
9
+ import { Stack } from "./stack.js";
10
+ import { Deploy } from "./decorators.js";
11
+ import { BaseBuilder } from "./resource.js";
12
+ import { Config } from "./config.js";
13
+ import { Output } from "./output.js";
14
+ // Helper delay
15
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
+ class DelayResource extends BaseBuilder {
17
+ delayMs;
18
+ executionLogs;
19
+ out = {
20
+ val: new Output(),
21
+ };
22
+ constructor(name, delayMs, executionLogs) {
23
+ super(name);
24
+ this.delayMs = delayMs;
25
+ this.executionLogs = executionLogs;
26
+ }
27
+ async deploy() {
28
+ this.executionLogs.push(`start:${this.name}`);
29
+ await delay(this.delayMs);
30
+ this.executionLogs.push(`end:${this.name}`);
31
+ this.out.val.resolve(this.name);
32
+ return { name: this.name };
33
+ }
34
+ async destroy() {
35
+ this.executionLogs.push(`destroy:${this.name}`);
36
+ await delay(this.delayMs);
37
+ return { name: this.name, destroyed: true };
38
+ }
39
+ }
40
+ class FailResource extends BaseBuilder {
41
+ async deploy() {
42
+ throw new Error("Failed to deploy!");
43
+ }
44
+ }
45
+ describe("Parallel Resource Deployment Unit Tests", () => {
46
+ let logs = [];
47
+ beforeEach(() => {
48
+ Config.set({
49
+ dryRun: false,
50
+ parallel: false,
51
+ providers: {},
52
+ });
53
+ logs = [];
54
+ });
55
+ test("runs sequential deployments by default (verifies base case)", async () => {
56
+ class SeqStack extends Stack {
57
+ r1 = new DelayResource("r1", 30, logs);
58
+ r2 = new DelayResource("r2", 30, logs);
59
+ }
60
+ const start = Date.now();
61
+ const stack = new SeqStack();
62
+ await stack.deploy();
63
+ const duration = Date.now() - start;
64
+ // In sequential, execution is r1 start -> r1 end -> r2 start -> r2 end
65
+ assert.deepStrictEqual(logs, [
66
+ "start:r1",
67
+ "end:r1",
68
+ "start:r2",
69
+ "end:r2"
70
+ ]);
71
+ assert.ok(duration >= 60, `Sequential should take at least 60ms, took ${duration}ms`);
72
+ });
73
+ test("runs parallel deployments concurrently when opted in", async () => {
74
+ // Enable parallel globally for the stack run
75
+ Config.set({
76
+ dryRun: false,
77
+ parallel: true,
78
+ providers: {},
79
+ });
80
+ class ParStack extends Stack {
81
+ r1 = new DelayResource("r1", 40, logs);
82
+ r2 = new DelayResource("r2", 40, logs);
83
+ }
84
+ const start = Date.now();
85
+ const stack = new ParStack();
86
+ await stack.deploy();
87
+ const duration = Date.now() - start;
88
+ // In parallel, both start before either finishes:
89
+ // start:r1 and start:r2 will both be printed first.
90
+ assert.strictEqual(logs[0].startsWith("start:"), true);
91
+ assert.strictEqual(logs[1].startsWith("start:"), true);
92
+ assert.ok(logs.includes("end:r1"));
93
+ assert.ok(logs.includes("end:r2"));
94
+ // Total duration should be closer to 40ms than 80ms
95
+ assert.ok(duration < 75, `Parallel should take less than 75ms (sum), took ${duration}ms`);
96
+ });
97
+ test("respects explicit dependsOn() ordering in parallel mode", async () => {
98
+ Config.set({
99
+ dryRun: false,
100
+ parallel: true,
101
+ providers: {},
102
+ });
103
+ class DependencyStack extends Stack {
104
+ // r1 depends on r2
105
+ r1 = new DelayResource("r1", 20, logs);
106
+ r2 = new DelayResource("r2", 20, logs);
107
+ constructor() {
108
+ super();
109
+ this.r1.dependsOn(this.r2);
110
+ }
111
+ }
112
+ const stack = new DependencyStack();
113
+ await stack.deploy();
114
+ // Since r1 depends on r2, r2 must fully complete before r1 starts
115
+ const r2StartIndex = logs.indexOf("start:r2");
116
+ const r2EndIndex = logs.indexOf("end:r2");
117
+ const r1StartIndex = logs.indexOf("start:r1");
118
+ assert.ok(r2StartIndex < r2EndIndex);
119
+ assert.ok(r2EndIndex < r1StartIndex);
120
+ });
121
+ test("respects implicit Output waiting in parallel mode", async () => {
122
+ Config.set({
123
+ dryRun: false,
124
+ parallel: true,
125
+ providers: {},
126
+ });
127
+ class OutputAwaitingResource extends BaseBuilder {
128
+ dependentVal;
129
+ executionLogs;
130
+ constructor(name, dependentVal, executionLogs) {
131
+ super(name);
132
+ this.dependentVal = dependentVal;
133
+ this.executionLogs = executionLogs;
134
+ }
135
+ async deploy() {
136
+ this.executionLogs.push(`start:${this.name}`);
137
+ // Blocks on the Output of the other resource!
138
+ const val = await this.dependentVal.get();
139
+ this.executionLogs.push(`end:${this.name}:${val}`);
140
+ return { name: this.name };
141
+ }
142
+ }
143
+ class OutputStack extends Stack {
144
+ r1 = new DelayResource("r1", 30, logs);
145
+ // r2 depends implicitly on r1's output
146
+ r2 = new OutputAwaitingResource("r2", this.r1.out.val, logs);
147
+ }
148
+ const stack = new OutputStack();
149
+ await stack.deploy();
150
+ // r2 starts in parallel, but it cannot end until r1 finishes and resolves the output
151
+ const r2StartIndex = logs.indexOf("start:r2");
152
+ const r1EndIndex = logs.indexOf("end:r1");
153
+ const r2EndIndex = logs.findIndex(line => line.startsWith("end:r2"));
154
+ assert.ok(r2StartIndex >= 0);
155
+ assert.ok(r1EndIndex >= 0);
156
+ assert.ok(r1EndIndex < r2EndIndex, `r1 end (${r1EndIndex}) must be before r2 end (${r2EndIndex})`);
157
+ assert.ok(logs.includes("end:r2:r1"));
158
+ });
159
+ test("runs parallel teardowns in reverse topological order on @Destroy", async () => {
160
+ Config.set({
161
+ dryRun: false,
162
+ parallel: true,
163
+ providers: {},
164
+ });
165
+ class TeardownStack extends Stack {
166
+ r1 = new DelayResource("r1", 20, logs);
167
+ r2 = new DelayResource("r2", 20, logs);
168
+ constructor() {
169
+ super();
170
+ // r2 depends on r1 during deploy (so r1 deploys first).
171
+ // Teardown should destroy r2 first, then r1!
172
+ this.r2.dependsOn(this.r1);
173
+ }
174
+ }
175
+ const stack = new TeardownStack();
176
+ await stack.destroy();
177
+ const r2DestroyIndex = logs.indexOf("destroy:r2");
178
+ const r1DestroyIndex = logs.indexOf("destroy:r1");
179
+ // r2 depends on r1, so r2 must be fully destroyed BEFORE r1 begins destruction
180
+ assert.ok(r2DestroyIndex < r1DestroyIndex, `r2 destroy (${r2DestroyIndex}) must be before r1 destroy (${r1DestroyIndex})`);
181
+ });
182
+ test("halts execution of dependent resources if a dependency fails", async () => {
183
+ Config.set({
184
+ dryRun: false,
185
+ parallel: true,
186
+ providers: {},
187
+ });
188
+ class FailStack extends Stack {
189
+ r1 = new FailResource("r1");
190
+ r2 = new DelayResource("r2", 30, logs);
191
+ constructor() {
192
+ super();
193
+ this.r2.dependsOn(this.r1);
194
+ }
195
+ }
196
+ const stack = new FailStack();
197
+ await assert.rejects(async () => {
198
+ await stack.deploy();
199
+ }, /Failed to deploy!/);
200
+ // r2 should never have started because its dependency r1 failed!
201
+ assert.strictEqual(logs.includes("start:r2"), false);
202
+ });
203
+ test("decorator option propagation sets configuration values", async () => {
204
+ // Clear parallel flag
205
+ Config.set({ parallel: false });
206
+ // We define a decorated simple stack
207
+ let SimpleDecoStack = class SimpleDecoStack extends Stack {
208
+ };
209
+ SimpleDecoStack = __decorate([
210
+ Deploy({ parallel: true })
211
+ ], SimpleDecoStack);
212
+ // Verify decorator correctly updated global configuration to true
213
+ assert.strictEqual(Config.isParallelActive(), true);
214
+ });
215
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Robust, zero-dependency, indentation-aware YAML parser.
3
+ * Parses sequences of key-value maps and nested string arrays.
4
+ */
5
+ export declare function parseYaml(content: string): any[];
6
+ /**
7
+ * Resolves a file path relative to the current working directory,
8
+ * reads its content, and parses it according to its extension (.json vs .yaml/.yml).
9
+ */
10
+ export declare function loadRecordsFromFile(filePath: string): any[];
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Strips comments from a YAML/JSON line while preserving comment hashes inside quotes.
5
+ */
6
+ function stripComments(line) {
7
+ const hashIdx = line.indexOf("#");
8
+ if (hashIdx < 0)
9
+ return line;
10
+ let inQuotes = false;
11
+ let quoteChar = "";
12
+ for (let i = 0; i < hashIdx; i++) {
13
+ const char = line[i];
14
+ if (char === '"' || char === "'") {
15
+ if (!inQuotes) {
16
+ inQuotes = true;
17
+ quoteChar = char;
18
+ }
19
+ else if (char === quoteChar) {
20
+ inQuotes = false;
21
+ }
22
+ }
23
+ }
24
+ if (inQuotes) {
25
+ return line;
26
+ }
27
+ return line.slice(0, hashIdx);
28
+ }
29
+ /**
30
+ * Robust, zero-dependency, indentation-aware YAML parser.
31
+ * Parses sequences of key-value maps and nested string arrays.
32
+ */
33
+ export function parseYaml(content) {
34
+ const list = [];
35
+ const lines = content.split(/\r?\n/);
36
+ let currentItem = null;
37
+ let activeKey = null;
38
+ let rootIndent = 0;
39
+ for (let line of lines) {
40
+ line = stripComments(line);
41
+ if (!line.trim())
42
+ continue;
43
+ const leadingSpaces = line.length - line.trimStart().length;
44
+ const trimmed = line.trim();
45
+ // Check if it's a bullet item list entry
46
+ if (trimmed.startsWith("-")) {
47
+ const rest = trimmed.slice(1).trim();
48
+ // If it is indented more than the root item and does not match a key-value pattern,
49
+ // treat it as an array item under the active key.
50
+ const isRestKeyValue = /^[a-zA-Z0-9_-]+\s*:/.test(rest);
51
+ if (leadingSpaces > rootIndent && !isRestKeyValue) {
52
+ if (currentItem && activeKey) {
53
+ if (!Array.isArray(currentItem[activeKey])) {
54
+ currentItem[activeKey] = [];
55
+ }
56
+ let val = rest;
57
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
58
+ val = val.slice(1, -1);
59
+ }
60
+ currentItem[activeKey].push(val);
61
+ }
62
+ continue;
63
+ }
64
+ // Otherwise, it represents a new item at the root list level
65
+ if (currentItem) {
66
+ list.push(currentItem);
67
+ }
68
+ currentItem = {};
69
+ rootIndent = leadingSpaces;
70
+ activeKey = null;
71
+ if (rest) {
72
+ const colonIdx = rest.indexOf(":");
73
+ if (colonIdx > 0 && isRestKeyValue) {
74
+ const key = rest.slice(0, colonIdx).trim();
75
+ let value = rest.slice(colonIdx + 1).trim();
76
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
77
+ value = value.slice(1, -1);
78
+ }
79
+ // Convert to number if numeric
80
+ if (/^\d+$/.test(value)) {
81
+ currentItem[key] = parseInt(value, 10);
82
+ }
83
+ else {
84
+ currentItem[key] = value;
85
+ }
86
+ activeKey = key;
87
+ }
88
+ }
89
+ }
90
+ else {
91
+ // Key-value pair or list start
92
+ const isKeyValue = /^[a-zA-Z0-9_-]+\s*:/.test(trimmed);
93
+ if (isKeyValue && currentItem) {
94
+ const colonIdx = trimmed.indexOf(":");
95
+ const key = trimmed.slice(0, colonIdx).trim();
96
+ let value = trimmed.slice(colonIdx + 1).trim();
97
+ if (value === "") {
98
+ // Starts a nested list/array
99
+ currentItem[key] = [];
100
+ activeKey = key;
101
+ }
102
+ else {
103
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
104
+ value = value.slice(1, -1);
105
+ }
106
+ if (/^\d+$/.test(value)) {
107
+ currentItem[key] = parseInt(value, 10);
108
+ }
109
+ else {
110
+ currentItem[key] = value;
111
+ }
112
+ activeKey = key;
113
+ }
114
+ }
115
+ }
116
+ }
117
+ if (currentItem) {
118
+ list.push(currentItem);
119
+ }
120
+ return list;
121
+ }
122
+ /**
123
+ * Resolves a file path relative to the current working directory,
124
+ * reads its content, and parses it according to its extension (.json vs .yaml/.yml).
125
+ */
126
+ export function loadRecordsFromFile(filePath) {
127
+ const absolutePath = path.resolve(process.cwd(), filePath);
128
+ const fileContent = fs.readFileSync(absolutePath, "utf-8");
129
+ const ext = path.extname(filePath).toLowerCase();
130
+ if (ext === ".json") {
131
+ const parsed = JSON.parse(fileContent);
132
+ return Array.isArray(parsed) ? parsed : [parsed];
133
+ }
134
+ else if (ext === ".yaml" || ext === ".yml") {
135
+ return parseYaml(fileContent);
136
+ }
137
+ else {
138
+ throw new Error(`Unsupported configuration file format: ${filePath}. Only JSON and YAML/YML files are supported.`);
139
+ }
140
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
1
+ import { test, describe } from "node:test";
2
+ import assert from "node:assert";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { parseYaml, loadRecordsFromFile } from "./parser.js";
6
+ describe("YAML & JSON Config Parser", () => {
7
+ test("parses standard simple key-value YAML blocks correctly", () => {
8
+ const yaml = `
9
+ - name: www
10
+ type: CNAME
11
+ value: lb.google.com
12
+ - name: mail
13
+ type: A
14
+ value: 1.2.3.4
15
+ ttl: 600
16
+ `;
17
+ const result = parseYaml(yaml);
18
+ assert.strictEqual(result.length, 2);
19
+ assert.deepStrictEqual(result[0], {
20
+ name: "www",
21
+ type: "CNAME",
22
+ value: "lb.google.com",
23
+ });
24
+ assert.deepStrictEqual(result[1], {
25
+ name: "mail",
26
+ type: "A",
27
+ value: "1.2.3.4",
28
+ ttl: 600,
29
+ });
30
+ });
31
+ test("ignores comments and blank lines in YAML", () => {
32
+ const yaml = `
33
+ # This is a comment at the top
34
+ - name: "@"
35
+ type: TXT # inline comment
36
+ value: "v=spf1 include:_spf.google.com ~all"
37
+
38
+ # Another comment with spaces
39
+ - name: api
40
+ type: CNAME
41
+ value: api.service.com
42
+ `;
43
+ const result = parseYaml(yaml);
44
+ assert.strictEqual(result.length, 2);
45
+ assert.strictEqual(result[0].name, "@");
46
+ assert.strictEqual(result[0].type, "TXT");
47
+ assert.strictEqual(result[0].value, "v=spf1 include:_spf.google.com ~all");
48
+ assert.strictEqual(result[1].name, "api");
49
+ });
50
+ test("parses nested array list values based on indentation", () => {
51
+ const yaml = `
52
+ - type: ingress
53
+ protocol: tcp
54
+ port: 80
55
+ sources:
56
+ - 0.0.0.0/0
57
+ - ::/0
58
+ - type: egress
59
+ protocol: tcp
60
+ port: all
61
+ destinations:
62
+ - 10.0.0.0/8
63
+ `;
64
+ const result = parseYaml(yaml);
65
+ assert.strictEqual(result.length, 2);
66
+ assert.deepStrictEqual(result[0], {
67
+ type: "ingress",
68
+ protocol: "tcp",
69
+ port: 80,
70
+ sources: ["0.0.0.0/0", "::/0"],
71
+ });
72
+ assert.deepStrictEqual(result[1], {
73
+ type: "egress",
74
+ protocol: "tcp",
75
+ port: "all",
76
+ destinations: ["10.0.0.0/8"],
77
+ });
78
+ });
79
+ test("loads from JSON and YAML files successfully", () => {
80
+ const tempJsonPath = path.resolve(process.cwd(), "temp-test-records.json");
81
+ const tempYamlPath = path.resolve(process.cwd(), "temp-test-records.yaml");
82
+ const jsonContent = JSON.stringify([
83
+ { name: "api", type: "CNAME", value: "lb.com" }
84
+ ]);
85
+ const yamlContent = `
86
+ - name: web
87
+ type: A
88
+ value: 5.6.7.8
89
+ `;
90
+ // Write temp files
91
+ fs.writeFileSync(tempJsonPath, jsonContent, "utf-8");
92
+ fs.writeFileSync(tempYamlPath, yamlContent, "utf-8");
93
+ try {
94
+ const parsedJson = loadRecordsFromFile("temp-test-records.json");
95
+ assert.strictEqual(parsedJson.length, 1);
96
+ assert.deepStrictEqual(parsedJson[0], {
97
+ name: "api",
98
+ type: "CNAME",
99
+ value: "lb.com",
100
+ });
101
+ const parsedYaml = loadRecordsFromFile("temp-test-records.yaml");
102
+ assert.strictEqual(parsedYaml.length, 1);
103
+ assert.deepStrictEqual(parsedYaml[0], {
104
+ name: "web",
105
+ type: "A",
106
+ value: "5.6.7.8",
107
+ });
108
+ }
109
+ finally {
110
+ // Clean up temp files
111
+ if (fs.existsSync(tempJsonPath))
112
+ fs.unlinkSync(tempJsonPath);
113
+ if (fs.existsSync(tempYamlPath))
114
+ fs.unlinkSync(tempYamlPath);
115
+ }
116
+ });
117
+ });
@@ -0,0 +1 @@
1
+ export {};