create-shopify-firebase-app 2.0.6 → 2.1.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.
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Preflight — environment doctor
3
+ *
4
+ * Runs FIRST, before any scaffolding, provisioning or network work, so the
5
+ * user never discovers a missing tool six minutes into a long flow.
6
+ *
7
+ * Modelled on `flutter doctor`: check everything at once, report it in one
8
+ * compact aligned block, fix what can be fixed, and be explicit about what is
9
+ * merely degraded versus what actually blocks the run.
10
+ *
11
+ * Checks:
12
+ * - Node.js >= 20 (fatal — templates target Node 22)
13
+ * - npm (fatal — installs everything else)
14
+ * - git (warn — only used for `git init`)
15
+ * - firebase-tools (fatal — offers to install)
16
+ * - @shopify/cli (fatal — offers to install)
17
+ * - gcloud (warn — API enablement / billing link)
18
+ *
19
+ * The five CLI probes are slow subprocess calls, so they run in parallel.
20
+ */
21
+
22
+ import { execSync, spawn } from "node:child_process";
23
+ import prompts from "prompts";
24
+
25
+ // ─── ANSI helpers ─────────────────────────────────────────────────────
26
+ const c = {
27
+ reset: "\x1b[0m",
28
+ bold: "\x1b[1m",
29
+ dim: "\x1b[2m",
30
+ green: "\x1b[32m",
31
+ cyan: "\x1b[36m",
32
+ yellow: "\x1b[33m",
33
+ red: "\x1b[31m",
34
+ };
35
+
36
+ const ok = (msg) => console.log(` ${c.green}✔${c.reset} ${msg}`);
37
+ const warn = (msg) => console.log(` ${c.yellow}⚠${c.reset} ${msg}`);
38
+ const info = (msg) => console.log(` ${c.cyan}ℹ${c.reset} ${msg}`);
39
+ const fail = (msg) => console.log(` ${c.red}✘${c.reset} ${msg}`);
40
+ const section = (title) => {
41
+ console.log();
42
+ console.log(` ${c.cyan}===${c.reset} ${c.bold}${title}${c.reset} ${c.cyan}===${c.reset}`);
43
+ console.log();
44
+ };
45
+
46
+ // ─── Requirements ─────────────────────────────────────────────────────
47
+
48
+ /** Cloud Functions templates target the Node 22 runtime; the CLI needs modern ESM. */
49
+ const MIN_NODE_MAJOR = 20;
50
+
51
+ /** Width of the name column in the report — keeps every result line aligned. */
52
+ const LABEL_WIDTH = 14;
53
+
54
+ // ─── Shell helpers ────────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Run a probe command without ever throwing.
58
+ *
59
+ * shell:true is required on Windows, where npm/firebase/shopify are .cmd
60
+ * shims that spawn() cannot execute directly (ENOENT).
61
+ *
62
+ * @returns {Promise<{ ok: boolean, stdout: string, stderr: string, error?: string }>}
63
+ */
64
+ function probe(cmd, timeout = 30000) {
65
+ return new Promise((resolve) => {
66
+ let stdout = "";
67
+ let stderr = "";
68
+ let child = null;
69
+ let settled = false;
70
+
71
+ const finish = (result) => {
72
+ if (settled) return;
73
+ settled = true;
74
+ clearTimeout(timer);
75
+ try { child?.kill(); } catch {}
76
+ resolve(result);
77
+ };
78
+
79
+ const timer = setTimeout(
80
+ () => finish({ ok: false, stdout, stderr, error: `timed out after ${Math.round(timeout / 1000)}s` }),
81
+ timeout,
82
+ );
83
+
84
+ try {
85
+ child = spawn(cmd, {
86
+ shell: true,
87
+ stdio: ["ignore", "pipe", "pipe"],
88
+ windowsHide: true,
89
+ });
90
+ } catch (e) {
91
+ finish({ ok: false, stdout, stderr, error: e.message });
92
+ return;
93
+ }
94
+
95
+ child.stdout?.on("data", (d) => (stdout += d.toString()));
96
+ child.stderr?.on("data", (d) => (stderr += d.toString()));
97
+ child.on("error", (e) => finish({ ok: false, stdout, stderr, error: e.message }));
98
+ child.on("close", (code) => finish({ ok: code === 0, stdout, stderr }));
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Pull a version out of CLI output.
104
+ *
105
+ * Prefers a line that is *only* a version ("13.35.1") so update notices and
106
+ * banners cannot win; falls back to the first version-shaped token, which is
107
+ * what "git version 2.47.0" and "Google Cloud SDK 542.0.0" need.
108
+ */
109
+ function parseVersion(text) {
110
+ const lines = String(text || "").split("\n");
111
+ for (const line of lines) {
112
+ const bare = line.trim().match(/^v?(\d+\.\d+(?:\.\d+)?)/);
113
+ if (bare) return bare[1];
114
+ }
115
+ const any = String(text || "").match(/\d+\.\d+(?:\.\d+)?/);
116
+ return any ? any[0] : null;
117
+ }
118
+
119
+ /** Install a global npm package, showing npm's own progress. Never throws. */
120
+ function installGlobal(pkg) {
121
+ try {
122
+ execSync(`npm install -g ${pkg}`, { stdio: "inherit", timeout: 300000 });
123
+ return { ok: true };
124
+ } catch (e) {
125
+ return { ok: false, error: (e?.message || "").toString().trim() };
126
+ }
127
+ }
128
+
129
+ // ─── Individual checks ────────────────────────────────────────────────
130
+ // Every check resolves to { name, ok, fatal, version, message }.
131
+ // `fatal` means "this failure blocks the run", so it is always false when
132
+ // ok === true, and false for the optional tools (git, gcloud) even when they
133
+ // are missing.
134
+
135
+ /** Node.js — read from the running process, no subprocess needed. */
136
+ function checkNode() {
137
+ try {
138
+ const version = process.versions?.node || String(process.version || "").replace(/^v/, "");
139
+ const major = parseInt(String(version).split(".")[0], 10);
140
+
141
+ if (!Number.isFinite(major)) {
142
+ return {
143
+ name: "Node.js",
144
+ ok: false,
145
+ fatal: true,
146
+ version: null,
147
+ message: "could not determine the running Node version",
148
+ };
149
+ }
150
+ if (major < MIN_NODE_MAJOR) {
151
+ return {
152
+ name: "Node.js",
153
+ ok: false,
154
+ fatal: true,
155
+ version,
156
+ message: `v${version} — Node ${MIN_NODE_MAJOR}+ required (templates target the Node 22 runtime)`,
157
+ };
158
+ }
159
+ return { name: "Node.js", ok: true, fatal: false, version, message: `v${version}` };
160
+ } catch (e) {
161
+ return { name: "Node.js", ok: false, fatal: true, version: null, message: e?.message || "check failed" };
162
+ }
163
+ }
164
+
165
+ async function checkNpm() {
166
+ const r = await probe("npm --version");
167
+ const version = r.ok ? parseVersion(r.stdout) : null;
168
+ if (r.ok) return { name: "npm", ok: true, fatal: false, version, message: version || "installed" };
169
+ return {
170
+ name: "npm",
171
+ ok: false,
172
+ fatal: true,
173
+ version: null,
174
+ message: "not found — npm ships with Node.js and is needed to install everything else",
175
+ };
176
+ }
177
+
178
+ async function checkGit() {
179
+ const r = await probe("git --version");
180
+ const version = r.ok ? parseVersion(r.stdout) : null;
181
+ if (r.ok) return { name: "git", ok: true, fatal: false, version, message: version || "installed" };
182
+ return {
183
+ name: "git",
184
+ ok: false,
185
+ fatal: false,
186
+ version: null,
187
+ message: "not found — the project will be created without 'git init' (non-blocking)",
188
+ };
189
+ }
190
+
191
+ async function checkFirebase() {
192
+ const r = await probe("firebase --version", 60000);
193
+ const version = r.ok ? parseVersion(r.stdout) : null;
194
+ if (r.ok) return { name: "Firebase CLI", ok: true, fatal: false, version, message: version || "installed" };
195
+ return {
196
+ name: "Firebase CLI",
197
+ ok: false,
198
+ fatal: true,
199
+ version: null,
200
+ message: "not found — required to create the project, Firestore and Hosting",
201
+ };
202
+ }
203
+
204
+ async function checkShopify() {
205
+ // `shopify version` is slow the first time it runs — give it room.
206
+ const r = await probe("shopify version", 90000);
207
+ const version = r.ok ? parseVersion(r.stdout) : null;
208
+ if (r.ok) return { name: "Shopify CLI", ok: true, fatal: false, version, message: version || "installed" };
209
+ return {
210
+ name: "Shopify CLI",
211
+ ok: false,
212
+ fatal: true,
213
+ version: null,
214
+ message: "not found — required to create and link the Shopify app",
215
+ };
216
+ }
217
+
218
+ async function checkGcloud() {
219
+ const r = await probe("gcloud --version", 60000);
220
+ const version = r.ok ? parseVersion(r.stdout) : null;
221
+ if (r.ok) return { name: "gcloud", ok: true, fatal: false, version, message: version || "installed" };
222
+ return {
223
+ name: "gcloud",
224
+ ok: false,
225
+ fatal: false,
226
+ version: null,
227
+ message: "not found — billing and API enablement will need manual steps",
228
+ };
229
+ }
230
+
231
+ // ─── The two CLIs preflight can install for you ───────────────────────
232
+ const INSTALLABLE = [
233
+ { name: "Firebase CLI", pkg: "firebase-tools", recheck: checkFirebase },
234
+ { name: "Shopify CLI", pkg: "@shopify/cli", recheck: checkShopify },
235
+ ];
236
+
237
+ // ─── Reporting ────────────────────────────────────────────────────────
238
+
239
+ /** One compact aligned line: `✔ Node.js v22.20.0`. */
240
+ function line(check) {
241
+ const label = String(check.name).padEnd(LABEL_WIDTH);
242
+ const detail = check.ok
243
+ ? `${c.cyan}${check.message}${c.reset}`
244
+ : check.message;
245
+ return `${label} ${detail}`;
246
+ }
247
+
248
+ function print(check) {
249
+ if (check.ok) ok(line(check));
250
+ else if (check.fatal) fail(line(check));
251
+ else warn(line(check));
252
+ }
253
+
254
+ // ─── Main ─────────────────────────────────────────────────────────────
255
+
256
+ /**
257
+ * Verify the local toolchain before anything else runs.
258
+ *
259
+ * @param {object} options
260
+ * @param {boolean} [options.autoInstall] Install missing firebase/shopify CLIs without asking.
261
+ * @param {boolean} [options.nonInteractive] Never prompt — report only.
262
+ * @returns {Promise<{ ok: boolean, checks: Array<{ name: string, ok: boolean, fatal: boolean, version: string|null, message: string }> }>}
263
+ */
264
+ export async function preflight(options = {}) {
265
+ const isCI = !!options.nonInteractive;
266
+ let checks = [];
267
+
268
+ try {
269
+ section("Environment Check");
270
+
271
+ // ── 1. Probe everything at once ─────────────────────────────
272
+ // These are slow subprocess calls (the Shopify CLI alone can take
273
+ // seconds), so they must not run one after another.
274
+ const node = checkNode();
275
+ const [npm, git, firebase, shopify, gcloud] = await Promise.all([
276
+ checkNpm(),
277
+ checkGit(),
278
+ checkFirebase(),
279
+ checkShopify(),
280
+ checkGcloud(),
281
+ ]);
282
+
283
+ checks = [node, npm, git, firebase, shopify, gcloud];
284
+ for (const check of checks) print(check);
285
+
286
+ // ── 2. Fix what can be fixed ────────────────────────────────
287
+ for (const spec of INSTALLABLE) {
288
+ const idx = checks.findIndex((ch) => ch.name === spec.name);
289
+ if (idx < 0 || checks[idx].ok) continue;
290
+
291
+ console.log();
292
+
293
+ let shouldInstall = false;
294
+ if (isCI) {
295
+ info(`Install it with: ${c.dim}npm install -g ${spec.pkg}${c.reset}`);
296
+ } else if (options.autoInstall) {
297
+ shouldInstall = true;
298
+ } else {
299
+ const answer = await prompts({
300
+ type: "confirm",
301
+ name: "shouldInstall",
302
+ message: `Install ${spec.name} now (npm install -g ${spec.pkg})?`,
303
+ initial: true,
304
+ });
305
+ shouldInstall = !!answer.shouldInstall;
306
+ }
307
+
308
+ if (!shouldInstall) {
309
+ if (!isCI) info(`Skipped — install later: ${c.dim}npm install -g ${spec.pkg}${c.reset}`);
310
+ continue;
311
+ }
312
+
313
+ info(`Installing ${spec.name}...`);
314
+ const result = installGlobal(spec.pkg);
315
+
316
+ if (result.ok) {
317
+ checks[idx] = await spec.recheck();
318
+ print(checks[idx]);
319
+ } else {
320
+ warn(`Could not install ${spec.pkg} automatically`);
321
+ info(`Install manually: ${c.dim}npm install -g ${spec.pkg}${c.reset}`);
322
+ if (result.error) info(`${c.dim}${result.error.split("\n")[0]}${c.reset}`);
323
+ }
324
+ }
325
+
326
+ // ── 3. Verdict ──────────────────────────────────────────────
327
+ const blockers = checks.filter((ch) => !ch.ok && ch.fatal);
328
+ const degraded = checks.filter((ch) => !ch.ok && !ch.fatal);
329
+
330
+ console.log();
331
+ if (blockers.length === 0) {
332
+ if (degraded.length > 0) {
333
+ ok(`Environment ready ${c.dim}(${degraded.length} optional tool${degraded.length > 1 ? "s" : ""} missing)${c.reset}`);
334
+ } else {
335
+ ok("Environment ready");
336
+ }
337
+ } else {
338
+ fail(`Cannot continue — ${blockers.length} required tool${blockers.length > 1 ? "s" : ""} missing or out of date`);
339
+ for (const b of blockers) info(`${b.name}: ${b.message}`);
340
+ if (blockers.some((b) => b.name === "Node.js")) {
341
+ info(`Upgrade Node: ${c.cyan}https://nodejs.org${c.reset}`);
342
+ }
343
+ }
344
+
345
+ return { ok: blockers.length === 0, checks };
346
+ } catch (e) {
347
+ // Preflight must never be the thing that crashes the CLI.
348
+ fail(`Environment check failed: ${e?.message || e}`);
349
+ checks.push({
350
+ name: "preflight",
351
+ ok: false,
352
+ fatal: true,
353
+ version: null,
354
+ message: e?.message || "unexpected error during the environment check",
355
+ });
356
+ return { ok: false, checks };
357
+ }
358
+ }
package/lib/provision.js CHANGED
@@ -81,6 +81,38 @@ function firebaseExec(args, projectId, timeout = 60000) {
81
81
  }
82
82
  }
83
83
 
84
+ /** Sleep — used by the retry/backoff loops below. */
85
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
86
+
87
+ // ─── gcloud CLI wrappers ──────────────────────────────────────────────
88
+
89
+ /** True when the gcloud CLI is installed and on PATH. */
90
+ function hasGcloud() {
91
+ try {
92
+ execSync("gcloud --version", { timeout: 30000, stdio: "ignore" });
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ /** Run gcloud command, return raw stdout string or { error, message }. */
100
+ function gcloudRaw(args, timeout = 180000) {
101
+ try {
102
+ return execSync(`gcloud ${args}`, {
103
+ encoding: "utf8",
104
+ timeout,
105
+ cwd: _cwd,
106
+ stdio: ["ignore", "pipe", "pipe"],
107
+ }).trim();
108
+ } catch (e) {
109
+ return {
110
+ error: true,
111
+ message: (e.stderr || e.stdout || e.message || "").trim(),
112
+ };
113
+ }
114
+ }
115
+
84
116
  // ─── Service checks ───────────────────────────────────────────────────
85
117
 
86
118
  function checkLogin() {
@@ -91,6 +123,41 @@ function checkLogin() {
91
123
  return { ok: false };
92
124
  }
93
125
 
126
+ /**
127
+ * Direct existence probe — a project-scoped call that only succeeds when the
128
+ * project really exists for this account. Unlike projects:list it is not
129
+ * served from an eventually-consistent index.
130
+ */
131
+ function projectExistsDirect(projectId) {
132
+ // apps:list is cheap and project-scoped: it errors on a missing project but
133
+ // succeeds (possibly with zero apps) on one that exists.
134
+ const r = firebaseJson("apps:list", projectId);
135
+ if (r?.status === "success") return true;
136
+ // Secondary probe — `firebase use` resolves the project id server-side.
137
+ return firebaseExec(`use ${projectId}`);
138
+ }
139
+
140
+ /**
141
+ * Confirm a project exists, tolerating the eventual consistency of
142
+ * `firebase projects:list`. A project created seconds earlier (by index.js)
143
+ * is often absent from that list, which used to produce a bogus
144
+ * "Create Firebase project X?" prompt for a project that already exists.
145
+ */
146
+ async function confirmProjectExists(projectId, listed = []) {
147
+ if (listed.some((p) => p.projectId === projectId)) return true;
148
+ if (projectExistsDirect(projectId)) return true;
149
+
150
+ info("Waiting for the project to appear in Firebase...");
151
+ for (const delay of [1500, 3000, 5000]) {
152
+ await sleep(delay);
153
+ const r = firebaseJson("projects:list");
154
+ const projects = r?.status === "success" ? r.result || [] : [];
155
+ if (projects.some((p) => p.projectId === projectId)) return true;
156
+ if (projectExistsDirect(projectId)) return true;
157
+ }
158
+ return false;
159
+ }
160
+
94
161
  function checkFirestore(projectId) {
95
162
  const r = firebaseJson("firestore:databases:list", projectId);
96
163
  if (r?.status === "success") {
@@ -110,6 +177,77 @@ function createFirestoreDb(projectId, region) {
110
177
  return { ok: false, error: out.message };
111
178
  }
112
179
 
180
+ /** Google API that must be enabled before the first Firestore call. */
181
+ const FIRESTORE_API = "firestore.googleapis.com";
182
+
183
+ /**
184
+ * True for the "API not enabled" 403. Google reports this with status
185
+ * PERMISSION_DENIED, so it must be tested *before* the billing/permission
186
+ * branches or it gets misdiagnosed as a billing problem.
187
+ */
188
+ function isApiDisabledError(message = "") {
189
+ return (
190
+ message.includes("has not been used in project") ||
191
+ message.includes("SERVICE_DISABLED") ||
192
+ message.includes("accessNotConfigured") ||
193
+ (message.includes(FIRESTORE_API) &&
194
+ (message.includes("403") || message.includes("is disabled")))
195
+ );
196
+ }
197
+
198
+ /**
199
+ * Ensure the Cloud Firestore API is enabled on a (possibly brand-new) project.
200
+ *
201
+ * A fresh GCP project has firestore.googleapis.com disabled, so the very first
202
+ * `firestore:databases:create` fails with 403. The firebase CLI has no command
203
+ * to enable an API, so shell out to gcloud when it is available.
204
+ *
205
+ * @returns {{ enabled: boolean, reason?: string, error?: string }}
206
+ */
207
+ function ensureFirestoreApi(projectId) {
208
+ if (!hasGcloud()) return { enabled: false, reason: "no-gcloud" };
209
+
210
+ const out = gcloudRaw(
211
+ `services enable ${FIRESTORE_API} --project=${projectId}`,
212
+ );
213
+ if (typeof out === "string") return { enabled: true };
214
+ return { enabled: false, reason: "failed", error: out.message };
215
+ }
216
+
217
+ /** Actionable guidance for the "API not enabled" 403 (retrying will not help). */
218
+ function printApiDisabledHelp(projectId, region) {
219
+ warn(`The Cloud Firestore API is not enabled on project "${projectId}"`);
220
+ info("Re-running the same create command will fail identically until it is.");
221
+ info(
222
+ `Enable: ${c.cyan}https://console.cloud.google.com/apis/library/${FIRESTORE_API}?project=${projectId}${c.reset}`,
223
+ );
224
+ info(
225
+ `Or run: ${c.dim}gcloud services enable ${FIRESTORE_API} --project=${projectId}${c.reset}`,
226
+ );
227
+ info("Then (allow ~1 min for it to propagate):");
228
+ info(
229
+ ` firebase firestore:databases:create "(default)" --location=${region} --project=${projectId}`,
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Set the Cloud Functions artifact cleanup policy.
235
+ *
236
+ * `firebase deploy` prompts for this on the first functions deploy. In a
237
+ * non-interactive shell that prompt aborts the run *after* functions deploy
238
+ * but *before* the hosting release, leaving Hosting UNRELEASED (site 404s).
239
+ * Setting the policy up-front removes the prompt from the user's later deploy.
240
+ */
241
+ function setArtifactPolicy(projectId) {
242
+ const out = firebaseRaw(
243
+ "functions:artifacts:setpolicy --force",
244
+ projectId,
245
+ 120000,
246
+ );
247
+ if (typeof out === "string") return { ok: true, output: out };
248
+ return { ok: false, error: out.message };
249
+ }
250
+
113
251
  function fetchLocations() {
114
252
  const raw = firebaseRaw("firestore:locations");
115
253
  if (typeof raw !== "string") return null;
@@ -207,9 +345,9 @@ export async function provisionFirebase(config, options = {}) {
207
345
  }
208
346
 
209
347
  // ── 2. Project ──────────────────────────────────────────────
210
- const projectExists = (login.projects || []).some(
211
- (p) => p.projectId === projectId,
212
- );
348
+ // projects:list is eventually consistent, so a project created moments ago
349
+ // may be missing from it — confirm properly before offering to create one.
350
+ const projectExists = await confirmProjectExists(projectId, login.projects);
213
351
 
214
352
  if (!projectExists && !isCI) {
215
353
  warn(`Project "${projectId}" not found in your Firebase account`);
@@ -283,10 +421,35 @@ export async function provisionFirebase(config, options = {}) {
283
421
  }
284
422
 
285
423
  if (region) {
424
+ // A brand-new GCP project has firestore.googleapis.com disabled, which
425
+ // makes the very first create return 403 — enable the API first.
426
+ info("Ensuring the Cloud Firestore API is enabled...");
427
+ const api = ensureFirestoreApi(projectId);
428
+ if (api.enabled) {
429
+ ok("Cloud Firestore API enabled");
430
+ } else if (api.reason === "no-gcloud") {
431
+ info("gcloud not found — skipping API pre-enable");
432
+ } else {
433
+ warn("Could not enable the Cloud Firestore API automatically");
434
+ }
435
+
286
436
  info(`Provisioning Firestore in ${c.cyan}${region}${c.reset}...`);
287
- const result = createFirestoreDb(projectId, region);
437
+ let result = createFirestoreDb(projectId, region);
438
+
439
+ // A freshly enabled API takes a moment to go live — retry once.
440
+ if (api.enabled && !result.ok && isApiDisabledError(result.error)) {
441
+ info("API just enabled — waiting for it to propagate, then retrying...");
442
+ await sleep(10000);
443
+ result = createFirestoreDb(projectId, region);
444
+ }
445
+
288
446
  if (result.ok) {
289
447
  ok(`Firestore: ${c.cyan}${region}${c.reset}`);
448
+ } else if (isApiDisabledError(result.error)) {
449
+ // Reported as PERMISSION_DENIED but it is not a billing/permission
450
+ // problem — handle it before those branches so the advice is correct.
451
+ fail("Firestore provisioning failed — Cloud Firestore API not enabled");
452
+ printApiDisabledHelp(projectId, region);
290
453
  } else {
291
454
  fail("Firestore provisioning failed");
292
455
  if (
@@ -378,7 +541,26 @@ export async function provisionFirebase(config, options = {}) {
378
541
  );
379
542
  }
380
543
 
381
- // ── 6. Cloud Functions / billing ────────────────────────────
544
+ // ── 6. Deploy prerequisites ─────────────────────────────────
545
+ // Do this now so the user's later plain `firebase deploy` never hits the
546
+ // artifact cleanup-policy prompt, which aborts between the functions deploy
547
+ // and the hosting release. Non-fatal — warn only.
548
+ console.log();
549
+ info("Setting Functions artifact cleanup policy...");
550
+ const policy = setArtifactPolicy(projectId);
551
+
552
+ if (policy.ok) {
553
+ ok("Artifact cleanup policy set — 'firebase deploy' will not prompt");
554
+ } else {
555
+ warn("Could not set the artifact cleanup policy (non-fatal)");
556
+ info("If 'firebase deploy' stops before releasing Hosting, run:");
557
+ info(
558
+ ` firebase functions:artifacts:setpolicy --force --project=${projectId}`,
559
+ );
560
+ info(" or deploy with: firebase deploy --force");
561
+ }
562
+
563
+ // ── 7. Cloud Functions / billing ────────────────────────────
382
564
  console.log();
383
565
  info(
384
566
  `${c.bold}Cloud Functions${c.reset} require the Blaze (pay-as-you-go) plan`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-shopify-firebase-app",
3
- "version": "2.0.6",
3
+ "version": "2.1.0",
4
4
  "description": "Create Shopify apps powered by Firebase — multi-page dashboard, App Bridge, Polaris components, TypeScript or JavaScript. Deploy for free.",
5
5
  "keywords": [
6
6
  "shopify",
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "main": "src/index.js",
5
5
  "engines": {
6
- "node": "20"
6
+ "node": "22"
7
7
  },
8
8
  "scripts": {
9
9
  "serve": "firebase emulators:start --only functions,firestore",
@@ -15,10 +15,10 @@
15
15
  "deploy:all": "firebase deploy"
16
16
  },
17
17
  "dependencies": {
18
- "cors": "^2.8.5",
19
- "express": "^4.21.0",
20
- "firebase-admin": "^13.0.0",
21
- "firebase-functions": "^6.3.0",
22
- "jsonwebtoken": "^9.0.2"
18
+ "cors": "^2.8.6",
19
+ "express": "^5.2.1",
20
+ "firebase-admin": "^14.2.0",
21
+ "firebase-functions": "^7.3.2",
22
+ "jsonwebtoken": "^9.0.3"
23
23
  }
24
24
  }
@@ -10,7 +10,7 @@ adminApiRouter.use(verifySessionToken);
10
10
 
11
11
  // Shopify API version — update when Shopify releases new versions
12
12
  // Docs: https://shopify.dev/docs/api/usage/versioning
13
- const API_VERSION = "2026-01";
13
+ const API_VERSION = "2026-07";
14
14
 
15
15
  // Default app settings — returned when no settings are saved yet
16
16
  const DEFAULT_SETTINGS = {