create-shopify-firebase-app 1.0.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,390 @@
1
+ /**
2
+ * Firebase Provisioning
3
+ *
4
+ * Checks and provisions Firebase services from the CLI:
5
+ * - Authentication (firebase login)
6
+ * - Project linking / creation
7
+ * - Firestore database (with region selection)
8
+ * - Web App registration
9
+ * - Hosting site verification
10
+ * - Cloud Functions billing warning
11
+ */
12
+
13
+ import { execSync } from "node:child_process";
14
+ import prompts from "prompts";
15
+
16
+ // ─── ANSI helpers ─────────────────────────────────────────────────────
17
+ const c = {
18
+ reset: "\x1b[0m",
19
+ bold: "\x1b[1m",
20
+ dim: "\x1b[2m",
21
+ green: "\x1b[32m",
22
+ cyan: "\x1b[36m",
23
+ yellow: "\x1b[33m",
24
+ red: "\x1b[31m",
25
+ };
26
+
27
+ const ok = (msg) => console.log(` ${c.green}✔${c.reset} ${msg}`);
28
+ const warn = (msg) => console.log(` ${c.yellow}⚠${c.reset} ${msg}`);
29
+ const info = (msg) => console.log(` ${c.cyan}ℹ${c.reset} ${msg}`);
30
+ const fail = (msg) => console.log(` ${c.red}✘${c.reset} ${msg}`);
31
+
32
+ // ─── Firebase CLI wrappers ────────────────────────────────────────────
33
+
34
+ // All wrappers accept an optional cwd for commands that need a project dir
35
+ let _cwd = undefined;
36
+
37
+ /** Run firebase command, parse JSON output. Returns null on failure. */
38
+ function firebaseJson(args, projectId) {
39
+ const pFlag = projectId ? ` --project=${projectId}` : "";
40
+ try {
41
+ const out = execSync(`firebase ${args}${pFlag} --json`, {
42
+ encoding: "utf8",
43
+ timeout: 60000,
44
+ cwd: _cwd,
45
+ stdio: ["ignore", "pipe", "pipe"],
46
+ });
47
+ return JSON.parse(out);
48
+ } catch (e) {
49
+ try { return JSON.parse(e.stdout || ""); } catch {}
50
+ try { return JSON.parse(e.stderr || ""); } catch {}
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /** Run firebase command, return raw stdout string or { error, message }. */
56
+ function firebaseRaw(args, projectId, timeout = 60000) {
57
+ const pFlag = projectId ? ` --project=${projectId}` : "";
58
+ try {
59
+ return execSync(`firebase ${args}${pFlag}`, {
60
+ encoding: "utf8",
61
+ timeout,
62
+ cwd: _cwd,
63
+ stdio: ["ignore", "pipe", "pipe"],
64
+ }).trim();
65
+ } catch (e) {
66
+ return {
67
+ error: true,
68
+ message: (e.stderr || e.stdout || e.message || "").trim(),
69
+ };
70
+ }
71
+ }
72
+
73
+ /** Run firebase command silently. Returns true on success. */
74
+ function firebaseExec(args, projectId, timeout = 60000) {
75
+ const pFlag = projectId ? ` --project=${projectId}` : "";
76
+ try {
77
+ execSync(`firebase ${args}${pFlag}`, { timeout, cwd: _cwd, stdio: "ignore" });
78
+ return true;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ // ─── Service checks ───────────────────────────────────────────────────
85
+
86
+ function checkLogin() {
87
+ const r = firebaseJson("projects:list");
88
+ if (r?.status === "success") {
89
+ return { ok: true, projects: r.result || [] };
90
+ }
91
+ return { ok: false };
92
+ }
93
+
94
+ function checkFirestore(projectId) {
95
+ const r = firebaseJson("firestore:databases:list", projectId);
96
+ if (r?.status === "success") {
97
+ const dbs = r.result || [];
98
+ return { provisioned: dbs.length > 0, databases: dbs };
99
+ }
100
+ return { provisioned: false };
101
+ }
102
+
103
+ function createFirestoreDb(projectId, region) {
104
+ const out = firebaseRaw(
105
+ `firestore:databases:create "(default)" --location=${region}`,
106
+ projectId,
107
+ 120000,
108
+ );
109
+ if (typeof out === "string") return { ok: true, output: out };
110
+ return { ok: false, error: out.message };
111
+ }
112
+
113
+ function fetchLocations() {
114
+ const raw = firebaseRaw("firestore:locations");
115
+ if (typeof raw !== "string") return null;
116
+
117
+ const locations = [];
118
+ for (const line of raw.split("\n")) {
119
+ const m = line.match(/│\s*(.+?)\s*│\s*(\S+)\s*│/);
120
+ if (m && !m[1].includes("Display Name")) {
121
+ locations.push({
122
+ title: `${m[2].trim().padEnd(28)} ${c.dim}${m[1].trim()}${c.reset}`,
123
+ value: m[2].trim(),
124
+ });
125
+ }
126
+ }
127
+ return locations.length > 0 ? locations : null;
128
+ }
129
+
130
+ function checkWebApps(projectId) {
131
+ const r = firebaseJson("apps:list WEB", projectId);
132
+ if (r?.status === "success") {
133
+ const apps = r.result || [];
134
+ return { exists: apps.length > 0, apps };
135
+ }
136
+ return { exists: false, apps: [] };
137
+ }
138
+
139
+ function createWebApp(projectId, name) {
140
+ const r = firebaseJson(`apps:create WEB "${name}"`, projectId);
141
+ if (r?.status === "success") {
142
+ return { ok: true, appId: r.result?.appId };
143
+ }
144
+ return { ok: false, error: r?.error };
145
+ }
146
+
147
+ function getAppConfig(appId, projectId) {
148
+ const r = firebaseJson(`apps:sdkconfig WEB ${appId}`, projectId);
149
+ return r?.status === "success" ? r.result?.sdkConfig : null;
150
+ }
151
+
152
+ function checkHosting(projectId) {
153
+ const r = firebaseJson("hosting:sites:list", projectId);
154
+ if (r?.status === "success") {
155
+ const sites = r.result?.sites || r.result || [];
156
+ return { exists: Array.isArray(sites) && sites.length > 0, sites };
157
+ }
158
+ return { exists: false, sites: [] };
159
+ }
160
+
161
+ // ─── Main provisioning flow ──────────────────────────────────────────
162
+
163
+ /**
164
+ * Run the Firebase provisioning flow.
165
+ *
166
+ * @param {object} config - { projectId, appName, projectName }
167
+ * @param {object} options - { skipProvision, firestoreRegion, nonInteractive, cwd }
168
+ */
169
+ export async function provisionFirebase(config, options = {}) {
170
+ const { projectId, appName } = config;
171
+ const isCI = !!options.nonInteractive;
172
+
173
+ // Set working directory for firebase commands that need firebase.json
174
+ _cwd = options.cwd;
175
+
176
+ // ── 1. Login ────────────────────────────────────────────────
177
+ const login = checkLogin();
178
+
179
+ if (!login.ok) {
180
+ fail("Not logged into Firebase CLI");
181
+ if (!isCI) {
182
+ const { doLogin } = await prompts({
183
+ type: "confirm",
184
+ name: "doLogin",
185
+ message: "Open browser to log in to Firebase?",
186
+ initial: true,
187
+ });
188
+ if (doLogin) {
189
+ info("Opening browser for Firebase login...");
190
+ try {
191
+ execSync("firebase login", { stdio: "inherit", timeout: 120000 });
192
+ ok("Logged in successfully");
193
+ } catch {
194
+ fail("Login failed — run 'firebase login' manually");
195
+ return;
196
+ }
197
+ } else {
198
+ info("Run 'firebase login' then re-run this tool");
199
+ return;
200
+ }
201
+ } else {
202
+ info("Run: firebase login");
203
+ return;
204
+ }
205
+ } else {
206
+ ok("Firebase authenticated");
207
+ }
208
+
209
+ // ── 2. Project ──────────────────────────────────────────────
210
+ const projectExists = (login.projects || []).some(
211
+ (p) => p.projectId === projectId,
212
+ );
213
+
214
+ if (!projectExists && !isCI) {
215
+ warn(`Project "${projectId}" not found in your Firebase account`);
216
+ const { shouldCreate } = await prompts({
217
+ type: "confirm",
218
+ name: "shouldCreate",
219
+ message: `Create Firebase project "${projectId}"?`,
220
+ initial: false,
221
+ });
222
+ if (shouldCreate) {
223
+ info("Creating project (this may take a moment)...");
224
+ const out = firebaseRaw(
225
+ `projects:create ${projectId}`,
226
+ null,
227
+ 120000,
228
+ );
229
+ if (typeof out === "string") {
230
+ ok("Firebase project created");
231
+ } else if (out.message?.includes("already exists")) {
232
+ info("GCP project exists — adding Firebase resources...");
233
+ firebaseRaw(`projects:addfirebase ${projectId}`, null, 120000);
234
+ } else {
235
+ fail("Could not create project");
236
+ info(out.message);
237
+ return;
238
+ }
239
+ }
240
+ }
241
+
242
+ if (firebaseExec(`use ${projectId}`)) {
243
+ ok(`Project: ${c.cyan}${projectId}${c.reset}`);
244
+ } else {
245
+ fail(`Could not link project "${projectId}"`);
246
+ info("Verify the project exists at console.firebase.google.com");
247
+ return;
248
+ }
249
+
250
+ if (options.skipProvision) {
251
+ info("Skipping service provisioning (--skip-provision)");
252
+ return;
253
+ }
254
+
255
+ // ── 3. Firestore ────────────────────────────────────────────
256
+ console.log();
257
+ info("Checking Firestore...");
258
+ const fs = checkFirestore(projectId);
259
+
260
+ if (fs.provisioned) {
261
+ const db = fs.databases[0];
262
+ ok(`Firestore: ${c.cyan}${db?.locationId || "provisioned"}${c.reset}`);
263
+ } else {
264
+ warn("Firestore not provisioned");
265
+
266
+ let region = options.firestoreRegion;
267
+
268
+ if (!region && !isCI) {
269
+ const locations = fetchLocations();
270
+ if (locations) {
271
+ const defaultIdx = locations.findIndex(
272
+ (l) => l.value === "asia-south1",
273
+ );
274
+ const answer = await prompts({
275
+ type: "select",
276
+ name: "region",
277
+ message: "Firestore region (cannot be changed later)",
278
+ choices: locations,
279
+ initial: defaultIdx >= 0 ? defaultIdx : 0,
280
+ });
281
+ region = answer.region;
282
+ }
283
+ }
284
+
285
+ if (region) {
286
+ info(`Provisioning Firestore in ${c.cyan}${region}${c.reset}...`);
287
+ const result = createFirestoreDb(projectId, region);
288
+ if (result.ok) {
289
+ ok(`Firestore: ${c.cyan}${region}${c.reset}`);
290
+ } else {
291
+ fail("Firestore provisioning failed");
292
+ if (
293
+ result.error?.includes("PERMISSION_DENIED") ||
294
+ result.error?.includes("billing") ||
295
+ result.error?.includes("Billing")
296
+ ) {
297
+ warn("Billing (Blaze plan) may be required");
298
+ info(
299
+ `Enable: https://console.firebase.google.com/project/${projectId}/usage/details`,
300
+ );
301
+ } else if (result.error) {
302
+ info(result.error.split("\n")[0]);
303
+ }
304
+ info(
305
+ `Manual: firebase firestore:databases:create "(default)" --location=${region} --project=${projectId}`,
306
+ );
307
+ }
308
+ } else if (!isCI) {
309
+ info("Skipped — provision later with:");
310
+ info(
311
+ `firebase firestore:databases:create "(default)" --location=REGION --project=${projectId}`,
312
+ );
313
+ }
314
+ }
315
+
316
+ // ── 4. Web App ──────────────────────────────────────────────
317
+ console.log();
318
+ info("Checking Web App...");
319
+ const wa = checkWebApps(projectId);
320
+
321
+ if (wa.exists) {
322
+ ok(
323
+ `Web App: ${c.cyan}${wa.apps[0].displayName || wa.apps[0].appId}${c.reset}`,
324
+ );
325
+ } else {
326
+ let shouldCreate = false;
327
+
328
+ if (!isCI) {
329
+ const answer = await prompts({
330
+ type: "confirm",
331
+ name: "shouldCreate",
332
+ message: `Create Firebase Web App "${appName}"?`,
333
+ initial: true,
334
+ });
335
+ shouldCreate = answer.shouldCreate;
336
+ } else if (options.firestoreRegion) {
337
+ // In CI with explicit flags, auto-create
338
+ shouldCreate = true;
339
+ }
340
+
341
+ if (shouldCreate) {
342
+ info("Creating Web App...");
343
+ const result = createWebApp(projectId, appName);
344
+ if (result.ok) {
345
+ ok(`Web App: ${c.cyan}${appName}${c.reset}`);
346
+ if (result.appId) {
347
+ const cfg = getAppConfig(result.appId, projectId);
348
+ if (cfg) {
349
+ info(`API Key: ${c.dim}${cfg.apiKey}${c.reset}`);
350
+ info(`Auth Domain: ${c.dim}${cfg.authDomain}${c.reset}`);
351
+ }
352
+ }
353
+ } else {
354
+ fail("Could not create Web App");
355
+ info(
356
+ `Manual: firebase apps:create WEB "${appName}" --project=${projectId}`,
357
+ );
358
+ }
359
+ } else if (!isCI) {
360
+ info(
361
+ `Create later: firebase apps:create WEB "${appName}" --project=${projectId}`,
362
+ );
363
+ }
364
+ }
365
+
366
+ // ── 5. Hosting ──────────────────────────────────────────────
367
+ console.log();
368
+ info("Checking Hosting...");
369
+ const hs = checkHosting(projectId);
370
+
371
+ if (hs.exists) {
372
+ const url = hs.sites[0]?.defaultUrl || hs.sites[0]?.name;
373
+ ok(`Hosting: ${c.cyan}${url}${c.reset}`);
374
+ } else {
375
+ info("Hosting auto-provisions on first deploy");
376
+ info(
377
+ `Deploy: firebase deploy --only hosting --project=${projectId}`,
378
+ );
379
+ }
380
+
381
+ // ── 6. Cloud Functions / billing ────────────────────────────
382
+ console.log();
383
+ info(
384
+ `${c.bold}Cloud Functions${c.reset} require the Blaze (pay-as-you-go) plan`,
385
+ );
386
+ info(
387
+ `Upgrade: ${c.cyan}https://console.firebase.google.com/project/${projectId}/usage/details${c.reset}`,
388
+ );
389
+ info("Blaze free tier includes 2M function invocations/month");
390
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "create-shopify-firebase-app",
3
+ "version": "1.0.0",
4
+ "description": "Create Shopify apps powered by Firebase — serverless, lightweight, zero-framework. The official alternative to Remix for Shopify + Firebase developers.",
5
+ "keywords": [
6
+ "shopify",
7
+ "firebase",
8
+ "shopify-app",
9
+ "shopify-firebase",
10
+ "create-shopify-app",
11
+ "firebase-shopify",
12
+ "shopify-cli",
13
+ "shopify-app-template",
14
+ "shopify-app-scaffold",
15
+ "firebase-functions",
16
+ "firestore",
17
+ "cloud-functions",
18
+ "serverless",
19
+ "embedded-app",
20
+ "app-bridge",
21
+ "theme-app-extension",
22
+ "shopify-oauth",
23
+ "shopify-webhooks",
24
+ "express",
25
+ "typescript"
26
+ ],
27
+ "homepage": "https://github.com/mksd0398/create-shopify-firebase-app#readme",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/mksd0398/create-shopify-firebase-app.git"
31
+ },
32
+ "license": "MIT",
33
+ "author": "Mayank Khan Singh DeSouza <mayank@kloctechnologies.com>",
34
+ "type": "module",
35
+ "bin": {
36
+ "create-shopify-firebase-app": "bin/create.js"
37
+ },
38
+ "files": [
39
+ "bin",
40
+ "lib",
41
+ "templates",
42
+ "LICENSE",
43
+ "README.md"
44
+ ],
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "dependencies": {
49
+ "prompts": "^2.4.2"
50
+ }
51
+ }
@@ -0,0 +1,5 @@
1
+ # Shopify App Credentials (from Partner Dashboard → App → API credentials)
2
+ SHOPIFY_API_KEY=your-api-key
3
+ SHOPIFY_API_SECRET=your-api-secret
4
+ SCOPES=read_products
5
+ APP_URL=https://your-project-id.web.app
@@ -0,0 +1,11 @@
1
+ .app-block { padding: 24px; margin: 16px 0; font-family: inherit; }
2
+ .app-block[data-show-border="true"] { border: 1px solid #e1e3e5; border-radius: 12px; }
3
+ .app-block__heading { font-size: 20px; font-weight: 600; margin-bottom: 12px; }
4
+ .app-block__content { color: #6d7175; margin-bottom: 16px; line-height: 1.6; }
5
+ .app-block__button {
6
+ display: inline-flex; align-items: center; padding: 10px 20px;
7
+ background: #008060; color: #fff; border: none; border-radius: 8px;
8
+ font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.15s;
9
+ }
10
+ .app-block__button:hover { background: #006e52; }
11
+ .app-block__button:disabled { opacity: 0.5; cursor: not-allowed; }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Theme App Extension — storefront JavaScript.
3
+ * Communicate with your backend via App Proxy:
4
+ * fetch("/apps/{subpath}/your-endpoint")
5
+ */
6
+ (function () {
7
+ "use strict";
8
+ document.querySelectorAll(".app-block__button").forEach((btn) => {
9
+ btn.addEventListener("click", async () => {
10
+ btn.disabled = true;
11
+ btn.textContent = "Loading...";
12
+ try {
13
+ // Example: fetch("/apps/myapp/hello")
14
+ // const data = await (await fetch("/apps/myapp/hello")).json();
15
+ alert("Connect this button to your App Proxy endpoint.");
16
+ } finally {
17
+ btn.disabled = false;
18
+ btn.textContent = "Click me";
19
+ }
20
+ });
21
+ });
22
+ })();
@@ -0,0 +1,69 @@
1
+ {% comment %}
2
+ Theme App Extension Block
3
+ ─────────────────────────
4
+ Merchants add this via Theme Editor. Communicates with your backend
5
+ through the App Proxy at: /apps/{subpath}/your-route
6
+ {% endcomment %}
7
+
8
+ {{ 'app-block.css' | asset_url | stylesheet_tag }}
9
+
10
+ <div
11
+ class="app-block"
12
+ id="app-block-{{ block.id }}"
13
+ data-show-border="{{ block.settings.show_border }}"
14
+ >
15
+ {% if block.settings.heading != blank %}
16
+ <h2 class="app-block__heading">{{ block.settings.heading }}</h2>
17
+ {% endif %}
18
+
19
+ <div class="app-block__content">
20
+ {{ block.settings.description }}
21
+ </div>
22
+
23
+ {% if block.settings.show_button %}
24
+ <button class="app-block__button" type="button">
25
+ {{ block.settings.button_text }}
26
+ </button>
27
+ {% endif %}
28
+ </div>
29
+
30
+ {{ 'app-block.js' | asset_url | script_tag }}
31
+
32
+ {% schema %}
33
+ {
34
+ "name": "App Block",
35
+ "target": "section",
36
+ "settings": [
37
+ {
38
+ "type": "text",
39
+ "id": "heading",
40
+ "label": "Heading",
41
+ "default": "My App"
42
+ },
43
+ {
44
+ "type": "richtext",
45
+ "id": "description",
46
+ "label": "Description",
47
+ "default": "<p>Customize this block in the Theme Editor.</p>"
48
+ },
49
+ {
50
+ "type": "checkbox",
51
+ "id": "show_button",
52
+ "label": "Show button",
53
+ "default": true
54
+ },
55
+ {
56
+ "type": "text",
57
+ "id": "button_text",
58
+ "label": "Button text",
59
+ "default": "Click me"
60
+ },
61
+ {
62
+ "type": "checkbox",
63
+ "id": "show_border",
64
+ "label": "Show border",
65
+ "default": true
66
+ }
67
+ ]
68
+ }
69
+ {% endschema %}
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "App Block",
3
+ "settings": {
4
+ "heading": { "label": "Heading" },
5
+ "description": { "label": "Description" },
6
+ "show_button": { "label": "Show button" },
7
+ "button_text": { "label": "Button text" },
8
+ "show_border": { "label": "Show border" }
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ api_version = "2025-04"
2
+
3
+ [[extensions]]
4
+ name = "App Block"
5
+ handle = "app-block"
6
+ type = "theme_app_extension"
7
+
8
+ [[extensions.targeting]]
9
+ module = "./blocks/app-block.liquid"
10
+ target = "section"
@@ -0,0 +1,24 @@
1
+ {
2
+ "hosting": {
3
+ "public": "web",
4
+ "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
5
+ "rewrites": [
6
+ { "source": "/auth", "function": "app" },
7
+ { "source": "/auth/**", "function": "app" },
8
+ { "source": "/api", "function": "app" },
9
+ { "source": "/api/**", "function": "app" },
10
+ { "source": "/proxy", "function": "app" },
11
+ { "source": "/proxy/**", "function": "app" },
12
+ { "source": "/webhooks", "function": "app" },
13
+ { "source": "/webhooks/**", "function": "app" }
14
+ ]
15
+ },
16
+ "functions": {
17
+ "source": "functions",
18
+ "runtime": "nodejs20"
19
+ },
20
+ "firestore": {
21
+ "rules": "firestore.rules",
22
+ "indexes": "firestore.indexes.json"
23
+ }
24
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "indexes": [],
3
+ "fieldOverrides": []
4
+ }
@@ -0,0 +1,10 @@
1
+ rules_version = '2';
2
+ service cloud.firestore {
3
+ match /databases/{database}/documents {
4
+ // All access goes through Cloud Functions (admin SDK).
5
+ // No direct client-side access — the backend is the single trusted entry point.
6
+ match /{document=**} {
7
+ allow read, write: if false;
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "shopify-firebase-functions",
3
+ "private": true,
4
+ "main": "lib/index.js",
5
+ "engines": {
6
+ "node": "20"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "serve": "npm run build && firebase emulators:start --only functions,firestore",
11
+ "deploy": "firebase deploy --only functions",
12
+ "deploy:all": "firebase deploy"
13
+ },
14
+ "dependencies": {
15
+ "cors": "^2.8.5",
16
+ "express": "^4.21.0",
17
+ "firebase-admin": "^12.7.0",
18
+ "firebase-functions": "^5.1.1",
19
+ "jsonwebtoken": "^9.0.2",
20
+ "node-fetch": "^2.7.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/cors": "^2.8.17",
24
+ "@types/express": "^5.0.0",
25
+ "@types/jsonwebtoken": "^9.0.7",
26
+ "@types/node-fetch": "^2.6.12",
27
+ "typescript": "^5.6.3"
28
+ }
29
+ }