puls-dev 0.3.6 → 0.3.7

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 (47) hide show
  1. package/README.md +11 -11
  2. package/dist/bin/install-shell.js +5 -6
  3. package/dist/bin/puls.js +10 -3
  4. package/dist/core/config.d.ts +4 -0
  5. package/dist/core/decorators.d.ts +4 -0
  6. package/dist/core/decorators.js +2 -0
  7. package/dist/core/parallel.test.js +4 -3
  8. package/dist/core/resource.d.ts +2 -1
  9. package/dist/core/resource.js +4 -2
  10. package/dist/core/stack.d.ts +4 -0
  11. package/dist/core/stack.js +8 -8
  12. package/dist/providers/aws/acm.test.d.ts +1 -0
  13. package/dist/providers/aws/acm.test.js +167 -0
  14. package/dist/providers/aws/cloudfront.test.d.ts +1 -0
  15. package/dist/providers/aws/cloudfront.test.js +170 -0
  16. package/dist/providers/aws/fargate.test.d.ts +1 -0
  17. package/dist/providers/aws/fargate.test.js +244 -0
  18. package/dist/providers/aws/rds.test.d.ts +1 -0
  19. package/dist/providers/aws/rds.test.js +219 -0
  20. package/dist/providers/aws/sqs.test.d.ts +1 -0
  21. package/dist/providers/aws/sqs.test.js +181 -0
  22. package/dist/providers/cloudflare/api.d.ts +15 -0
  23. package/dist/providers/cloudflare/api.js +199 -0
  24. package/dist/providers/cloudflare/index.d.ts +14 -0
  25. package/dist/providers/cloudflare/index.js +19 -0
  26. package/dist/providers/cloudflare/kv.d.ts +20 -0
  27. package/dist/providers/cloudflare/kv.js +69 -0
  28. package/dist/providers/cloudflare/kv.test.d.ts +1 -0
  29. package/dist/providers/cloudflare/kv.test.js +134 -0
  30. package/dist/providers/cloudflare/r2.d.ts +14 -0
  31. package/dist/providers/cloudflare/r2.js +57 -0
  32. package/dist/providers/cloudflare/r2.test.d.ts +1 -0
  33. package/dist/providers/cloudflare/r2.test.js +132 -0
  34. package/dist/providers/cloudflare/worker.d.ts +28 -0
  35. package/dist/providers/cloudflare/worker.js +172 -0
  36. package/dist/providers/cloudflare/worker.test.d.ts +1 -0
  37. package/dist/providers/cloudflare/worker.test.js +220 -0
  38. package/dist/providers/cloudflare/zone.d.ts +42 -0
  39. package/dist/providers/cloudflare/zone.js +280 -0
  40. package/dist/providers/cloudflare/zone.test.d.ts +1 -0
  41. package/dist/providers/cloudflare/zone.test.js +284 -0
  42. package/dist/providers/firebase/auth.test.d.ts +1 -0
  43. package/dist/providers/firebase/auth.test.js +145 -0
  44. package/dist/providers/firebase/hosting.test.js +7 -6
  45. package/dist/providers/firebase/storage.test.d.ts +1 -0
  46. package/dist/providers/firebase/storage.test.js +148 -0
  47. package/package.json +6 -2
@@ -0,0 +1,57 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { getCloudflareApi, getCloudflareAccountId } from "./api.js";
3
+ export class R2Builder extends BaseBuilder {
4
+ bucketName;
5
+ constructor(bucketName) {
6
+ super(bucketName);
7
+ this.bucketName = bucketName;
8
+ this.discoveryPromise = this.discoverBucket(bucketName);
9
+ }
10
+ async discoverBucket(name) {
11
+ try {
12
+ const api = getCloudflareApi();
13
+ const accountId = getCloudflareAccountId();
14
+ const res = await api.get(`/accounts/${accountId}/r2/buckets`);
15
+ return (res.result?.buckets ?? []).find((b) => b.name === name) ?? null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ async deploy() {
22
+ const dryRun = this.isDryRunActive();
23
+ const existing = await this.discoveryPromise;
24
+ const api = getCloudflareApi();
25
+ const accountId = getCloudflareAccountId();
26
+ console.log(`\n🪣 Finalizing Cloudflare R2 Bucket "${this.bucketName}"...`);
27
+ if (existing) {
28
+ console.log(` āœ… R2 Bucket "${this.bucketName}" already exists`);
29
+ return { bucket: this.bucketName };
30
+ }
31
+ if (dryRun) {
32
+ console.log(` šŸ“ [PLAN] Create R2 Bucket "${this.bucketName}"`);
33
+ return { bucket: this.bucketName };
34
+ }
35
+ await api.put(`/accounts/${accountId}/r2/buckets/${this.bucketName}`, {});
36
+ console.log(`šŸš€ Created R2 Bucket "${this.bucketName}"`);
37
+ return { bucket: this.bucketName };
38
+ }
39
+ async destroy() {
40
+ const dryRun = this.isDryRunActive();
41
+ const existing = await this.discoveryPromise;
42
+ const api = getCloudflareApi();
43
+ const accountId = getCloudflareAccountId();
44
+ console.log(`\nšŸ—‘ļø Destroying Cloudflare R2 Bucket "${this.bucketName}"...`);
45
+ if (!existing) {
46
+ console.log(` ─ R2 Bucket "${this.bucketName}" not found`);
47
+ return { destroyed: false };
48
+ }
49
+ if (dryRun) {
50
+ console.log(` šŸ“ [PLAN] Delete R2 Bucket "${this.bucketName}"`);
51
+ return { destroyed: this.bucketName };
52
+ }
53
+ await api.delete(`/accounts/${accountId}/r2/buckets/${this.bucketName}`);
54
+ console.log(` šŸ—‘ļø Removed R2 Bucket "${this.bucketName}"`);
55
+ return { destroyed: this.bucketName };
56
+ }
57
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,132 @@
1
+ import { test, describe, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { R2Builder } from "./r2.js";
4
+ import { Config } from "../../core/config.js";
5
+ describe("R2Builder Unit Tests", () => {
6
+ let originalFetch;
7
+ let fetchCalls = [];
8
+ let mockResponses = {};
9
+ beforeEach(() => {
10
+ Config.set({
11
+ dryRun: false,
12
+ providers: {
13
+ cloudflare: { token: "fake-cf-token", accountId: "fake-cf-account" }
14
+ }
15
+ });
16
+ originalFetch = globalThis.fetch;
17
+ fetchCalls = [];
18
+ mockResponses = {};
19
+ globalThis.fetch = async (input, init) => {
20
+ const url = String(input);
21
+ const method = init?.method ?? "GET";
22
+ let body;
23
+ if (init?.body) {
24
+ if (typeof init.body === "string") {
25
+ try {
26
+ body = JSON.parse(init.body);
27
+ }
28
+ catch {
29
+ body = init.body;
30
+ }
31
+ }
32
+ else {
33
+ body = init.body;
34
+ }
35
+ }
36
+ const headers = init?.headers;
37
+ fetchCalls.push({ url, method, body, headers });
38
+ const matchKey = Object.keys(mockResponses).find(key => {
39
+ const [mMethod, mPath] = key.split(" ");
40
+ return method === mMethod && url.endsWith(mPath);
41
+ });
42
+ if (matchKey) {
43
+ const resp = mockResponses[matchKey];
44
+ return {
45
+ ok: resp.status >= 200 && resp.status < 300,
46
+ status: resp.status,
47
+ json: async () => resp.body,
48
+ text: async () => JSON.stringify(resp.body),
49
+ };
50
+ }
51
+ return {
52
+ ok: false,
53
+ status: 404,
54
+ json: async () => ({ errors: [{ message: "Not found" }] }),
55
+ text: async () => "Not found",
56
+ };
57
+ };
58
+ });
59
+ afterEach(() => {
60
+ globalThis.fetch = originalFetch;
61
+ });
62
+ test("discovers bucket successfully if it already exists", async () => {
63
+ mockResponses["GET /accounts/fake-cf-account/r2/buckets"] = {
64
+ status: 200,
65
+ body: {
66
+ result: {
67
+ buckets: [
68
+ { name: "my-bucket" }
69
+ ]
70
+ }
71
+ }
72
+ };
73
+ const builder = new R2Builder("my-bucket");
74
+ const result = await builder.deploy();
75
+ assert.deepStrictEqual(result, { bucket: "my-bucket" });
76
+ const puts = fetchCalls.filter(c => c.method === "PUT");
77
+ assert.strictEqual(puts.length, 0);
78
+ });
79
+ test("creates bucket if it does not exist", async () => {
80
+ mockResponses["GET /accounts/fake-cf-account/r2/buckets"] = {
81
+ status: 200,
82
+ body: { result: { buckets: [] } }
83
+ };
84
+ mockResponses["PUT /accounts/fake-cf-account/r2/buckets/my-bucket"] = {
85
+ status: 200,
86
+ body: {}
87
+ };
88
+ const builder = new R2Builder("my-bucket");
89
+ const result = await builder.deploy();
90
+ assert.deepStrictEqual(result, { bucket: "my-bucket" });
91
+ const putCall = fetchCalls.find(c => c.method === "PUT");
92
+ assert.ok(putCall);
93
+ });
94
+ test("does not call fetch during dryRun deploy", async () => {
95
+ Config.set({
96
+ dryRun: true,
97
+ providers: {
98
+ cloudflare: { token: "fake-cf-token", accountId: "fake-cf-account" }
99
+ }
100
+ });
101
+ mockResponses["GET /accounts/fake-cf-account/r2/buckets"] = {
102
+ status: 200,
103
+ body: { result: { buckets: [] } }
104
+ };
105
+ const builder = new R2Builder("my-bucket");
106
+ const result = await builder.deploy();
107
+ assert.deepStrictEqual(result, { bucket: "my-bucket" });
108
+ const puts = fetchCalls.filter(c => c.method === "PUT");
109
+ assert.strictEqual(puts.length, 0);
110
+ });
111
+ test("destroys bucket successfully if exists", async () => {
112
+ mockResponses["GET /accounts/fake-cf-account/r2/buckets"] = {
113
+ status: 200,
114
+ body: {
115
+ result: {
116
+ buckets: [
117
+ { name: "my-bucket" }
118
+ ]
119
+ }
120
+ }
121
+ };
122
+ mockResponses["DELETE /accounts/fake-cf-account/r2/buckets/my-bucket"] = {
123
+ status: 200,
124
+ body: {}
125
+ };
126
+ const builder = new R2Builder("my-bucket");
127
+ const result = await builder.destroy();
128
+ assert.deepStrictEqual(result, { destroyed: "my-bucket" });
129
+ const deleteCall = fetchCalls.find(c => c.method === "DELETE");
130
+ assert.ok(deleteCall);
131
+ });
132
+ });
@@ -0,0 +1,28 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { Output } from "../../core/output.js";
3
+ import { KVBuilder } from "./kv.js";
4
+ import { R2Builder } from "./r2.js";
5
+ export declare class WorkerBuilder extends BaseBuilder {
6
+ scriptName: string;
7
+ private _scriptPath?;
8
+ private _routes;
9
+ private _kvs;
10
+ private _r2s;
11
+ private _envs;
12
+ constructor(scriptName: string);
13
+ script(filePath: string): this;
14
+ route(pattern: string): this;
15
+ kv(bindingName: string, kvNamespace: KVBuilder): this;
16
+ r2(bindingName: string, r2Bucket: R2Builder): this;
17
+ env(bindingName: string, value: string | Output<string>): this;
18
+ deploy(): Promise<{
19
+ scriptName: string;
20
+ routes?: undefined;
21
+ } | {
22
+ scriptName: string;
23
+ routes: string[];
24
+ }>;
25
+ destroy(): Promise<{
26
+ destroyed: string;
27
+ }>;
28
+ }
@@ -0,0 +1,172 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { BaseBuilder } from "../../core/resource.js";
3
+ import { Output } from "../../core/output.js";
4
+ import { getCloudflareApi, getCloudflareAccountId } from "./api.js";
5
+ async function getZoneIdForPattern(pattern, api) {
6
+ let host = pattern.split("/")[0];
7
+ const parts = host.split(".");
8
+ for (let i = 0; i < parts.length - 1; i++) {
9
+ const possibleZone = parts.slice(i).join(".");
10
+ try {
11
+ const res = await api.get(`/zones?name=${possibleZone}`);
12
+ if (res.result && res.result.length > 0) {
13
+ return res.result[0].id;
14
+ }
15
+ }
16
+ catch {
17
+ // Ignore and try next parent domain
18
+ }
19
+ }
20
+ throw new Error(`Could not find Cloudflare DNS Zone for route pattern "${pattern}"`);
21
+ }
22
+ export class WorkerBuilder extends BaseBuilder {
23
+ scriptName;
24
+ _scriptPath;
25
+ _routes = [];
26
+ _kvs = new Map();
27
+ _r2s = new Map();
28
+ _envs = new Map();
29
+ constructor(scriptName) {
30
+ super(scriptName);
31
+ this.scriptName = scriptName;
32
+ }
33
+ script(filePath) {
34
+ this._scriptPath = filePath;
35
+ return this;
36
+ }
37
+ route(pattern) {
38
+ this._routes.push(pattern);
39
+ return this;
40
+ }
41
+ kv(bindingName, kvNamespace) {
42
+ this._kvs.set(bindingName, kvNamespace);
43
+ this.dependsOn(kvNamespace);
44
+ return this;
45
+ }
46
+ r2(bindingName, r2Bucket) {
47
+ this._r2s.set(bindingName, r2Bucket);
48
+ this.dependsOn(r2Bucket);
49
+ return this;
50
+ }
51
+ env(bindingName, value) {
52
+ this._envs.set(bindingName, value);
53
+ return this;
54
+ }
55
+ async deploy() {
56
+ if (!this._scriptPath) {
57
+ throw new Error(`Worker script path is not configured for "${this.name}". Call .script("filePath")`);
58
+ }
59
+ const dryRun = this.isDryRunActive();
60
+ const api = getCloudflareApi();
61
+ const accountId = getCloudflareAccountId();
62
+ console.log(`\n⚔ Finalizing Cloudflare Worker "${this.scriptName}"...`);
63
+ const metadata = {
64
+ main_module: "index.js",
65
+ bindings: [],
66
+ };
67
+ // Resolve KV Bindings
68
+ for (const [binding, kv] of this._kvs.entries()) {
69
+ const kvId = dryRun ? "mock-kv-id" : await kv.out.id.get();
70
+ metadata.bindings.push({
71
+ type: "kv_namespace",
72
+ name: binding,
73
+ namespace_id: kvId,
74
+ });
75
+ }
76
+ // Resolve R2 Bindings
77
+ for (const [binding, r2] of this._r2s.entries()) {
78
+ metadata.bindings.push({
79
+ type: "r2_bucket",
80
+ name: binding,
81
+ bucket_name: r2.bucketName,
82
+ });
83
+ }
84
+ // Resolve Env Bindings
85
+ for (const [binding, val] of this._envs.entries()) {
86
+ const resolvedVal = val instanceof Output ? await val.get() : val;
87
+ metadata.bindings.push({
88
+ type: "plain_text",
89
+ name: binding,
90
+ text: resolvedVal,
91
+ });
92
+ }
93
+ if (dryRun) {
94
+ console.log(` šŸ“ [PLAN] Upload Worker script "${this.scriptName}"`);
95
+ for (const r of this._routes) {
96
+ console.log(` └─ Route: ${r}`);
97
+ }
98
+ return { scriptName: this.scriptName };
99
+ }
100
+ // Load and build FormData
101
+ const scriptContent = readFileSync(this._scriptPath, "utf8");
102
+ const form = new FormData();
103
+ form.append("metadata", JSON.stringify(metadata));
104
+ form.append("script", new Blob([scriptContent], { type: "application/javascript+module" }), "index.js");
105
+ await api.put(`/accounts/${accountId}/workers/scripts/${this.scriptName}`, form);
106
+ console.log(`šŸš€ Uploaded Worker script "${this.scriptName}"`);
107
+ // Reconcile Routes
108
+ for (const pattern of this._routes) {
109
+ const zoneId = await getZoneIdForPattern(pattern, api);
110
+ const routesRes = await api.get(`/zones/${zoneId}/workers/routes`);
111
+ const existingRoute = (routesRes.result ?? []).find((r) => r.pattern === pattern);
112
+ if (existingRoute) {
113
+ if (existingRoute.script !== this.scriptName) {
114
+ await api.put(`/zones/${zoneId}/workers/routes/${existingRoute.id}`, {
115
+ pattern,
116
+ script: this.scriptName,
117
+ });
118
+ console.log(` šŸ”„ Updated route ${pattern} → ${this.scriptName}`);
119
+ }
120
+ else {
121
+ console.log(` āœ… Route ${pattern} is up to date`);
122
+ }
123
+ }
124
+ else {
125
+ await api.post(`/zones/${zoneId}/workers/routes`, {
126
+ pattern,
127
+ script: this.scriptName,
128
+ });
129
+ console.log(` šŸš€ Created route ${pattern} → ${this.scriptName}`);
130
+ }
131
+ }
132
+ return { scriptName: this.scriptName, routes: this._routes };
133
+ }
134
+ async destroy() {
135
+ const dryRun = this.isDryRunActive();
136
+ const api = getCloudflareApi();
137
+ const accountId = getCloudflareAccountId();
138
+ console.log(`\nšŸ—‘ļø Destroying Cloudflare Worker "${this.scriptName}"...`);
139
+ if (dryRun) {
140
+ console.log(` šŸ“ [PLAN] Delete Worker script "${this.scriptName}"`);
141
+ return { destroyed: this.scriptName };
142
+ }
143
+ // Clean up routes pointing to this script
144
+ if (this._routes.length > 0) {
145
+ for (const pattern of this._routes) {
146
+ try {
147
+ const zoneId = await getZoneIdForPattern(pattern, api);
148
+ const routesRes = await api.get(`/zones/${zoneId}/workers/routes`);
149
+ const match = (routesRes.result ?? []).find((r) => r.pattern === pattern && r.script === this.scriptName);
150
+ if (match) {
151
+ await api.delete(`/zones/${zoneId}/workers/routes/${match.id}`);
152
+ console.log(` šŸ—‘ļø Removed route ${pattern}`);
153
+ }
154
+ }
155
+ catch {
156
+ // Ignore if zone or route is already gone
157
+ }
158
+ }
159
+ }
160
+ try {
161
+ await api.delete(`/accounts/${accountId}/workers/scripts/${this.scriptName}`);
162
+ console.log(` šŸ—‘ļø Removed Worker script "${this.scriptName}"`);
163
+ }
164
+ catch (err) {
165
+ // Ignore if already deleted
166
+ if (!err.message.includes("404"))
167
+ throw err;
168
+ }
169
+ await this.destroySidecars();
170
+ return { destroyed: this.scriptName };
171
+ }
172
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,220 @@
1
+ import { test, describe, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { WorkerBuilder } from "./worker.js";
6
+ import { KVBuilder } from "./kv.js";
7
+ import { R2Builder } from "./r2.js";
8
+ import { Config } from "../../core/config.js";
9
+ describe("WorkerBuilder Unit Tests", () => {
10
+ let originalFetch;
11
+ let fetchCalls = [];
12
+ let mockResponses = {};
13
+ const tempScriptPath = path.resolve(process.cwd(), "temp-worker.js");
14
+ beforeEach(() => {
15
+ Config.set({
16
+ dryRun: false,
17
+ providers: {
18
+ cloudflare: { token: "fake-cf-token", accountId: "fake-cf-account" }
19
+ }
20
+ });
21
+ originalFetch = globalThis.fetch;
22
+ fetchCalls = [];
23
+ mockResponses = {};
24
+ fs.writeFileSync(tempScriptPath, "export default { fetch() { return new Response('hello'); } };", "utf8");
25
+ globalThis.fetch = async (input, init) => {
26
+ const url = String(input);
27
+ const method = init?.method ?? "GET";
28
+ let body;
29
+ if (init?.body) {
30
+ if (init.body instanceof FormData) {
31
+ body = init.body;
32
+ }
33
+ else if (typeof init.body === "string") {
34
+ try {
35
+ body = JSON.parse(init.body);
36
+ }
37
+ catch {
38
+ body = init.body;
39
+ }
40
+ }
41
+ else {
42
+ body = init.body;
43
+ }
44
+ }
45
+ const headers = init?.headers;
46
+ fetchCalls.push({ url, method, body, headers });
47
+ const matchKey = Object.keys(mockResponses).find(key => {
48
+ const [mMethod, mPath] = key.split(" ");
49
+ return method === mMethod && url.endsWith(mPath);
50
+ });
51
+ if (matchKey) {
52
+ const resp = mockResponses[matchKey];
53
+ return {
54
+ ok: resp.status >= 200 && resp.status < 300,
55
+ status: resp.status,
56
+ json: async () => resp.body,
57
+ text: async () => JSON.stringify(resp.body),
58
+ };
59
+ }
60
+ return {
61
+ ok: false,
62
+ status: 404,
63
+ json: async () => ({ errors: [{ message: "Not found" }] }),
64
+ text: async () => "Not found",
65
+ };
66
+ };
67
+ });
68
+ afterEach(() => {
69
+ globalThis.fetch = originalFetch;
70
+ if (fs.existsSync(tempScriptPath)) {
71
+ fs.unlinkSync(tempScriptPath);
72
+ }
73
+ });
74
+ test("throws if script path is not configured", async () => {
75
+ const builder = new WorkerBuilder("my-worker");
76
+ await assert.rejects(async () => {
77
+ await builder.deploy();
78
+ }, /Worker script path is not configured/);
79
+ });
80
+ test("deploys worker script with bindings and reconciles routes", async () => {
81
+ // KV namespace mock response for builder dependency discovery
82
+ mockResponses["GET /accounts/fake-cf-account/workers/namespaces?per_page=100"] = {
83
+ status: 200,
84
+ body: {
85
+ result: [{ id: "kv-id-999", title: "my-kv" }]
86
+ }
87
+ };
88
+ // R2 mock response for discovery
89
+ mockResponses["GET /accounts/fake-cf-account/r2/buckets"] = {
90
+ status: 200,
91
+ body: {
92
+ result: { buckets: [{ name: "my-bucket" }] }
93
+ }
94
+ };
95
+ // Worker upload mock response
96
+ mockResponses["PUT /accounts/fake-cf-account/workers/scripts/my-worker"] = {
97
+ status: 200,
98
+ body: { result: { id: "my-worker" } }
99
+ };
100
+ // Route resolution: zone matching
101
+ mockResponses["GET /zones?name=api.example.com"] = {
102
+ status: 200,
103
+ body: { result: [] }
104
+ };
105
+ mockResponses["GET /zones?name=example.com"] = {
106
+ status: 200,
107
+ body: { result: [{ id: "zone-123", name: "example.com" }] }
108
+ };
109
+ // Get routes mock response (1 existing route to update, 1 new route to create)
110
+ mockResponses["GET /zones/zone-123/workers/routes"] = {
111
+ status: 200,
112
+ body: {
113
+ result: [
114
+ { id: "route-existing", pattern: "api.example.com/*", script: "old-worker" }
115
+ ]
116
+ }
117
+ };
118
+ mockResponses["PUT /zones/zone-123/workers/routes/route-existing"] = {
119
+ status: 200,
120
+ body: { result: { id: "route-existing" } }
121
+ };
122
+ mockResponses["POST /zones/zone-123/workers/routes"] = {
123
+ status: 200,
124
+ body: { result: { id: "route-new" } }
125
+ };
126
+ const kv = new KVBuilder("my-kv");
127
+ const r2 = new R2Builder("my-bucket");
128
+ // Initialize/deploy dependencies first
129
+ await kv.deploy();
130
+ await r2.deploy();
131
+ const worker = new WorkerBuilder("my-worker")
132
+ .script(tempScriptPath)
133
+ .kv("MY_KV", kv)
134
+ .r2("MY_BUCKET", r2)
135
+ .env("ENV_VAR", "my-env-value")
136
+ .route("api.example.com/*")
137
+ .route("example.com/*");
138
+ const result = await worker.deploy();
139
+ assert.strictEqual(result.scriptName, "my-worker");
140
+ assert.deepStrictEqual(result.routes, ["api.example.com/*", "example.com/*"]);
141
+ // Verify multipart request body contains metadata with bindings
142
+ const uploadCall = fetchCalls.find(c => c.method === "PUT" && c.url.endsWith("/workers/scripts/my-worker"));
143
+ assert.ok(uploadCall);
144
+ assert.ok(uploadCall.body instanceof FormData);
145
+ const metadataStr = uploadCall.body.get("metadata");
146
+ assert.ok(typeof metadataStr === "string");
147
+ const metadata = JSON.parse(metadataStr);
148
+ assert.deepStrictEqual(metadata, {
149
+ main_module: "index.js",
150
+ bindings: [
151
+ { type: "kv_namespace", name: "MY_KV", namespace_id: "kv-id-999" },
152
+ { type: "r2_bucket", name: "MY_BUCKET", bucket_name: "my-bucket" },
153
+ { type: "plain_text", name: "ENV_VAR", text: "my-env-value" }
154
+ ]
155
+ });
156
+ // Verify route updates & creations
157
+ const putRouteCall = fetchCalls.find(c => c.method === "PUT" && c.url.includes("/workers/routes/"));
158
+ assert.ok(putRouteCall);
159
+ assert.ok(putRouteCall.url.endsWith("/zones/zone-123/workers/routes/route-existing"));
160
+ assert.deepStrictEqual(putRouteCall.body, {
161
+ pattern: "api.example.com/*",
162
+ script: "my-worker"
163
+ });
164
+ const postRouteCall = fetchCalls.find(c => c.method === "POST" && c.url.endsWith("/zones/zone-123/workers/routes"));
165
+ assert.ok(postRouteCall);
166
+ assert.deepStrictEqual(postRouteCall.body, {
167
+ pattern: "example.com/*",
168
+ script: "my-worker"
169
+ });
170
+ });
171
+ test("does not call fetch during dryRun deploy", async () => {
172
+ Config.set({
173
+ dryRun: true,
174
+ providers: {
175
+ cloudflare: { token: "fake-cf-token", accountId: "fake-cf-account" }
176
+ }
177
+ });
178
+ const worker = new WorkerBuilder("my-worker")
179
+ .script(tempScriptPath)
180
+ .route("example.com/*");
181
+ const result = await worker.deploy();
182
+ assert.strictEqual(result.scriptName, "my-worker");
183
+ const puts = fetchCalls.filter(c => c.method === "PUT");
184
+ assert.strictEqual(puts.length, 0);
185
+ });
186
+ test("destroys routes and script successfully", async () => {
187
+ // Mock zone resolution for route cleanup
188
+ mockResponses["GET /zones?name=example.com"] = {
189
+ status: 200,
190
+ body: { result: [{ id: "zone-123", name: "example.com" }] }
191
+ };
192
+ mockResponses["GET /zones/zone-123/workers/routes"] = {
193
+ status: 200,
194
+ body: {
195
+ result: [
196
+ { id: "route-existing", pattern: "example.com/*", script: "my-worker" }
197
+ ]
198
+ }
199
+ };
200
+ mockResponses["DELETE /zones/zone-123/workers/routes/route-existing"] = {
201
+ status: 200,
202
+ body: {}
203
+ };
204
+ mockResponses["DELETE /accounts/fake-cf-account/workers/scripts/my-worker"] = {
205
+ status: 200,
206
+ body: {}
207
+ };
208
+ const worker = new WorkerBuilder("my-worker")
209
+ .script(tempScriptPath)
210
+ .route("example.com/*");
211
+ const result = await worker.destroy();
212
+ assert.deepStrictEqual(result, { destroyed: "my-worker" });
213
+ const deleteRouteCall = fetchCalls.find(c => c.method === "DELETE" && c.url.includes("/workers/routes/"));
214
+ assert.ok(deleteRouteCall);
215
+ assert.ok(deleteRouteCall.url.endsWith("/zones/zone-123/workers/routes/route-existing"));
216
+ const deleteScriptCall = fetchCalls.find(c => c.method === "DELETE" && c.url.includes("/workers/scripts/"));
217
+ assert.ok(deleteScriptCall);
218
+ assert.ok(deleteScriptCall.url.endsWith("/accounts/fake-cf-account/workers/scripts/my-worker"));
219
+ });
220
+ });
@@ -0,0 +1,42 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { Output } from "../../core/output.js";
3
+ export interface CFDNSRecord {
4
+ type: "A" | "CNAME" | "TXT" | "MX" | "AAAA" | "SRV" | "CAA";
5
+ name: string;
6
+ value: string | BaseBuilder | Output<string>;
7
+ ttl?: number;
8
+ priority?: number;
9
+ port?: number;
10
+ weight?: number;
11
+ flags?: number;
12
+ tag?: string;
13
+ proxied?: boolean;
14
+ }
15
+ export declare class ZoneBuilder extends BaseBuilder {
16
+ domainName: string;
17
+ readonly out: {
18
+ id: Output<string>;
19
+ };
20
+ resolvedId: string | null;
21
+ private records;
22
+ constructor(domainName: string);
23
+ private discoverZone;
24
+ record(filePath: string): this;
25
+ record(name: string, type: CFDNSRecord["type"], value: string | BaseBuilder | Output<string>, ttl?: number, priority?: number, port?: number, weight?: number, flags?: number, tag?: string, proxied?: boolean): this;
26
+ pointer(name: string, target: BaseBuilder | Output<string> | string, proxied?: boolean): this;
27
+ cname(name: string, target: string, proxied?: boolean): this;
28
+ aaaa(name: string, target: string | Output<string>, proxied?: boolean): this;
29
+ txt(name: string, target: string): this;
30
+ mx(name: string, target: string, priority?: number): this;
31
+ srv(name: string, target: string, port: number, priority?: number, weight?: number): this;
32
+ caa(name: string, tag: string, target: string, flags?: number): this;
33
+ deploy(): Promise<{
34
+ zone: string;
35
+ zoneId: string | null;
36
+ }>;
37
+ destroy(): Promise<{
38
+ destroyed: boolean;
39
+ } | {
40
+ destroyed: string;
41
+ }>;
42
+ }