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.
- package/README.md +122 -74
- package/lib/index.js +540 -238
- package/lib/preflight.js +358 -0
- package/lib/provision.js +187 -5
- package/package.json +1 -1
- package/templates/js/functions/package.json +6 -6
- package/templates/js/functions/src/admin-api.js +1 -1
- package/templates/js/functions/src/auth.js +45 -13
- package/templates/js/functions/src/firebase.js +6 -4
- package/templates/js/functions/src/index.js +14 -0
- package/templates/shared/extensions/theme-block/blocks/app-block.liquid +1 -1
- package/templates/shared/extensions/theme-block/shopify.extension.toml +1 -1
- package/templates/shared/firebase.json +1 -1
- package/templates/shared/firestore.rules +10 -2
- package/templates/shopify.app.toml +1 -1
- package/templates/ts/functions/package.json +10 -10
- package/templates/ts/functions/src/admin-api.ts +5 -2
- package/templates/ts/functions/src/auth.ts +69 -15
- package/templates/ts/functions/src/firebase.ts +8 -4
- package/templates/ts/functions/tsconfig.json +2 -1
- package/templates/web/css/app.css +78 -1138
- package/templates/web/index.html +79 -68
- package/templates/web/js/app.js +60 -33
- package/templates/web/js/pages/home.js +33 -22
- package/templates/web/js/pages/polaris-demo.js +25 -83
- package/templates/web/js/pages/products.js +100 -114
- package/templates/web/js/pages/settings.js +68 -166
- package/templates/web/polaris.html +1221 -950
- package/templates/web/products.html +35 -40
- package/templates/web/settings.html +69 -17
package/lib/preflight.js
ADDED
|
@@ -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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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": "
|
|
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.
|
|
19
|
-
"express": "^
|
|
20
|
-
"firebase-admin": "^
|
|
21
|
-
"firebase-functions": "^
|
|
22
|
-
"jsonwebtoken": "^9.0.
|
|
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-
|
|
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 = {
|