puls-dev 0.2.8 → 0.3.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.
Files changed (66) hide show
  1. package/dist/core/checker.js +71 -0
  2. package/dist/core/config.d.ts +5 -0
  3. package/dist/core/config.js +12 -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 +2 -0
  7. package/dist/core/decorators.js +8 -14
  8. package/dist/core/group.test.d.ts +1 -0
  9. package/dist/core/group.test.js +94 -0
  10. package/dist/core/parallel.test.d.ts +1 -0
  11. package/dist/core/parallel.test.js +215 -0
  12. package/dist/core/production.test.d.ts +1 -0
  13. package/dist/core/production.test.js +189 -0
  14. package/dist/core/provisioner.js +29 -11
  15. package/dist/core/resource.d.ts +8 -0
  16. package/dist/core/resource.js +45 -0
  17. package/dist/core/retry.d.ts +9 -0
  18. package/dist/core/retry.js +28 -0
  19. package/dist/core/retry.test.d.ts +1 -0
  20. package/dist/core/retry.test.js +66 -0
  21. package/dist/core/secret.d.ts +2 -1
  22. package/dist/core/secret.js +12 -2
  23. package/dist/core/stack.js +381 -75
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/providers/aws/api.js +97 -17
  27. package/dist/providers/aws/ec2.d.ts +3 -0
  28. package/dist/providers/aws/ec2.js +37 -3
  29. package/dist/providers/aws/ec2.test.js +5 -3
  30. package/dist/providers/aws/index.d.ts +2 -0
  31. package/dist/providers/aws/index.js +2 -0
  32. package/dist/providers/aws/secrets.js +20 -3
  33. package/dist/providers/aws/template.d.ts +34 -0
  34. package/dist/providers/aws/template.js +252 -0
  35. package/dist/providers/aws/template.test.d.ts +1 -0
  36. package/dist/providers/aws/template.test.js +208 -0
  37. package/dist/providers/do/api.d.ts +2 -0
  38. package/dist/providers/do/api.js +124 -26
  39. package/dist/providers/do/droplet.js +14 -0
  40. package/dist/providers/firebase/api.js +92 -29
  41. package/dist/providers/firebase/list.d.ts +2 -0
  42. package/dist/providers/firebase/list.js +25 -0
  43. package/dist/providers/gcp/api.js +88 -14
  44. package/dist/providers/gcp/index.d.ts +3 -1
  45. package/dist/providers/gcp/index.js +3 -1
  46. package/dist/providers/gcp/list.d.ts +2 -0
  47. package/dist/providers/gcp/list.js +55 -0
  48. package/dist/providers/gcp/secrets.js +21 -4
  49. package/dist/providers/gcp/template.d.ts +32 -0
  50. package/dist/providers/gcp/template.js +252 -0
  51. package/dist/providers/gcp/template.test.d.ts +1 -0
  52. package/dist/providers/gcp/template.test.js +227 -0
  53. package/dist/providers/gcp/vm.d.ts +3 -0
  54. package/dist/providers/gcp/vm.js +46 -3
  55. package/dist/providers/proxmox/api.d.ts +1 -0
  56. package/dist/providers/proxmox/api.js +72 -16
  57. package/dist/providers/proxmox/index.d.ts +3 -1
  58. package/dist/providers/proxmox/index.js +14 -1
  59. package/dist/providers/proxmox/template.d.ts +44 -0
  60. package/dist/providers/proxmox/template.js +350 -0
  61. package/dist/providers/proxmox/template.test.d.ts +1 -0
  62. package/dist/providers/proxmox/template.test.js +215 -0
  63. package/dist/providers/proxmox/vm.d.ts +3 -0
  64. package/dist/providers/proxmox/vm.js +43 -11
  65. package/dist/types/inventory.d.ts +44 -1
  66. package/package.json +2 -2
@@ -123,6 +123,53 @@ function renderAws(inv) {
123
123
  ]);
124
124
  }
125
125
  }
126
+ function renderGcp(inv) {
127
+ if (inv.vms.length > 0) {
128
+ printSection(`GCP Compute VMs · ${inv.vms.length}`, inv.vms, [
129
+ { header: "Name", width: 24, render: (v) => v.name },
130
+ { header: "Zone", width: 15, render: (v) => v.zone },
131
+ { header: "Machine Type", width: 14, render: (v) => v.machineType },
132
+ { header: "Status", width: 8, render: (v) => v.status },
133
+ { header: "IP", width: 15, render: (v) => v.ip },
134
+ ]);
135
+ }
136
+ if (inv.rdsInstances.length > 0) {
137
+ printSection(`GCP Cloud SQL · ${inv.rdsInstances.length}`, inv.rdsInstances, [
138
+ { header: "Name", width: 24, render: (i) => i.name },
139
+ { header: "Engine", width: 18, render: (i) => i.engine },
140
+ { header: "Tier", width: 12, render: (i) => i.tier },
141
+ { header: "Status", width: 10, render: (i) => i.status },
142
+ ]);
143
+ }
144
+ if (inv.distributions.length > 0) {
145
+ printSection(`GCP Cloud Run · ${inv.distributions.length}`, inv.distributions, [
146
+ { header: "Service", width: 24, render: (s) => s.name },
147
+ { header: "Region", width: 12, render: (s) => s.region },
148
+ { header: "URL", width: 42, render: (s) => s.url },
149
+ ]);
150
+ }
151
+ if (inv.hostedZones.length > 0) {
152
+ printSection(`GCP Cloud DNS · ${inv.hostedZones.length}`, inv.hostedZones, [
153
+ { header: "Zone", width: 24, render: (z) => z.name },
154
+ { header: "DNS Name", width: 32, render: (z) => z.dnsName },
155
+ ]);
156
+ }
157
+ }
158
+ function renderFirebase(inv) {
159
+ if (inv.hostingSites.length > 0) {
160
+ printSection(`Firebase Hosting · ${inv.hostingSites.length}`, inv.hostingSites, [
161
+ { header: "Site ID", width: 32, render: (s) => s.site },
162
+ ]);
163
+ }
164
+ if (inv.functions.length > 0) {
165
+ printSection(`Firebase Functions · ${inv.functions.length}`, inv.functions, [
166
+ { header: "Function", width: 24, render: (f) => f.name },
167
+ { header: "Region", width: 12, render: (f) => f.region },
168
+ { header: "Entry Point", width: 18, render: (f) => f.entryPoint },
169
+ { header: "Runtime", width: 10, render: (f) => f.runtime },
170
+ ]);
171
+ }
172
+ }
126
173
  // ─── Checker ──────────────────────────────────────────────────────────────────
127
174
  export class Checker {
128
175
  async check() {
@@ -161,6 +208,26 @@ export class Checker {
161
208
  errors.push({ provider: "aws", message: err.message });
162
209
  }));
163
210
  }
211
+ if (cfg.providers.gcp?.serviceAccountPath || process.env.GCP_SA) {
212
+ tasks.push(import("../providers/gcp/list.js")
213
+ .then((m) => m.listGcpResources())
214
+ .then((inv) => {
215
+ result.gcp = inv;
216
+ })
217
+ .catch((err) => {
218
+ errors.push({ provider: "gcp", message: err.message });
219
+ }));
220
+ }
221
+ if (cfg.providers.firebase?.serviceAccountPath || process.env.FIREBASE_SA) {
222
+ tasks.push(import("../providers/firebase/list.js")
223
+ .then((m) => m.listFirebaseResources())
224
+ .then((inv) => {
225
+ result.firebase = inv;
226
+ })
227
+ .catch((err) => {
228
+ errors.push({ provider: "firebase", message: err.message });
229
+ }));
230
+ }
164
231
  await Promise.all(tasks);
165
232
  if (result.proxmox)
166
233
  renderProxmox(result.proxmox);
@@ -168,6 +235,10 @@ export class Checker {
168
235
  renderDo(result.do);
169
236
  if (result.aws)
170
237
  renderAws(result.aws);
238
+ if (result.gcp)
239
+ renderGcp(result.gcp);
240
+ if (result.firebase)
241
+ renderFirebase(result.firebase);
171
242
  for (const e of errors) {
172
243
  console.warn(`\n [!] ${e.provider}: ${e.message}`);
173
244
  }
@@ -1,5 +1,8 @@
1
+ import "dotenv/config";
1
2
  export interface GlobalConfig {
2
3
  dryRun?: boolean;
4
+ parallel?: boolean;
5
+ offline?: boolean;
3
6
  providers: {
4
7
  do?: {
5
8
  token: string;
@@ -37,6 +40,8 @@ declare class ConfigManager {
37
40
  set(newConfig: Partial<GlobalConfig>): void;
38
41
  get(): GlobalConfig;
39
42
  isGlobalDryRun(): boolean;
43
+ isParallelActive(): boolean;
44
+ isOfflineMode(): boolean;
40
45
  }
41
46
  export declare const Config: ConfigManager;
42
47
  export {};
@@ -1,9 +1,14 @@
1
+ import "dotenv/config";
1
2
  class ConfigManager {
2
3
  config = {
3
4
  providers: {},
4
5
  };
5
6
  set(newConfig) {
6
- this.config = { ...this.config, ...newConfig };
7
+ this.config = {
8
+ ...this.config,
9
+ ...newConfig,
10
+ providers: { ...this.config.providers, ...newConfig.providers },
11
+ };
7
12
  }
8
13
  get() {
9
14
  return this.config;
@@ -11,5 +16,11 @@ class ConfigManager {
11
16
  isGlobalDryRun() {
12
17
  return this.config.dryRun ?? false;
13
18
  }
19
+ isParallelActive() {
20
+ return this.config.parallel ?? false;
21
+ }
22
+ isOfflineMode() {
23
+ return this.config.offline ?? false;
24
+ }
14
25
  }
15
26
  export const Config = new ConfigManager();
@@ -0,0 +1,14 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export interface HostEntry {
3
+ name: string;
4
+ ip: string;
5
+ user: string;
6
+ sshKey?: string;
7
+ provider: string;
8
+ }
9
+ export interface ResourceContext {
10
+ abortSignal?: AbortSignal;
11
+ hosts?: HostEntry[];
12
+ stackName?: string;
13
+ }
14
+ export declare const resourceContextStorage: AsyncLocalStorage<ResourceContext>;
@@ -0,0 +1,2 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export const resourceContextStorage = new AsyncLocalStorage();
@@ -4,6 +4,8 @@ type ProviderOpts = {
4
4
  region?: string;
5
5
  regions?: string[];
6
6
  dryRun?: boolean;
7
+ parallel?: boolean;
8
+ offline?: boolean;
7
9
  firebase?: string;
8
10
  proxmox?: {
9
11
  url: string;
@@ -5,27 +5,21 @@ import { Stack } from "./stack.js";
5
5
  function applyConfig(opts) {
6
6
  if (opts.dryRun !== undefined)
7
7
  Config.set({ dryRun: opts.dryRun });
8
+ if (opts.parallel !== undefined)
9
+ Config.set({ parallel: opts.parallel });
10
+ if (opts.offline !== undefined)
11
+ Config.set({ offline: opts.offline });
8
12
  if (opts.token)
9
- Config.set({
10
- providers: { ...Config.get().providers, do: { token: opts.token } },
11
- });
13
+ Config.set({ providers: { do: { token: opts.token } } });
12
14
  if (opts.region)
13
- Config.set({
14
- providers: { ...Config.get().providers, aws: { region: opts.region } },
15
- });
15
+ Config.set({ providers: { aws: { region: opts.region } } });
16
16
  if (opts.proxmox)
17
- Config.set({
18
- providers: { ...Config.get().providers, proxmox: opts.proxmox },
19
- });
17
+ Config.set({ providers: { proxmox: opts.proxmox } });
20
18
  if (opts.firebase) {
21
19
  const sa = JSON.parse(readFileSync(opts.firebase, "utf8"));
22
20
  Config.set({
23
21
  providers: {
24
- ...Config.get().providers,
25
- firebase: {
26
- projectId: sa.project_id,
27
- serviceAccountPath: opts.firebase,
28
- },
22
+ firebase: { projectId: sa.project_id, serviceAccountPath: opts.firebase },
29
23
  },
30
24
  });
31
25
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import { test, describe } from "node:test";
2
+ import assert from "node:assert";
3
+ import { BaseBuilder, createBuilderArray } from "./resource.js";
4
+ import { Stack } from "./stack.js";
5
+ import { Config } from "./config.js";
6
+ class MockResourceBuilder extends BaseBuilder {
7
+ val = 0;
8
+ strVal = "";
9
+ deployCount = 0;
10
+ destroyCount = 0;
11
+ constructor(name) {
12
+ super(name);
13
+ }
14
+ setVal(n) {
15
+ // Overloaded to support array assignment in proxy test
16
+ if (typeof n === "number") {
17
+ this.val = n;
18
+ }
19
+ return this;
20
+ }
21
+ setStr(s) {
22
+ if (typeof s === "string") {
23
+ this.strVal = s;
24
+ }
25
+ return this;
26
+ }
27
+ async deploy() {
28
+ this.deployCount++;
29
+ return { name: this.name, val: this.val, strVal: this.strVal };
30
+ }
31
+ async destroy() {
32
+ this.destroyCount++;
33
+ return { destroyedName: this.name };
34
+ }
35
+ }
36
+ describe("Resource Builder Groups Unit Tests", () => {
37
+ test("createBuilderArray proxies and chains methods", () => {
38
+ const b1 = new MockResourceBuilder("r1");
39
+ const b2 = new MockResourceBuilder("r2");
40
+ const group = createBuilderArray([b1, b2]);
41
+ // Test method chaining and uniform distribution
42
+ group.setVal(42).setStr("hello");
43
+ assert.strictEqual(b1.val, 42);
44
+ assert.strictEqual(b2.val, 42);
45
+ assert.strictEqual(b1.strVal, "hello");
46
+ assert.strictEqual(b2.strVal, "hello");
47
+ });
48
+ test("createBuilderArray distributes array arguments by index", () => {
49
+ const b1 = new MockResourceBuilder("r1");
50
+ const b2 = new MockResourceBuilder("r2");
51
+ const group = createBuilderArray([b1, b2]);
52
+ // Test index-based distribution
53
+ group.setVal([10, 20]).setStr(["first", "second"]);
54
+ // In MockResourceBuilder, calling setVal with [10, 20] gets distributed by proxy:
55
+ // b1 gets setVal(10) -> val = 10
56
+ // b2 gets setVal(20) -> val = 20
57
+ assert.strictEqual(b1.val, 10);
58
+ assert.strictEqual(b2.val, 20);
59
+ assert.strictEqual(b1.strVal, "first");
60
+ assert.strictEqual(b2.strVal, "second");
61
+ });
62
+ test("Stack deploy gathers array builders and structures outputs as array", async () => {
63
+ Config.set({ dryRun: false });
64
+ const b1 = new MockResourceBuilder("s1");
65
+ const b2 = new MockResourceBuilder("s2");
66
+ class ArrayStack extends Stack {
67
+ servers = createBuilderArray([b1, b2]).setVal([100, 200]);
68
+ }
69
+ const stack = new ArrayStack();
70
+ const result = await stack.deploy();
71
+ assert.strictEqual(b1.deployCount, 1);
72
+ assert.strictEqual(b2.deployCount, 1);
73
+ assert.ok(Array.isArray(result.servers));
74
+ assert.strictEqual(result.servers.length, 2);
75
+ assert.deepStrictEqual(result.servers[0], { name: "s1", val: 100, strVal: "" });
76
+ assert.deepStrictEqual(result.servers[1], { name: "s2", val: 200, strVal: "" });
77
+ });
78
+ test("Stack destroy gathers array builders and structures outputs as array", async () => {
79
+ Config.set({ dryRun: false });
80
+ const b1 = new MockResourceBuilder("s1");
81
+ const b2 = new MockResourceBuilder("s2");
82
+ class ArrayStack extends Stack {
83
+ servers = createBuilderArray([b1, b2]);
84
+ }
85
+ const stack = new ArrayStack();
86
+ const result = await stack.destroy();
87
+ assert.strictEqual(b1.destroyCount, 1);
88
+ assert.strictEqual(b2.destroyCount, 1);
89
+ assert.ok(Array.isArray(result.servers));
90
+ assert.strictEqual(result.servers.length, 2);
91
+ assert.deepStrictEqual(result.servers[0], { destroyedName: "s1" });
92
+ assert.deepStrictEqual(result.servers[1], { destroyedName: "s2" });
93
+ });
94
+ });
@@ -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 @@
1
+ export {};