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.
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/create.js +17 -0
- package/lib/index.js +488 -0
- package/lib/provision.js +390 -0
- package/package.json +51 -0
- package/templates/env.example +5 -0
- package/templates/extensions/theme-block/assets/app-block.css +11 -0
- package/templates/extensions/theme-block/assets/app-block.js +22 -0
- package/templates/extensions/theme-block/blocks/app-block.liquid +69 -0
- package/templates/extensions/theme-block/locales/en.default.json +10 -0
- package/templates/extensions/theme-block/shopify.extension.toml +10 -0
- package/templates/firebase.json +24 -0
- package/templates/firestore.indexes.json +4 -0
- package/templates/firestore.rules +10 -0
- package/templates/functions/package.json +29 -0
- package/templates/functions/src/admin-api.ts +116 -0
- package/templates/functions/src/auth.ts +130 -0
- package/templates/functions/src/config.ts +12 -0
- package/templates/functions/src/firebase.ts +10 -0
- package/templates/functions/src/index.ts +40 -0
- package/templates/functions/src/proxy.ts +50 -0
- package/templates/functions/src/verify-token.ts +55 -0
- package/templates/functions/src/webhooks.ts +77 -0
- package/templates/functions/tsconfig.json +15 -0
- package/templates/gitignore +33 -0
- package/templates/shopify.app.toml +38 -0
- package/templates/web/css/app.css +57 -0
- package/templates/web/index.html +64 -0
- package/templates/web/js/bridge.js +98 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* create-shopify-firebase-app — CLI core
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the entire scaffolding flow:
|
|
5
|
+
* 1. Collect project config (interactive prompts or CLI args)
|
|
6
|
+
* 2. Scaffold files from templates
|
|
7
|
+
* 3. Install dependencies
|
|
8
|
+
* 4. Wire up Firebase + Shopify
|
|
9
|
+
* 5. Initialize git
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { execSync, spawn } from "node:child_process";
|
|
16
|
+
import prompts from "prompts";
|
|
17
|
+
import { provisionFirebase } from "./provision.js";
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
|
|
21
|
+
|
|
22
|
+
// ─── ANSI helpers (no chalk dependency) ──────────────────────────────────
|
|
23
|
+
const c = {
|
|
24
|
+
reset: "\x1b[0m",
|
|
25
|
+
bold: "\x1b[1m",
|
|
26
|
+
dim: "\x1b[2m",
|
|
27
|
+
green: "\x1b[32m",
|
|
28
|
+
cyan: "\x1b[36m",
|
|
29
|
+
yellow: "\x1b[33m",
|
|
30
|
+
red: "\x1b[31m",
|
|
31
|
+
magenta: "\x1b[35m",
|
|
32
|
+
white: "\x1b[37m",
|
|
33
|
+
bgGreen: "\x1b[42m",
|
|
34
|
+
bgCyan: "\x1b[46m",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ok = (msg) => console.log(` ${c.green}✔${c.reset} ${msg}`);
|
|
38
|
+
const warn = (msg) => console.log(` ${c.yellow}⚠${c.reset} ${msg}`);
|
|
39
|
+
const info = (msg) => console.log(` ${c.cyan}ℹ${c.reset} ${msg}`);
|
|
40
|
+
const step = (n, total, msg) =>
|
|
41
|
+
console.log(
|
|
42
|
+
`\n ${c.dim}[${n}/${total}]${c.reset} ${c.bold}${msg}${c.reset}`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// ─── Check if a CLI tool is available ────────────────────────────────────
|
|
46
|
+
function hasCommand(cmd) {
|
|
47
|
+
try {
|
|
48
|
+
execSync(`${cmd} --version`, { stdio: "ignore" });
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Run a command with live output ──────────────────────────────────────
|
|
56
|
+
function exec(cmd, cwd) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const child = spawn(cmd, {
|
|
59
|
+
cwd,
|
|
60
|
+
shell: true,
|
|
61
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let stderr = "";
|
|
65
|
+
child.stdout?.on("data", () => {}); // consume but don't print
|
|
66
|
+
child.stderr?.on("data", (d) => (stderr += d.toString()));
|
|
67
|
+
|
|
68
|
+
child.on("close", (code) => {
|
|
69
|
+
if (code === 0) resolve();
|
|
70
|
+
else reject(new Error(`Command failed: ${cmd}\n${stderr}`));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Parse CLI arguments ─────────────────────────────────────────────────
|
|
76
|
+
function parseArgs(argv) {
|
|
77
|
+
const args = {};
|
|
78
|
+
let projectName = null;
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < argv.length; i++) {
|
|
81
|
+
const arg = argv[i];
|
|
82
|
+
if (arg.startsWith("--")) {
|
|
83
|
+
const [key, val] = arg.slice(2).split("=");
|
|
84
|
+
args[key] = val ?? true;
|
|
85
|
+
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
86
|
+
// Single-dash flags: -h, -v
|
|
87
|
+
for (const ch of arg.slice(1)) {
|
|
88
|
+
args[ch] = true;
|
|
89
|
+
}
|
|
90
|
+
} else if (!projectName) {
|
|
91
|
+
projectName = arg;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { projectName, ...args };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Interactive prompts ─────────────────────────────────────────────────
|
|
99
|
+
async function getConfig(args) {
|
|
100
|
+
// Check if running non-interactively
|
|
101
|
+
const isCI =
|
|
102
|
+
args["api-key"] && args["api-secret"] && args["project-id"];
|
|
103
|
+
|
|
104
|
+
if (isCI) {
|
|
105
|
+
return {
|
|
106
|
+
projectName: args.projectName || "my-shopify-app",
|
|
107
|
+
appName: args["app-name"] || args.projectName || "My Shopify App",
|
|
108
|
+
apiKey: args["api-key"],
|
|
109
|
+
apiSecret: args["api-secret"],
|
|
110
|
+
scopes: args.scopes || "read_products",
|
|
111
|
+
projectId: args["project-id"],
|
|
112
|
+
appUrl: `https://${args["project-id"]}.web.app`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log();
|
|
117
|
+
console.log(
|
|
118
|
+
` ${c.bgGreen}${c.white}${c.bold} SHOPIFY + FIREBASE ${c.reset} Create a new Shopify app`,
|
|
119
|
+
);
|
|
120
|
+
console.log();
|
|
121
|
+
|
|
122
|
+
const questions = [];
|
|
123
|
+
|
|
124
|
+
if (!args.projectName) {
|
|
125
|
+
questions.push({
|
|
126
|
+
type: "text",
|
|
127
|
+
name: "projectName",
|
|
128
|
+
message: "Project directory name",
|
|
129
|
+
initial: "my-shopify-app",
|
|
130
|
+
validate: (v) => {
|
|
131
|
+
if (!v.trim()) return "Required";
|
|
132
|
+
if (/[^a-zA-Z0-9._-]/.test(v)) return "Use only letters, numbers, dots, hyphens, underscores";
|
|
133
|
+
return true;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
questions.push(
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
name: "appName",
|
|
142
|
+
message: "App name (shown in Shopify admin)",
|
|
143
|
+
initial: args.projectName || "My Shopify App",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
name: "apiKey",
|
|
148
|
+
message: `Shopify API Key ${c.dim}(Partner Dashboard → App → Client ID)${c.reset}`,
|
|
149
|
+
validate: (v) => (v.trim() ? true : "Required — get it from partners.shopify.com"),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: "password",
|
|
153
|
+
name: "apiSecret",
|
|
154
|
+
message: "Shopify API Secret",
|
|
155
|
+
validate: (v) => (v.trim() ? true : "Required"),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: "text",
|
|
159
|
+
name: "scopes",
|
|
160
|
+
message: "API Scopes",
|
|
161
|
+
initial: "read_products",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: "text",
|
|
165
|
+
name: "projectId",
|
|
166
|
+
message: `Firebase Project ID ${c.dim}(Firebase Console → Project Settings)${c.reset}`,
|
|
167
|
+
validate: (v) => (v.trim() ? true : "Required — create one at console.firebase.google.com"),
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const onCancel = () => {
|
|
172
|
+
console.log("\n Cancelled.\n");
|
|
173
|
+
process.exit(0);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const answers = await prompts(questions, { onCancel });
|
|
177
|
+
|
|
178
|
+
const projectName = args.projectName || answers.projectName;
|
|
179
|
+
const projectId = answers.projectId;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
projectName,
|
|
183
|
+
appName: answers.appName || projectName,
|
|
184
|
+
apiKey: answers.apiKey,
|
|
185
|
+
apiSecret: answers.apiSecret,
|
|
186
|
+
scopes: answers.scopes || "read_products",
|
|
187
|
+
projectId,
|
|
188
|
+
appUrl: `https://${projectId}.web.app`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Scaffold files from templates ───────────────────────────────────────
|
|
193
|
+
function scaffold(outputDir, config) {
|
|
194
|
+
// Recursively copy templates directory
|
|
195
|
+
copyDirSync(TEMPLATES_DIR, outputDir);
|
|
196
|
+
|
|
197
|
+
// Rename dotfiles (npm strips leading dots on publish)
|
|
198
|
+
const renames = [
|
|
199
|
+
["gitignore", ".gitignore"],
|
|
200
|
+
["env.example", ".env.example"],
|
|
201
|
+
];
|
|
202
|
+
for (const [from, to] of renames) {
|
|
203
|
+
const src = path.join(outputDir, from);
|
|
204
|
+
const dest = path.join(outputDir, to);
|
|
205
|
+
if (fs.existsSync(src)) {
|
|
206
|
+
fs.renameSync(src, dest);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Variable substitution map
|
|
211
|
+
const vars = {
|
|
212
|
+
"{{APP_NAME}}": config.appName,
|
|
213
|
+
"{{API_KEY}}": config.apiKey,
|
|
214
|
+
"{{API_SECRET}}": config.apiSecret,
|
|
215
|
+
"{{SCOPES}}": config.scopes,
|
|
216
|
+
"{{PROJECT_ID}}": config.projectId,
|
|
217
|
+
"{{APP_URL}}": config.appUrl,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Files that need variable substitution
|
|
221
|
+
const templateFiles = [
|
|
222
|
+
"shopify.app.toml",
|
|
223
|
+
"web/index.html",
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
for (const relPath of templateFiles) {
|
|
227
|
+
const filePath = path.join(outputDir, relPath);
|
|
228
|
+
if (!fs.existsSync(filePath)) continue;
|
|
229
|
+
let content = fs.readFileSync(filePath, "utf8");
|
|
230
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
231
|
+
content = content.replaceAll(key, val);
|
|
232
|
+
}
|
|
233
|
+
fs.writeFileSync(filePath, content);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Generate functions/.env (secrets — not a template to avoid leaks)
|
|
237
|
+
const envContent = [
|
|
238
|
+
`SHOPIFY_API_KEY=${config.apiKey}`,
|
|
239
|
+
`SHOPIFY_API_SECRET=${config.apiSecret}`,
|
|
240
|
+
`SCOPES=${config.scopes}`,
|
|
241
|
+
`APP_URL=${config.appUrl}`,
|
|
242
|
+
"",
|
|
243
|
+
].join("\n");
|
|
244
|
+
fs.writeFileSync(path.join(outputDir, "functions", ".env"), envContent);
|
|
245
|
+
|
|
246
|
+
// Generate .firebaserc
|
|
247
|
+
const firebaserc = JSON.stringify(
|
|
248
|
+
{ projects: { default: config.projectId } },
|
|
249
|
+
null,
|
|
250
|
+
2,
|
|
251
|
+
);
|
|
252
|
+
fs.writeFileSync(path.join(outputDir, ".firebaserc"), firebaserc + "\n");
|
|
253
|
+
|
|
254
|
+
// Generate root package.json (required by Shopify CLI for `shopify app deploy`)
|
|
255
|
+
const rootPkg = JSON.stringify(
|
|
256
|
+
{ name: config.name, private: true },
|
|
257
|
+
null,
|
|
258
|
+
2,
|
|
259
|
+
);
|
|
260
|
+
fs.writeFileSync(path.join(outputDir, "package.json"), rootPkg + "\n");
|
|
261
|
+
|
|
262
|
+
// Count files
|
|
263
|
+
let count = 0;
|
|
264
|
+
countFiles(outputDir, () => count++);
|
|
265
|
+
return count;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function copyDirSync(src, dest) {
|
|
269
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
270
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
271
|
+
const srcPath = path.join(src, entry.name);
|
|
272
|
+
const destPath = path.join(dest, entry.name);
|
|
273
|
+
if (entry.isDirectory()) {
|
|
274
|
+
copyDirSync(srcPath, destPath);
|
|
275
|
+
} else {
|
|
276
|
+
fs.copyFileSync(srcPath, destPath);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function countFiles(dir, cb) {
|
|
282
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
283
|
+
if (entry.isDirectory()) {
|
|
284
|
+
countFiles(path.join(dir, entry.name), cb);
|
|
285
|
+
} else {
|
|
286
|
+
cb();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─── Main flow ───────────────────────────────────────────────────────────
|
|
292
|
+
export async function run(argv) {
|
|
293
|
+
const args = parseArgs(argv);
|
|
294
|
+
|
|
295
|
+
// Show help
|
|
296
|
+
if (args.help || args.h) {
|
|
297
|
+
printHelp();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Show version
|
|
302
|
+
if (args.version || args.v) {
|
|
303
|
+
const pkg = JSON.parse(
|
|
304
|
+
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"),
|
|
305
|
+
);
|
|
306
|
+
console.log(pkg.version);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Collect config
|
|
311
|
+
const config = await getConfig(args);
|
|
312
|
+
const outputDir = path.resolve(process.cwd(), config.projectName);
|
|
313
|
+
|
|
314
|
+
// Check if directory exists
|
|
315
|
+
if (fs.existsSync(outputDir)) {
|
|
316
|
+
const { overwrite } = await prompts({
|
|
317
|
+
type: "confirm",
|
|
318
|
+
name: "overwrite",
|
|
319
|
+
message: `Directory "${config.projectName}" already exists. Overwrite?`,
|
|
320
|
+
initial: false,
|
|
321
|
+
});
|
|
322
|
+
if (!overwrite) {
|
|
323
|
+
console.log("\n Cancelled.\n");
|
|
324
|
+
process.exit(0);
|
|
325
|
+
}
|
|
326
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const totalSteps = 6;
|
|
330
|
+
|
|
331
|
+
// ── Step 1: Scaffold ──────────────────────────────────────────────
|
|
332
|
+
step(1, totalSteps, "Scaffolding project...");
|
|
333
|
+
const fileCount = scaffold(outputDir, config);
|
|
334
|
+
ok(`Created ${fileCount} files in ${c.cyan}${config.projectName}/${c.reset}`);
|
|
335
|
+
|
|
336
|
+
// ── Step 2: Install dependencies ──────────────────────────────────
|
|
337
|
+
step(2, totalSteps, "Installing dependencies...");
|
|
338
|
+
const functionsDir = path.join(outputDir, "functions");
|
|
339
|
+
try {
|
|
340
|
+
await exec("npm install", functionsDir);
|
|
341
|
+
ok("Dependencies installed");
|
|
342
|
+
} catch (e) {
|
|
343
|
+
warn(`npm install failed — run manually: cd ${config.projectName}/functions && npm install`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── Step 3: Build TypeScript ──────────────────────────────────────
|
|
347
|
+
step(3, totalSteps, "Building TypeScript...");
|
|
348
|
+
try {
|
|
349
|
+
await exec("npm run build", functionsDir);
|
|
350
|
+
ok("TypeScript compiled successfully");
|
|
351
|
+
} catch (e) {
|
|
352
|
+
warn("Build failed — run manually: cd functions && npm run build");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── Step 4: Firebase provisioning ───────────────────────────────────
|
|
356
|
+
step(4, totalSteps, "Setting up Firebase...");
|
|
357
|
+
if (hasCommand("firebase")) {
|
|
358
|
+
const isCI = args["api-key"] && args["api-secret"] && args["project-id"];
|
|
359
|
+
await provisionFirebase(config, {
|
|
360
|
+
skipProvision: !!args["skip-provision"],
|
|
361
|
+
firestoreRegion: args["firestore-region"],
|
|
362
|
+
nonInteractive: isCI,
|
|
363
|
+
cwd: outputDir,
|
|
364
|
+
});
|
|
365
|
+
} else {
|
|
366
|
+
warn("Firebase CLI not found. Install: npm i -g firebase-tools");
|
|
367
|
+
info(`Then run: cd ${config.projectName} && firebase use ${config.projectId}`);
|
|
368
|
+
info("After installing, re-run with --skip-provision to skip provisioning");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Step 5: Shopify CLI check ─────────────────────────────────────
|
|
372
|
+
step(5, totalSteps, "Checking Shopify CLI...");
|
|
373
|
+
if (hasCommand("shopify")) {
|
|
374
|
+
ok("Shopify CLI detected — you can use `shopify app dev` for local development");
|
|
375
|
+
} else {
|
|
376
|
+
warn("Shopify CLI not found. Install: npm i -g @shopify/cli");
|
|
377
|
+
info("Optional — you can also develop with Firebase emulators");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Step 6: Initialize git ────────────────────────────────────────
|
|
381
|
+
step(6, totalSteps, "Initializing git...");
|
|
382
|
+
if (hasCommand("git")) {
|
|
383
|
+
try {
|
|
384
|
+
await exec("git init", outputDir);
|
|
385
|
+
await exec("git add -A", outputDir);
|
|
386
|
+
await exec('git commit -m "Initial scaffold from create-shopify-firebase-app"', outputDir);
|
|
387
|
+
ok("Git repository initialized with first commit");
|
|
388
|
+
} catch {
|
|
389
|
+
warn("Git init failed — initialize manually if needed");
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
warn("Git not found — skipping repository initialization");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── Done! ─────────────────────────────────────────────────────────
|
|
396
|
+
printSuccess(config);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── Success output ──────────────────────────────────────────────────────
|
|
400
|
+
function printSuccess(config) {
|
|
401
|
+
console.log();
|
|
402
|
+
console.log(
|
|
403
|
+
` ${c.bgGreen}${c.white}${c.bold} SUCCESS ${c.reset} Your Shopify + Firebase app is ready!`,
|
|
404
|
+
);
|
|
405
|
+
console.log();
|
|
406
|
+
console.log(` ${c.bold}Next steps:${c.reset}`);
|
|
407
|
+
console.log();
|
|
408
|
+
console.log(` ${c.cyan}cd ${config.projectName}${c.reset}`);
|
|
409
|
+
console.log(` ${c.cyan}firebase deploy${c.reset}`);
|
|
410
|
+
console.log();
|
|
411
|
+
console.log(` ${c.bold}Then install on your dev store:${c.reset}`);
|
|
412
|
+
console.log();
|
|
413
|
+
console.log(
|
|
414
|
+
` ${c.cyan}${config.appUrl}/auth?shop=YOUR-STORE.myshopify.com${c.reset}`,
|
|
415
|
+
);
|
|
416
|
+
console.log();
|
|
417
|
+
console.log(` ${c.bold}Or use Shopify CLI for local development:${c.reset}`);
|
|
418
|
+
console.log();
|
|
419
|
+
console.log(` ${c.cyan}shopify app dev${c.reset}`);
|
|
420
|
+
console.log();
|
|
421
|
+
console.log(` ${c.dim}─────────────────────────────────────────${c.reset}`);
|
|
422
|
+
console.log();
|
|
423
|
+
console.log(` ${c.dim}App URL: ${config.appUrl}${c.reset}`);
|
|
424
|
+
console.log(` ${c.dim}Firebase: ${config.projectId}${c.reset}`);
|
|
425
|
+
console.log(` ${c.dim}Scopes: ${config.scopes}${c.reset}`);
|
|
426
|
+
console.log();
|
|
427
|
+
console.log(` ${c.dim}Docs: https://github.com/mksd0398/create-shopify-firebase-app${c.reset}`);
|
|
428
|
+
console.log();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─── Help output ─────────────────────────────────────────────────────────
|
|
432
|
+
function printHelp() {
|
|
433
|
+
console.log(`
|
|
434
|
+
${c.bold}create-shopify-firebase-app${c.reset}
|
|
435
|
+
|
|
436
|
+
Create Shopify apps powered by Firebase.
|
|
437
|
+
Serverless, lightweight, zero-framework.
|
|
438
|
+
|
|
439
|
+
${c.bold}Usage:${c.reset}
|
|
440
|
+
|
|
441
|
+
${c.cyan}npx create-shopify-firebase-app${c.reset} [project-name] [options]
|
|
442
|
+
|
|
443
|
+
${c.bold}Options:${c.reset}
|
|
444
|
+
|
|
445
|
+
--api-key=KEY Shopify API Key (client_id)
|
|
446
|
+
--api-secret=SECRET Shopify API Secret
|
|
447
|
+
--project-id=ID Firebase Project ID
|
|
448
|
+
--scopes=SCOPES API scopes (default: read_products)
|
|
449
|
+
--app-name=NAME App name shown in Shopify admin
|
|
450
|
+
--help, -h Show this help
|
|
451
|
+
--version, -v Show version
|
|
452
|
+
|
|
453
|
+
${c.bold}Firebase provisioning:${c.reset}
|
|
454
|
+
|
|
455
|
+
--skip-provision Skip Firebase service provisioning
|
|
456
|
+
--firestore-region=LOC Firestore region (e.g. asia-south1, us-central1)
|
|
457
|
+
|
|
458
|
+
${c.bold}Examples:${c.reset}
|
|
459
|
+
|
|
460
|
+
${c.dim}# Interactive mode (prompts for config + provisions Firebase)${c.reset}
|
|
461
|
+
npx create-shopify-firebase-app
|
|
462
|
+
|
|
463
|
+
${c.dim}# With project name${c.reset}
|
|
464
|
+
npx create-shopify-firebase-app my-app
|
|
465
|
+
|
|
466
|
+
${c.dim}# Non-interactive (CI/CD)${c.reset}
|
|
467
|
+
npx create-shopify-firebase-app my-app \\
|
|
468
|
+
--api-key=abc123 \\
|
|
469
|
+
--api-secret=secret \\
|
|
470
|
+
--project-id=my-firebase-project
|
|
471
|
+
|
|
472
|
+
${c.dim}# With Firebase provisioning in CI${c.reset}
|
|
473
|
+
npx create-shopify-firebase-app my-app \\
|
|
474
|
+
--api-key=abc123 \\
|
|
475
|
+
--api-secret=secret \\
|
|
476
|
+
--project-id=my-firebase-project \\
|
|
477
|
+
--firestore-region=asia-south1
|
|
478
|
+
|
|
479
|
+
${c.bold}What you get:${c.reset}
|
|
480
|
+
|
|
481
|
+
✔ Express + TypeScript Cloud Functions (OAuth, webhooks, GDPR)
|
|
482
|
+
✔ Firestore for sessions and app data
|
|
483
|
+
✔ App Bridge embedded admin dashboard (vanilla HTML/JS)
|
|
484
|
+
✔ Theme App Extension for storefront UI
|
|
485
|
+
✔ Firebase Hosting (free tier covers most apps)
|
|
486
|
+
✔ Auto-provisioning: Firestore, Web App, Hosting (interactive)
|
|
487
|
+
`);
|
|
488
|
+
}
|