puls-dev 0.1.0 → 0.1.8

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 (50) hide show
  1. package/README.md +10 -8
  2. package/dist/core/checker.d.ts +1 -1
  3. package/dist/core/checker.js +88 -56
  4. package/dist/core/config.test.d.ts +1 -0
  5. package/dist/core/config.test.js +21 -0
  6. package/dist/core/decorators.js +8 -2
  7. package/dist/core/output.test.d.ts +1 -0
  8. package/dist/core/output.test.js +18 -0
  9. package/dist/core/resource.js +2 -2
  10. package/dist/core/stack.d.ts +1 -1
  11. package/dist/core/stack.js +2 -2
  12. package/dist/providers/aws/acm.d.ts +1 -1
  13. package/dist/providers/aws/acm.js +27 -23
  14. package/dist/providers/aws/api.d.ts +14 -14
  15. package/dist/providers/aws/api.js +21 -21
  16. package/dist/providers/aws/apigateway.d.ts +2 -2
  17. package/dist/providers/aws/apigateway.js +33 -29
  18. package/dist/providers/aws/cloudfront.d.ts +3 -3
  19. package/dist/providers/aws/cloudfront.js +49 -34
  20. package/dist/providers/aws/fargate.d.ts +2 -2
  21. package/dist/providers/aws/fargate.js +99 -52
  22. package/dist/providers/aws/lambda.d.ts +2 -2
  23. package/dist/providers/aws/lambda.js +63 -32
  24. package/dist/providers/aws/rds.d.ts +1 -1
  25. package/dist/providers/aws/rds.js +77 -39
  26. package/dist/providers/aws/route53.d.ts +5 -5
  27. package/dist/providers/aws/route53.js +42 -35
  28. package/dist/providers/aws/s3.d.ts +2 -2
  29. package/dist/providers/aws/s3.js +40 -33
  30. package/dist/providers/aws/secrets.js +15 -7
  31. package/dist/providers/aws/sqs.d.ts +1 -1
  32. package/dist/providers/aws/sqs.js +47 -23
  33. package/dist/providers/do/domain.d.ts +4 -4
  34. package/dist/providers/do/domain.js +15 -11
  35. package/dist/providers/firebase/auth.d.ts +1 -1
  36. package/dist/providers/firebase/auth.js +65 -33
  37. package/dist/providers/firebase/firestore.d.ts +2 -2
  38. package/dist/providers/firebase/firestore.js +45 -28
  39. package/dist/providers/firebase/functions.d.ts +1 -1
  40. package/dist/providers/firebase/functions.js +75 -42
  41. package/dist/providers/firebase/hosting.d.ts +1 -1
  42. package/dist/providers/firebase/hosting.js +92 -52
  43. package/dist/providers/firebase/remoteconfig.d.ts +1 -1
  44. package/dist/providers/firebase/remoteconfig.js +42 -33
  45. package/dist/providers/firebase/storage.d.ts +1 -1
  46. package/dist/providers/firebase/storage.js +38 -24
  47. package/dist/providers/proxmox/vm.d.ts +1 -1
  48. package/dist/providers/proxmox/vm.js +43 -24
  49. package/dist/types/aws.js +1 -1
  50. package/package.json +3 -2
@@ -1,25 +1,25 @@
1
- import { execSync } from 'node:child_process';
2
- import { mkdtempSync, readFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { BaseBuilder } from '../../core/resource.js';
6
- import { cloudFetch, getProjectId } from './api.js';
7
- const CF_BASE = 'https://cloudfunctions.googleapis.com/v2';
1
+ import { execSync } from "node:child_process";
2
+ import { mkdtempSync, readFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { BaseBuilder } from "../../core/resource.js";
6
+ import { cloudFetch, getProjectId } from "./api.js";
7
+ const CF_BASE = "https://cloudfunctions.googleapis.com/v2";
8
8
  export const FUNCTIONS_RUNTIME = {
9
- NODEJS_22: 'nodejs22',
10
- NODEJS_20: 'nodejs20',
11
- NODEJS_18: 'nodejs18',
12
- PYTHON_312: 'python312',
13
- PYTHON_311: 'python311',
14
- GO_122: 'go122',
15
- JAVA_21: 'java21',
9
+ NODEJS_22: "nodejs22",
10
+ NODEJS_20: "nodejs20",
11
+ NODEJS_18: "nodejs18",
12
+ PYTHON_312: "python312",
13
+ PYTHON_311: "python311",
14
+ GO_122: "go122",
15
+ JAVA_21: "java21",
16
16
  };
17
17
  export class FirebaseFunctionsBuilder extends BaseBuilder {
18
18
  _sourcePath;
19
- _entryPoint = 'handler';
19
+ _entryPoint = "handler";
20
20
  _runtime = FUNCTIONS_RUNTIME.NODEJS_20;
21
- _region = 'us-central1';
22
- _memory = '256M';
21
+ _region = "us-central1";
22
+ _memory = "256M";
23
23
  _timeout = 60;
24
24
  _maxInstances = 100;
25
25
  _minInstances = 0;
@@ -29,15 +29,42 @@ export class FirebaseFunctionsBuilder extends BaseBuilder {
29
29
  // Discovery runs lazily in deploy() because we need _region, which may not be set at construction time
30
30
  this.discoveryPromise = Promise.resolve(null);
31
31
  }
32
- source(path) { this._sourcePath = path; return this; }
33
- entryPoint(e) { this._entryPoint = e; return this; }
34
- runtime(r) { this._runtime = r; return this; }
35
- region(r) { this._region = r; return this; }
36
- memory(m) { this._memory = m; return this; }
37
- timeout(seconds) { this._timeout = seconds; return this; }
38
- maxInstances(n) { this._maxInstances = n; return this; }
39
- minInstances(n) { this._minInstances = n; return this; }
40
- env(vars) { this._env = vars; return this; }
32
+ source(path) {
33
+ this._sourcePath = path;
34
+ return this;
35
+ }
36
+ entryPoint(e) {
37
+ this._entryPoint = e;
38
+ return this;
39
+ }
40
+ runtime(r) {
41
+ this._runtime = r;
42
+ return this;
43
+ }
44
+ region(r) {
45
+ this._region = r;
46
+ return this;
47
+ }
48
+ memory(m) {
49
+ this._memory = m;
50
+ return this;
51
+ }
52
+ timeout(seconds) {
53
+ this._timeout = seconds;
54
+ return this;
55
+ }
56
+ maxInstances(n) {
57
+ this._maxInstances = n;
58
+ return this;
59
+ }
60
+ minInstances(n) {
61
+ this._minInstances = n;
62
+ return this;
63
+ }
64
+ env(vars) {
65
+ this._env = vars;
66
+ return this;
67
+ }
41
68
  fnPath() {
42
69
  return `/projects/${getProjectId()}/locations/${this._region}/functions/${this.name}`;
43
70
  }
@@ -46,7 +73,7 @@ export class FirebaseFunctionsBuilder extends BaseBuilder {
46
73
  return await cloudFetch(CF_BASE, this.fnPath());
47
74
  }
48
75
  catch (e) {
49
- if (e.message?.includes('404'))
76
+ if (e.message?.includes("404"))
50
77
  return null;
51
78
  throw e;
52
79
  }
@@ -54,17 +81,19 @@ export class FirebaseFunctionsBuilder extends BaseBuilder {
54
81
  zipSource() {
55
82
  if (!this._sourcePath)
56
83
  throw new Error(`[Firebase.Functions:${this.name}] .source() is required`);
57
- const tmp = mkdtempSync(join(tmpdir(), 'puls-fn-'));
58
- const zipPath = join(tmp, 'function.zip');
59
- execSync(`cd "${this._sourcePath}" && zip -r "${zipPath}" .`, { stdio: 'pipe' });
84
+ const tmp = mkdtempSync(join(tmpdir(), "puls-fn-"));
85
+ const zipPath = join(tmp, "function.zip");
86
+ execSync(`cd "${this._sourcePath}" && zip -r "${zipPath}" .`, {
87
+ stdio: "pipe",
88
+ });
60
89
  return readFileSync(zipPath);
61
90
  }
62
91
  async uploadSource() {
63
- const { uploadUrl, storageSource } = await cloudFetch(CF_BASE, `/projects/${getProjectId()}/locations/${this._region}/functions:generateUploadUrl`, { method: 'POST', body: '{}' });
92
+ const { uploadUrl, storageSource } = await cloudFetch(CF_BASE, `/projects/${getProjectId()}/locations/${this._region}/functions:generateUploadUrl`, { method: "POST", body: "{}" });
64
93
  const zipBuffer = this.zipSource();
65
94
  const res = await fetch(uploadUrl, {
66
- method: 'PUT',
67
- headers: { 'Content-Type': 'application/zip' },
95
+ method: "PUT",
96
+ headers: { "Content-Type": "application/zip" },
68
97
  body: new Uint8Array(zipBuffer),
69
98
  });
70
99
  if (!res.ok)
@@ -75,7 +104,7 @@ export class FirebaseFunctionsBuilder extends BaseBuilder {
75
104
  const start = Date.now();
76
105
  const timeout = 5 * 60 * 1000;
77
106
  while (Date.now() - start < timeout) {
78
- await new Promise(r => setTimeout(r, 4000));
107
+ await new Promise((r) => setTimeout(r, 4000));
79
108
  const op = await cloudFetch(CF_BASE, `/${operationName}`);
80
109
  if (op.done) {
81
110
  if (op.error)
@@ -94,7 +123,7 @@ export class FirebaseFunctionsBuilder extends BaseBuilder {
94
123
  if (dryRun) {
95
124
  const existing = await this.getExisting();
96
125
  if (existing) {
97
- console.log(` ✅ Function "${this.name}" exists (${existing.state ?? 'ACTIVE'})`);
126
+ console.log(` ✅ Function "${this.name}" exists (${existing.state ?? "ACTIVE"})`);
98
127
  console.log(` 📝 [PLAN] Update → ${this._runtime}, ${this._memory}, ${this._timeout}s timeout`);
99
128
  }
100
129
  else {
@@ -123,23 +152,27 @@ export class FirebaseFunctionsBuilder extends BaseBuilder {
123
152
  maxInstanceCount: this._maxInstances,
124
153
  minInstanceCount: this._minInstances,
125
154
  environmentVariables: this._env,
126
- ingressSettings: 'ALLOW_ALL',
155
+ ingressSettings: "ALLOW_ALL",
127
156
  },
128
157
  };
129
158
  const existing = await this.getExisting();
130
159
  let op;
131
160
  if (!existing) {
132
- op = await cloudFetch(CF_BASE, `/projects/${project}/locations/${this._region}/functions?functionId=${this.name}`, { method: 'POST', body: JSON.stringify(body) });
161
+ op = await cloudFetch(CF_BASE, `/projects/${project}/locations/${this._region}/functions?functionId=${this.name}`, { method: "POST", body: JSON.stringify(body) });
133
162
  console.log(` 🚀 Creating function...`);
134
163
  }
135
164
  else {
136
- const mask = 'buildConfig.source,buildConfig.runtime,buildConfig.entryPoint,buildConfig.environmentVariables,serviceConfig.availableMemory,serviceConfig.timeoutSeconds,serviceConfig.maxInstanceCount,serviceConfig.minInstanceCount,serviceConfig.environmentVariables';
137
- op = await cloudFetch(CF_BASE, `${this.fnPath()}?updateMask=${mask}`, { method: 'PATCH', body: JSON.stringify(body) });
165
+ const mask = "buildConfig.source,buildConfig.runtime,buildConfig.entryPoint,buildConfig.environmentVariables,serviceConfig.availableMemory,serviceConfig.timeoutSeconds,serviceConfig.maxInstanceCount,serviceConfig.minInstanceCount,serviceConfig.environmentVariables";
166
+ op = await cloudFetch(CF_BASE, `${this.fnPath()}?updateMask=${mask}`, {
167
+ method: "PATCH",
168
+ body: JSON.stringify(body),
169
+ });
138
170
  console.log(` 🔄 Updating function...`);
139
171
  }
140
172
  await this.pollOperation(op.name);
141
173
  const fn = await this.getExisting();
142
- const url = fn?.serviceConfig?.uri ?? `https://${this._region}-${project}.cloudfunctions.net/${this.name}`;
174
+ const url = fn?.serviceConfig?.uri ??
175
+ `https://${this._region}-${project}.cloudfunctions.net/${this.name}`;
143
176
  console.log(` ✅ Function live → ${url}`);
144
177
  return { name: this.name, url, project, region: this._region };
145
178
  }
@@ -148,14 +181,14 @@ export class FirebaseFunctionsBuilder extends BaseBuilder {
148
181
  console.log(`\n🗑️ Destroying Firebase Function "${this.name}"...`);
149
182
  const existing = await this.getExisting();
150
183
  if (!existing) {
151
- console.log(` ✅ Function "${this.name}" does not exist nothing to do`);
184
+ console.log(` ✅ Function "${this.name}" does not exist - nothing to do`);
152
185
  return { destroyed: this.name };
153
186
  }
154
187
  if (dryRun) {
155
188
  console.log(` 📝 [PLAN] Delete function "${this.name}" in ${this._region}`);
156
189
  return { destroyed: this.name };
157
190
  }
158
- const op = await cloudFetch(CF_BASE, this.fnPath(), { method: 'DELETE' });
191
+ const op = await cloudFetch(CF_BASE, this.fnPath(), { method: "DELETE" });
159
192
  await this.pollOperation(op.name);
160
193
  console.log(` ✅ Function "${this.name}" deleted`);
161
194
  return { destroyed: this.name };
@@ -1,4 +1,4 @@
1
- import { BaseBuilder } from '../../core/resource.js';
1
+ import { BaseBuilder } from "../../core/resource.js";
2
2
  export declare class FirebaseHostingBuilder extends BaseBuilder {
3
3
  private _sourcePath?;
4
4
  constructor(siteId: string);
@@ -1,22 +1,23 @@
1
- import { createHash } from 'node:crypto';
2
- import { readdirSync, statSync, readFileSync } from 'node:fs';
3
- import { join, relative, extname } from 'node:path';
4
- import { BaseBuilder } from '../../core/resource.js';
5
- import { getProjectId, hostingFetch, getFirebaseToken } from './api.js';
1
+ import { createHash } from "node:crypto";
2
+ import { readdirSync, statSync, readFileSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { gzipSync } from "node:zlib";
5
+ import { BaseBuilder } from "../../core/resource.js";
6
+ import { getProjectId, hostingFetch, getFirebaseToken } from "./api.js";
6
7
  const CONTENT_TYPES = {
7
- '.html': 'text/html',
8
- '.css': 'text/css',
9
- '.js': 'application/javascript',
10
- '.json': 'application/json',
11
- '.png': 'image/png',
12
- '.jpg': 'image/jpeg',
13
- '.svg': 'image/svg+xml',
14
- '.ico': 'image/x-icon',
15
- '.woff2': 'font/woff2',
16
- '.woff': 'font/woff',
17
- '.ttf': 'font/ttf',
18
- '.txt': 'text/plain',
19
- '.xml': 'application/xml',
8
+ ".html": "text/html",
9
+ ".css": "text/css",
10
+ ".js": "application/javascript",
11
+ ".json": "application/json",
12
+ ".png": "image/png",
13
+ ".jpg": "image/jpeg",
14
+ ".svg": "image/svg+xml",
15
+ ".ico": "image/x-icon",
16
+ ".woff2": "font/woff2",
17
+ ".woff": "font/woff",
18
+ ".ttf": "font/ttf",
19
+ ".txt": "text/plain",
20
+ ".xml": "application/xml",
20
21
  };
21
22
  function walkDir(dir) {
22
23
  const results = [];
@@ -33,7 +34,7 @@ function walkDir(dir) {
33
34
  }
34
35
  function sha256(filePath) {
35
36
  const content = readFileSync(filePath);
36
- return createHash('sha256').update(content).digest('hex');
37
+ return createHash("sha256").update(content).digest("hex");
37
38
  }
38
39
  export class FirebaseHostingBuilder extends BaseBuilder {
39
40
  _sourcePath;
@@ -42,17 +43,21 @@ export class FirebaseHostingBuilder extends BaseBuilder {
42
43
  // Discovery: check if the site has any releases (confirms it exists and is active)
43
44
  this.discoveryPromise = this.discoverSite(siteId);
44
45
  }
45
- source(path) { this._sourcePath = path; return this; }
46
+ source(path) {
47
+ this._sourcePath = path;
48
+ return this;
49
+ }
46
50
  async discoverSite(siteId) {
47
51
  try {
48
- const result = await hostingFetch(`/sites/${siteId}/releases?pageSize=1`);
49
- return result ?? null;
52
+ const projectId = getProjectId();
53
+ const result = await hostingFetch(`/projects/${projectId}/sites/${siteId}`);
54
+ return result;
50
55
  }
51
56
  catch (e) {
52
- // Site doesn't exist yet or credentials not set yet not an error at construction time
53
- if (e.message?.includes('403') || e.message?.includes('404'))
57
+ // Site doesn't exist yet or credentials not set yet - not an error at construction time
58
+ if (e.message?.includes("403") || e.message?.includes("404"))
54
59
  return null;
55
- if (e.message?.includes('Firebase not configured'))
60
+ if (e.message?.includes("Firebase not configured"))
56
61
  return null;
57
62
  throw e;
58
63
  }
@@ -61,14 +66,18 @@ export class FirebaseHostingBuilder extends BaseBuilder {
61
66
  const dryRun = this.isDryRunActive();
62
67
  const projectId = getProjectId();
63
68
  const siteId = this.name;
69
+ const parent = `projects/${projectId}/sites/${siteId}`;
64
70
  console.log(`\n⚡ Finalizing Firebase Hosting "${siteId}"...`);
65
71
  if (!this._sourcePath)
66
72
  throw new Error(`[Firebase.Hosting:${siteId}] .source("./dist") is required`);
67
73
  const files = walkDir(this._sourcePath);
68
74
  if (files.length === 0)
69
75
  throw new Error(`[Firebase.Hosting:${siteId}] No files found in "${this._sourcePath}"`);
76
+ const existing = await this.discoveryPromise;
70
77
  if (dryRun) {
71
78
  console.log(` 📝 [PLAN] Deploy ${files.length} file(s) to https://${siteId}.web.app`);
79
+ if (!existing)
80
+ console.log(` └─ Will create secondary site "${siteId}"`);
72
81
  for (const f of files.slice(0, 5)) {
73
82
  console.log(` └─ /${relative(this._sourcePath, f)}`);
74
83
  }
@@ -76,52 +85,83 @@ export class FirebaseHostingBuilder extends BaseBuilder {
76
85
  console.log(` └─ ... and ${files.length - 5} more`);
77
86
  return { siteId, url: `https://${siteId}.web.app` };
78
87
  }
88
+ // 0. Ensure site exists (for secondary sites)
89
+ if (!existing) {
90
+ console.log(` 🆕 Creating secondary site "${siteId}"...`);
91
+ try {
92
+ await hostingFetch(`/projects/${projectId}/sites?siteId=${siteId}`, {
93
+ method: "POST",
94
+ body: JSON.stringify({}), // Defaults to USER_SITE
95
+ });
96
+ }
97
+ catch (e) {
98
+ if (!e.message.includes("409"))
99
+ throw e;
100
+ }
101
+ }
79
102
  // 1. Create a new version
80
- const version = await hostingFetch(`/sites/${siteId}/versions`, {
81
- method: 'POST',
82
- body: JSON.stringify({ config: { headers: [{ glob: '**', headers: { 'Cache-Control': 'max-age=3600' } }] } }),
103
+ const version = await hostingFetch(`/${parent}/versions`, {
104
+ method: "POST",
105
+ body: JSON.stringify({
106
+ config: {
107
+ headers: [
108
+ { glob: "**", headers: { "Cache-Control": "max-age=3600" } },
109
+ ],
110
+ },
111
+ }),
83
112
  });
84
- const versionId = version.name.split('/').pop();
113
+ const versionId = version.name.split("/").pop();
85
114
  console.log(` 📦 Version created: ${versionId}`);
86
- // 2. Build file hash map keys are URL paths, values are SHA256 hashes
115
+ // 2. Build file hash map - keys are URL paths, values are SHA256 hashes
116
+ // NOTE: Firebase Hosting requires gzipped content and the hash must match the gzipped bytes.
87
117
  const fileMap = {};
88
- const absoluteToUrl = {};
118
+ const absoluteToCompressed = {};
119
+ const hashToUrl = {};
89
120
  for (const absPath of files) {
90
- const urlPath = '/' + relative(this._sourcePath, absPath).replace(/\\/g, '/');
91
- const hash = sha256(absPath);
121
+ const urlPath = "/" + relative(this._sourcePath, absPath).replace(/\\/g, "/");
122
+ const content = readFileSync(absPath);
123
+ const compressed = gzipSync(content);
124
+ const hash = createHash("sha256").update(compressed).digest("hex");
92
125
  fileMap[urlPath] = hash;
93
- absoluteToUrl[hash] = absPath;
126
+ absoluteToCompressed[absPath] = compressed;
127
+ hashToUrl[hash] = absPath;
94
128
  }
95
- // 3. Populate files API tells us which hashes need uploading
96
- const populate = await hostingFetch(`/sites/${siteId}/versions/${versionId}/populateFiles`, {
97
- method: 'POST',
129
+ // 3. Populate files - API tells us which hashes need uploading
130
+ const populate = await hostingFetch(`/${parent}/versions/${versionId}:populateFiles`, {
131
+ method: "POST",
98
132
  body: JSON.stringify({ files: fileMap }),
99
133
  });
100
134
  const uploadUrl = populate.uploadUrl;
101
135
  const required = populate.uploadRequiredHashes ?? [];
102
136
  console.log(` 📤 Uploading ${required.length} file(s) (${files.length - required.length} cached)`);
103
137
  // 4. Upload each required file
104
- const token = await getFirebaseToken(['https://www.googleapis.com/auth/firebase.hosting']);
138
+ const token = await getFirebaseToken([
139
+ "https://www.googleapis.com/auth/firebase.hosting",
140
+ ]);
105
141
  for (const hash of required) {
106
- const absPath = absoluteToUrl[hash];
107
- const content = readFileSync(absPath);
108
- const contentType = CONTENT_TYPES[extname(absPath).toLowerCase()] ?? 'application/octet-stream';
142
+ const absPath = hashToUrl[hash];
143
+ const compressed = absoluteToCompressed[absPath];
109
144
  const res = await fetch(`${uploadUrl}/${hash}`, {
110
- method: 'POST',
111
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': contentType },
112
- body: content,
145
+ method: "POST",
146
+ headers: {
147
+ Authorization: `Bearer ${token}`,
148
+ "Content-Type": "application/octet-stream",
149
+ },
150
+ body: compressed,
113
151
  });
114
- if (!res.ok)
115
- throw new Error(`Upload failed for ${absPath}: ${res.status}`);
152
+ if (!res.ok) {
153
+ const errorBody = await res.text();
154
+ throw new Error(`Upload failed for ${absPath}: ${res.status} - ${errorBody}`);
155
+ }
116
156
  }
117
157
  // 5. Finalize the version
118
- await hostingFetch(`/sites/${siteId}/versions/${versionId}`, {
119
- method: 'PATCH',
120
- body: JSON.stringify({ status: 'FINALIZED' }),
158
+ await hostingFetch(`/${parent}/versions/${versionId}`, {
159
+ method: "PATCH",
160
+ body: JSON.stringify({ status: "FINALIZED" }),
121
161
  });
122
162
  // 6. Create a release
123
- await hostingFetch(`/sites/${siteId}/releases?versionName=sites/${siteId}/versions/${versionId}`, {
124
- method: 'POST',
163
+ await hostingFetch(`/${parent}/releases?versionName=${parent}/versions/${versionId}`, {
164
+ method: "POST",
125
165
  body: JSON.stringify({}),
126
166
  });
127
167
  const url = `https://${siteId}.web.app`;
@@ -135,7 +175,7 @@ export class FirebaseHostingBuilder extends BaseBuilder {
135
175
  console.log(` 📝 [PLAN] Delete all releases for site "${this.name}"`);
136
176
  return { destroyed: this.name };
137
177
  }
138
- // Firebase doesn't support deleting the default site we can only roll back releases.
178
+ // Firebase doesn't support deleting the default site - we can only roll back releases.
139
179
  // For non-default sites, the Sites API supports deletion (requires Blaze plan).
140
180
  console.log(` ℹ️ Firebase Hosting sites cannot be deleted via API (default site is permanent).`);
141
181
  console.log(` To unpublish, go to the Firebase console and disable Hosting.`);
@@ -1,4 +1,4 @@
1
- import { BaseBuilder } from '../../core/resource.js';
1
+ import { BaseBuilder } from "../../core/resource.js";
2
2
  type ParamValue = string | number | boolean | object;
3
3
  export declare class FirebaseRemoteConfigBuilder extends BaseBuilder {
4
4
  private _params;
@@ -1,43 +1,43 @@
1
- import { BaseBuilder } from '../../core/resource.js';
2
- import { getFirebaseToken, getProjectId } from './api.js';
3
- const RC_BASE = 'https://firebaseremoteconfig.googleapis.com/v1';
4
- const RC_SCOPE = 'https://www.googleapis.com/auth/firebase.remoteconfig';
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { getFirebaseToken, getProjectId } from "./api.js";
3
+ const RC_BASE = "https://firebaseremoteconfig.googleapis.com/v1";
4
+ const RC_SCOPE = "https://www.googleapis.com/auth/firebase.remoteconfig";
5
5
  function inferType(v) {
6
- if (typeof v === 'boolean')
7
- return 'BOOLEAN';
8
- if (typeof v === 'number')
9
- return 'NUMBER';
10
- if (typeof v === 'object')
11
- return 'JSON';
12
- return 'STRING';
6
+ if (typeof v === "boolean")
7
+ return "BOOLEAN";
8
+ if (typeof v === "number")
9
+ return "NUMBER";
10
+ if (typeof v === "object")
11
+ return "JSON";
12
+ return "STRING";
13
13
  }
14
14
  function serialize(v) {
15
- return typeof v === 'object' ? JSON.stringify(v) : String(v);
15
+ return typeof v === "object" ? JSON.stringify(v) : String(v);
16
16
  }
17
17
  export class FirebaseRemoteConfigBuilder extends BaseBuilder {
18
18
  _params = [];
19
19
  _conditions = [];
20
20
  constructor() {
21
- super('remoteconfig');
21
+ super("remoteconfig");
22
22
  this.discoveryPromise = Promise.resolve(null);
23
23
  }
24
24
  string(key, value, description) {
25
- this._params.push({ key, value, type: 'STRING', description });
25
+ this._params.push({ key, value, type: "STRING", description });
26
26
  return this;
27
27
  }
28
28
  bool(key, value, description) {
29
- this._params.push({ key, value, type: 'BOOLEAN', description });
29
+ this._params.push({ key, value, type: "BOOLEAN", description });
30
30
  return this;
31
31
  }
32
32
  number(key, value, description) {
33
- this._params.push({ key, value, type: 'NUMBER', description });
33
+ this._params.push({ key, value, type: "NUMBER", description });
34
34
  return this;
35
35
  }
36
36
  json(key, value, description) {
37
- this._params.push({ key, value, type: 'JSON', description });
37
+ this._params.push({ key, value, type: "JSON", description });
38
38
  return this;
39
39
  }
40
- // Generic param type inferred from value
40
+ // Generic param - type inferred from value
41
41
  param(key, value, description) {
42
42
  this._params.push({ key, value, type: inferType(value), description });
43
43
  return this;
@@ -48,9 +48,9 @@ export class FirebaseRemoteConfigBuilder extends BaseBuilder {
48
48
  }
49
49
  // Override a param's value for a specific condition
50
50
  override(paramKey, conditionName, value) {
51
- const p = this._params.find(p => p.key === paramKey);
51
+ const p = this._params.find((p) => p.key === paramKey);
52
52
  if (!p)
53
- throw new Error(`[RemoteConfig] param "${paramKey}" not defined call .param()/.bool()/etc. first`);
53
+ throw new Error(`[RemoteConfig] param "${paramKey}" not defined - call .param()/.bool()/etc. first`);
54
54
  p.conditionalValues ??= {};
55
55
  p.conditionalValues[conditionName] = value;
56
56
  return this;
@@ -59,18 +59,21 @@ export class FirebaseRemoteConfigBuilder extends BaseBuilder {
59
59
  async rcFetch(path, opts = {}) {
60
60
  const token = await getFirebaseToken([RC_SCOPE]);
61
61
  const headers = {
62
- 'Authorization': `Bearer ${token}`,
63
- 'Content-Type': 'application/json',
62
+ Authorization: `Bearer ${token}`,
63
+ "Content-Type": "application/json",
64
64
  };
65
65
  if (opts.etag)
66
- headers['If-Match'] = opts.etag;
66
+ headers["If-Match"] = opts.etag;
67
67
  const res = await fetch(`${RC_BASE}${path}`, { ...opts, headers });
68
68
  if (!res.ok) {
69
69
  const body = await res.text();
70
- throw new Error(`RemoteConfig API ${opts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
70
+ throw new Error(`RemoteConfig API ${opts.method ?? "GET"} ${path} → ${res.status}: ${body}`);
71
71
  }
72
72
  const text = await res.text();
73
- return { data: text ? JSON.parse(text) : null, etag: res.headers.get('etag') };
73
+ return {
74
+ data: text ? JSON.parse(text) : null,
75
+ etag: res.headers.get("etag"),
76
+ };
74
77
  }
75
78
  // ── Deploy ────────────────────────────────────────────────────────────────
76
79
  async deploy() {
@@ -80,7 +83,9 @@ export class FirebaseRemoteConfigBuilder extends BaseBuilder {
80
83
  if (dryRun) {
81
84
  console.log(` 📝 [PLAN] Set ${this._params.length} parameter(s):`);
82
85
  for (const p of this._params) {
83
- const overrides = p.conditionalValues ? ` + ${Object.keys(p.conditionalValues).length} override(s)` : '';
86
+ const overrides = p.conditionalValues
87
+ ? ` + ${Object.keys(p.conditionalValues).length} override(s)`
88
+ : "";
84
89
  console.log(` └─ ${p.key} (${p.type}): ${serialize(p.value)}${overrides}`);
85
90
  }
86
91
  if (this._conditions.length) {
@@ -92,7 +97,7 @@ export class FirebaseRemoteConfigBuilder extends BaseBuilder {
92
97
  }
93
98
  // Fetch current template + ETag
94
99
  const { data: current, etag } = await this.rcFetch(`/projects/${project}/remoteConfig`);
95
- // Merge our params into the existing template (non-destructive preserves params we didn't define)
100
+ // Merge our params into the existing template (non-destructive - preserves params we didn't define)
96
101
  const parameters = { ...(current?.parameters ?? {}) };
97
102
  for (const p of this._params) {
98
103
  const entry = {
@@ -114,13 +119,17 @@ export class FirebaseRemoteConfigBuilder extends BaseBuilder {
114
119
  const existingNames = new Set(existingConditions.map((c) => c.name));
115
120
  const conditions = [
116
121
  ...existingConditions,
117
- ...this._conditions.filter(c => !existingNames.has(c.name)),
122
+ ...this._conditions.filter((c) => !existingNames.has(c.name)),
118
123
  ];
119
- const body = { parameters, conditions, parameterGroups: current?.parameterGroups ?? {} };
124
+ const body = {
125
+ parameters,
126
+ conditions,
127
+ parameterGroups: current?.parameterGroups ?? {},
128
+ };
120
129
  await this.rcFetch(`/projects/${project}/remoteConfig`, {
121
- method: 'PUT',
130
+ method: "PUT",
122
131
  body: JSON.stringify(body),
123
- etag: etag ?? '*',
132
+ etag: etag ?? "*",
124
133
  });
125
134
  console.log(` ✅ ${this._params.length} parameter(s) published`);
126
135
  if (this._conditions.length)
@@ -129,7 +138,7 @@ export class FirebaseRemoteConfigBuilder extends BaseBuilder {
129
138
  }
130
139
  async destroy() {
131
140
  console.log(`\n🗑️ Destroying Firebase RemoteConfig...`);
132
- console.log(` ℹ️ RemoteConfig templates cannot be deleted via API clear parameters in the Firebase console`);
133
- return { destroyed: 'remoteconfig' };
141
+ console.log(` ℹ️ RemoteConfig templates cannot be deleted via API - clear parameters in the Firebase console`);
142
+ return { destroyed: "remoteconfig" };
134
143
  }
135
144
  }
@@ -1,4 +1,4 @@
1
- import { BaseBuilder } from '../../core/resource.js';
1
+ import { BaseBuilder } from "../../core/resource.js";
2
2
  interface CorsRule {
3
3
  origin: string[];
4
4
  method: string[];