create-shopify-firebase-app 1.3.0 → 2.0.1
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/lib/index.js +704 -456
- package/package.json +2 -2
- package/templates/js/functions/package.json +24 -0
- package/templates/js/functions/src/admin-api.js +292 -0
- package/templates/js/functions/src/auth.js +147 -0
- package/templates/js/functions/src/config.js +14 -0
- package/templates/js/functions/src/firebase.js +12 -0
- package/templates/js/functions/src/index.js +93 -0
- package/templates/js/functions/src/proxy.js +60 -0
- package/templates/js/functions/src/verify-token.js +39 -0
- package/templates/js/functions/src/webhooks.js +111 -0
- package/templates/{firebase.json → shared/firebase.json} +1 -0
- package/templates/shopify.app.toml +1 -1
- package/templates/ts/functions/src/admin-api.ts +290 -0
- package/templates/web/css/app.css +1287 -47
- package/templates/web/index.html +84 -49
- package/templates/web/js/app.js +177 -0
- package/templates/web/js/pages/home.js +90 -0
- package/templates/web/js/pages/polaris-demo.js +190 -0
- package/templates/web/js/pages/products.js +319 -0
- package/templates/web/js/pages/settings.js +241 -0
- package/templates/web/polaris.html +1149 -0
- package/templates/web/products.html +86 -0
- package/templates/web/settings.html +40 -0
- package/templates/functions/src/admin-api.ts +0 -125
- package/templates/web/js/bridge.js +0 -98
- /package/templates/{env.example → shared/env.example} +0 -0
- /package/templates/{extensions → shared/extensions}/theme-block/assets/app-block.css +0 -0
- /package/templates/{extensions → shared/extensions}/theme-block/assets/app-block.js +0 -0
- /package/templates/{extensions → shared/extensions}/theme-block/blocks/app-block.liquid +0 -0
- /package/templates/{extensions → shared/extensions}/theme-block/locales/en.default.json +0 -0
- /package/templates/{extensions → shared/extensions}/theme-block/shopify.extension.toml +0 -0
- /package/templates/{firestore.indexes.json → shared/firestore.indexes.json} +0 -0
- /package/templates/{firestore.rules → shared/firestore.rules} +0 -0
- /package/templates/{gitignore → shared/gitignore} +0 -0
- /package/templates/{functions → ts/functions}/package.json +0 -0
- /package/templates/{functions → ts/functions}/src/auth.ts +0 -0
- /package/templates/{functions → ts/functions}/src/config.ts +0 -0
- /package/templates/{functions → ts/functions}/src/firebase.ts +0 -0
- /package/templates/{functions → ts/functions}/src/index.ts +0 -0
- /package/templates/{functions → ts/functions}/src/proxy.ts +0 -0
- /package/templates/{functions → ts/functions}/src/verify-token.ts +0 -0
- /package/templates/{functions → ts/functions}/src/webhooks.ts +0 -0
- /package/templates/{functions → ts/functions}/tsconfig.json +0 -0
package/lib/index.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* create-shopify-firebase-app — CLI core
|
|
2
|
+
* create-shopify-firebase-app — CLI core (v2)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Full-stack scaffolding for Shopify + Firebase apps.
|
|
5
|
+
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* 1. App type selection (extension-only vs full-stack Firebase)
|
|
8
|
+
* 2. Language selection (TypeScript / JavaScript)
|
|
9
|
+
* 3. Project name + app name
|
|
10
|
+
* 4. Scaffold files (multi-page frontend + backend)
|
|
11
|
+
* 5. Firebase setup (login, create/select project, provision)
|
|
12
|
+
* 6. Shopify app creation (login, create/link app via CLI)
|
|
13
|
+
* 7. Configure URLs + credentials
|
|
14
|
+
* 8. Install dependencies + build
|
|
15
|
+
* 9. Git init
|
|
16
|
+
* 10. Ready to deploy!
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
import fs from "node:fs";
|
|
@@ -37,13 +44,20 @@ const c = {
|
|
|
37
44
|
const ok = (msg) => console.log(` ${c.green}✔${c.reset} ${msg}`);
|
|
38
45
|
const warn = (msg) => console.log(` ${c.yellow}⚠${c.reset} ${msg}`);
|
|
39
46
|
const info = (msg) => console.log(` ${c.cyan}ℹ${c.reset} ${msg}`);
|
|
47
|
+
const fail = (msg) => console.log(` ${c.red}✘${c.reset} ${msg}`);
|
|
40
48
|
const section = (title) => {
|
|
41
49
|
console.log();
|
|
42
50
|
console.log(` ${c.cyan}===${c.reset} ${c.bold}${title}${c.reset} ${c.cyan}===${c.reset}`);
|
|
43
51
|
console.log();
|
|
44
52
|
};
|
|
45
53
|
|
|
46
|
-
|
|
54
|
+
const onCancel = () => {
|
|
55
|
+
console.log("\n Cancelled.\n");
|
|
56
|
+
process.exit(0);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ─── Shell helpers ───────────────────────────────────────────────────────
|
|
60
|
+
|
|
47
61
|
function hasCommand(cmd) {
|
|
48
62
|
try {
|
|
49
63
|
execSync(`${cmd} --version`, { stdio: "ignore" });
|
|
@@ -53,19 +67,12 @@ function hasCommand(cmd) {
|
|
|
53
67
|
}
|
|
54
68
|
}
|
|
55
69
|
|
|
56
|
-
// ─── Run a command with live output ──────────────────────────────────────
|
|
57
70
|
function exec(cmd, cwd) {
|
|
58
71
|
return new Promise((resolve, reject) => {
|
|
59
|
-
const child = spawn(cmd, {
|
|
60
|
-
cwd,
|
|
61
|
-
shell: true,
|
|
62
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
63
|
-
});
|
|
64
|
-
|
|
72
|
+
const child = spawn(cmd, { cwd, shell: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
65
73
|
let stderr = "";
|
|
66
|
-
child.stdout?.on("data", () => {});
|
|
74
|
+
child.stdout?.on("data", () => {});
|
|
67
75
|
child.stderr?.on("data", (d) => (stderr += d.toString()));
|
|
68
|
-
|
|
69
76
|
child.on("close", (code) => {
|
|
70
77
|
if (code === 0) resolve();
|
|
71
78
|
else reject(new Error(`Command failed: ${cmd}\n${stderr}`));
|
|
@@ -73,30 +80,16 @@ function exec(cmd, cwd) {
|
|
|
73
80
|
});
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const [key, val] = arg.slice(2).split("=");
|
|
85
|
-
args[key] = val ?? true;
|
|
86
|
-
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
87
|
-
// Single-dash flags: -h, -v
|
|
88
|
-
for (const ch of arg.slice(1)) {
|
|
89
|
-
args[ch] = true;
|
|
90
|
-
}
|
|
91
|
-
} else if (!projectName) {
|
|
92
|
-
projectName = arg;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { projectName, ...args };
|
|
83
|
+
function execInteractive(cmd, cwd) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const child = spawn(cmd, { cwd, shell: true, stdio: "inherit" });
|
|
86
|
+
child.on("close", (code) => {
|
|
87
|
+
if (code === 0) resolve();
|
|
88
|
+
else reject(new Error(`Command exited with code ${code}`));
|
|
89
|
+
});
|
|
90
|
+
});
|
|
97
91
|
}
|
|
98
92
|
|
|
99
|
-
// ─── Open URL in browser (cross-platform) ───────────────────────────────
|
|
100
93
|
function openBrowser(url) {
|
|
101
94
|
const platform = process.platform;
|
|
102
95
|
try {
|
|
@@ -108,7 +101,16 @@ function openBrowser(url) {
|
|
|
108
101
|
}
|
|
109
102
|
}
|
|
110
103
|
|
|
111
|
-
|
|
104
|
+
function parseTomlField(tomlPath, field) {
|
|
105
|
+
try {
|
|
106
|
+
const content = fs.readFileSync(tomlPath, "utf8");
|
|
107
|
+
const match = content.match(new RegExp(`${field}\\s*=\\s*"([^"]+)"`));
|
|
108
|
+
return match ? match[1] : null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
112
114
|
function listFirebaseProjects() {
|
|
113
115
|
try {
|
|
114
116
|
const output = execSync("firebase projects:list --json", {
|
|
@@ -128,88 +130,269 @@ function listFirebaseProjects() {
|
|
|
128
130
|
return [];
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
function execInteractive(cmd, cwd) {
|
|
133
|
-
return new Promise((resolve, reject) => {
|
|
134
|
-
const child = spawn(cmd, {
|
|
135
|
-
cwd,
|
|
136
|
-
shell: true,
|
|
137
|
-
stdio: "inherit",
|
|
138
|
-
});
|
|
139
|
-
child.on("close", (code) => {
|
|
140
|
-
if (code === 0) resolve();
|
|
141
|
-
else reject(new Error(`Command exited with code ${code}`));
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ─── Check if logged into Shopify CLI ───────────────────────────────────
|
|
147
|
-
function isShopifyLoggedIn() {
|
|
133
|
+
async function createFirebaseProject(projectId, displayName) {
|
|
148
134
|
try {
|
|
149
|
-
|
|
150
|
-
// but `shopify app info` does — use a quick check
|
|
151
|
-
const out = execSync("shopify auth login --help", {
|
|
152
|
-
encoding: "utf8",
|
|
153
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
154
|
-
});
|
|
135
|
+
await exec(`firebase projects:create "${projectId}" --display-name "${displayName}"`);
|
|
155
136
|
return true;
|
|
156
137
|
} catch {
|
|
157
138
|
return false;
|
|
158
139
|
}
|
|
159
140
|
}
|
|
160
141
|
|
|
161
|
-
// ─── Parse
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
142
|
+
// ─── Parse CLI arguments ─────────────────────────────────────────────────
|
|
143
|
+
function parseArgs(argv) {
|
|
144
|
+
const args = {};
|
|
145
|
+
let projectName = null;
|
|
146
|
+
|
|
147
|
+
for (let i = 0; i < argv.length; i++) {
|
|
148
|
+
const arg = argv[i];
|
|
149
|
+
if (arg.startsWith("--")) {
|
|
150
|
+
const [key, val] = arg.slice(2).split("=");
|
|
151
|
+
args[key] = val ?? true;
|
|
152
|
+
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
153
|
+
for (const ch of arg.slice(1)) args[ch] = true;
|
|
154
|
+
} else if (!projectName) {
|
|
155
|
+
projectName = arg;
|
|
156
|
+
}
|
|
169
157
|
}
|
|
158
|
+
|
|
159
|
+
return { projectName, ...args };
|
|
170
160
|
}
|
|
171
161
|
|
|
172
|
-
// ───
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
162
|
+
// ─── File helpers ────────────────────────────────────────────────────────
|
|
163
|
+
function copyDirSync(src, dest) {
|
|
164
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
165
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
166
|
+
const srcPath = path.join(src, entry.name);
|
|
167
|
+
const destPath = path.join(dest, entry.name);
|
|
168
|
+
if (entry.isDirectory()) copyDirSync(srcPath, destPath);
|
|
169
|
+
else fs.copyFileSync(srcPath, destPath);
|
|
179
170
|
}
|
|
180
171
|
}
|
|
181
172
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
173
|
+
function countFiles(dir) {
|
|
174
|
+
let count = 0;
|
|
175
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
176
|
+
if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
|
|
177
|
+
else count++;
|
|
178
|
+
}
|
|
179
|
+
return count;
|
|
180
|
+
}
|
|
187
181
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
182
|
+
function substituteVars(filePath, vars) {
|
|
183
|
+
if (!fs.existsSync(filePath)) return;
|
|
184
|
+
let content = fs.readFileSync(filePath, "utf8");
|
|
185
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
186
|
+
content = content.replaceAll(key, val);
|
|
187
|
+
}
|
|
188
|
+
fs.writeFileSync(filePath, content);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Scaffold ────────────────────────────────────────────────────────────
|
|
192
|
+
function scaffold(outputDir, config) {
|
|
193
|
+
// 1. Copy shared files (firebase.json, firestore, gitignore, extensions)
|
|
194
|
+
copyDirSync(path.join(TEMPLATES_DIR, "shared"), outputDir);
|
|
195
|
+
|
|
196
|
+
// 2. Copy web frontend (multi-page with App Bridge + Polaris)
|
|
197
|
+
copyDirSync(path.join(TEMPLATES_DIR, "web"), path.join(outputDir, "web"));
|
|
198
|
+
|
|
199
|
+
// 3. Copy functions backend (TS or JS based on language choice)
|
|
200
|
+
const lang = config.language === "javascript" ? "js" : "ts";
|
|
201
|
+
copyDirSync(
|
|
202
|
+
path.join(TEMPLATES_DIR, lang, "functions"),
|
|
203
|
+
path.join(outputDir, "functions"),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// 4. Copy shopify.app.toml
|
|
207
|
+
fs.copyFileSync(
|
|
208
|
+
path.join(TEMPLATES_DIR, "shopify.app.toml"),
|
|
209
|
+
path.join(outputDir, "shopify.app.toml"),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// 5. Rename dotfiles (npm strips leading dots on publish)
|
|
213
|
+
const renames = [
|
|
214
|
+
["gitignore", ".gitignore"],
|
|
215
|
+
["env.example", ".env.example"],
|
|
216
|
+
];
|
|
217
|
+
for (const [from, to] of renames) {
|
|
218
|
+
const src = path.join(outputDir, from);
|
|
219
|
+
const dest = path.join(outputDir, to);
|
|
220
|
+
if (fs.existsSync(src)) fs.renameSync(src, dest);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 6. Variable substitution
|
|
224
|
+
const vars = {
|
|
225
|
+
"{{APP_NAME}}": config.appName,
|
|
226
|
+
"{{API_KEY}}": config.apiKey || "",
|
|
227
|
+
"{{API_SECRET}}": config.apiSecret || "",
|
|
228
|
+
"{{SCOPES}}": config.scopes,
|
|
229
|
+
"{{PROJECT_ID}}": config.projectId || "",
|
|
230
|
+
"{{APP_URL}}": config.appUrl || "",
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const templateFiles = [
|
|
234
|
+
"shopify.app.toml",
|
|
235
|
+
"web/index.html",
|
|
236
|
+
"web/products.html",
|
|
237
|
+
"web/settings.html",
|
|
238
|
+
"web/polaris.html",
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
for (const relPath of templateFiles) {
|
|
242
|
+
substituteVars(path.join(outputDir, relPath), vars);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 7. Generate functions/.env
|
|
246
|
+
const envContent = [
|
|
247
|
+
`SHOPIFY_API_KEY=${config.apiKey || ""}`,
|
|
248
|
+
`SHOPIFY_API_SECRET=${config.apiSecret || ""}`,
|
|
249
|
+
`SCOPES=${config.scopes}`,
|
|
250
|
+
`APP_URL=${config.appUrl || ""}`,
|
|
251
|
+
"",
|
|
252
|
+
].join("\n");
|
|
253
|
+
fs.writeFileSync(path.join(outputDir, "functions", ".env"), envContent);
|
|
254
|
+
|
|
255
|
+
// 8. Generate .firebaserc
|
|
256
|
+
if (config.projectId) {
|
|
257
|
+
const firebaserc = JSON.stringify(
|
|
258
|
+
{ projects: { default: config.projectId } },
|
|
259
|
+
null,
|
|
260
|
+
2,
|
|
261
|
+
);
|
|
262
|
+
fs.writeFileSync(path.join(outputDir, ".firebaserc"), firebaserc + "\n");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 9. Generate root package.json
|
|
266
|
+
const rootPkg = JSON.stringify(
|
|
267
|
+
{ name: config.projectName, private: true },
|
|
268
|
+
null,
|
|
269
|
+
2,
|
|
270
|
+
);
|
|
271
|
+
fs.writeFileSync(path.join(outputDir, "package.json"), rootPkg + "\n");
|
|
272
|
+
|
|
273
|
+
return countFiles(outputDir);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Update credentials after Shopify app creation ───────────────────────
|
|
277
|
+
function updateCredentials(outputDir, config) {
|
|
278
|
+
const vars = {
|
|
279
|
+
"{{API_KEY}}": config.apiKey || "",
|
|
280
|
+
"{{API_SECRET}}": config.apiSecret || "",
|
|
281
|
+
"{{APP_URL}}": config.appUrl || "",
|
|
203
282
|
};
|
|
204
283
|
|
|
284
|
+
// Update shopify.app.toml
|
|
285
|
+
substituteVars(path.join(outputDir, "shopify.app.toml"), vars);
|
|
286
|
+
|
|
287
|
+
// Update functions/.env
|
|
288
|
+
const envContent = [
|
|
289
|
+
`SHOPIFY_API_KEY=${config.apiKey || ""}`,
|
|
290
|
+
`SHOPIFY_API_SECRET=${config.apiSecret || ""}`,
|
|
291
|
+
`SCOPES=${config.scopes}`,
|
|
292
|
+
`APP_URL=${config.appUrl || ""}`,
|
|
293
|
+
"",
|
|
294
|
+
].join("\n");
|
|
295
|
+
fs.writeFileSync(path.join(outputDir, "functions", ".env"), envContent);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
299
|
+
// ─── MAIN FLOW ───────────────────────────────────────────────────────────
|
|
300
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
301
|
+
|
|
302
|
+
export async function run(argv) {
|
|
303
|
+
const args = parseArgs(argv);
|
|
304
|
+
|
|
305
|
+
// ── Handle flags ──────────────────────────────────────────────────
|
|
306
|
+
if (args.help || args.h) { printHelp(); return; }
|
|
307
|
+
if (args.version || args.v) {
|
|
308
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
309
|
+
console.log(pkg.version);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── Handle --distribute ───────────────────────────────────────────
|
|
314
|
+
if (args.distribute) {
|
|
315
|
+
await distributeFlow();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── CI / non-interactive mode ─────────────────────────────────────
|
|
320
|
+
const isCI = args["api-key"] && args["api-secret"] && args["project-id"];
|
|
321
|
+
if (isCI) {
|
|
322
|
+
await runCI(args);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
327
|
+
// ── Interactive flow — guided project wizard ────
|
|
328
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
329
|
+
|
|
205
330
|
// ── Banner ────────────────────────────────────────────────────────
|
|
206
331
|
console.log();
|
|
207
332
|
console.log(` ${c.green}${c.bold}🛍️ + 🔥${c.reset} ${c.bold}create-shopify-firebase-app${c.reset}`);
|
|
208
|
-
console.log(` ${c.dim}
|
|
333
|
+
console.log(` ${c.dim}Build Shopify apps for free — serverless, zero-framework${c.reset}`);
|
|
334
|
+
|
|
335
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
336
|
+
section("Choose Your Template");
|
|
337
|
+
|
|
338
|
+
const { appTemplate } = await prompts({
|
|
339
|
+
type: "select",
|
|
340
|
+
name: "appTemplate",
|
|
341
|
+
message: "What would you like to create?",
|
|
342
|
+
choices: [
|
|
343
|
+
{
|
|
344
|
+
title: `${c.bold}Shopify + Firebase app${c.reset} ${c.dim}(full-stack serverless)${c.reset}`,
|
|
345
|
+
description: "Dashboard, product search, settings, Polaris components — ready to deploy",
|
|
346
|
+
value: "firebase",
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
title: `Extension-only app ${c.dim}(Shopify CLI)${c.reset}`,
|
|
350
|
+
description: "Theme extensions, checkout extensions — no backend needed",
|
|
351
|
+
value: "extension",
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
}, { onCancel });
|
|
355
|
+
|
|
356
|
+
// ── Extension-only: delegate to Shopify CLI ───────────────────────
|
|
357
|
+
if (appTemplate === "extension") {
|
|
358
|
+
console.log();
|
|
359
|
+
if (!hasCommand("shopify")) {
|
|
360
|
+
info("Installing Shopify CLI...");
|
|
361
|
+
try {
|
|
362
|
+
await exec("npm install -g @shopify/cli");
|
|
363
|
+
ok("Shopify CLI installed");
|
|
364
|
+
} catch {
|
|
365
|
+
fail("Could not install Shopify CLI");
|
|
366
|
+
info("Install manually: npm i -g @shopify/cli");
|
|
367
|
+
info("Then run: shopify app init");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
info("Launching Shopify CLI...");
|
|
372
|
+
console.log();
|
|
373
|
+
try {
|
|
374
|
+
await execInteractive("shopify app init");
|
|
375
|
+
} catch {
|
|
376
|
+
warn("Shopify CLI exited");
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
209
380
|
|
|
210
381
|
// ═══════════════════════════════════════════════════════════════════
|
|
211
|
-
section("
|
|
382
|
+
section("Project Setup");
|
|
212
383
|
|
|
384
|
+
// ── Language ──────────────────────────────────────────────────────
|
|
385
|
+
const { language } = await prompts({
|
|
386
|
+
type: "select",
|
|
387
|
+
name: "language",
|
|
388
|
+
message: "Language for Cloud Functions",
|
|
389
|
+
choices: [
|
|
390
|
+
{ title: `TypeScript ${c.dim}(recommended)${c.reset}`, value: "typescript" },
|
|
391
|
+
{ title: "JavaScript", value: "javascript" },
|
|
392
|
+
],
|
|
393
|
+
}, { onCancel });
|
|
394
|
+
|
|
395
|
+
// ── Project name ──────────────────────────────────────────────────
|
|
213
396
|
let projectName = args.projectName;
|
|
214
397
|
if (!projectName) {
|
|
215
398
|
const res = await prompts({
|
|
@@ -226,136 +409,15 @@ async function getConfig(args) {
|
|
|
226
409
|
projectName = res.projectName;
|
|
227
410
|
}
|
|
228
411
|
|
|
412
|
+
// ── App name ──────────────────────────────────────────────────────
|
|
229
413
|
const { appName } = await prompts({
|
|
230
414
|
type: "text",
|
|
231
415
|
name: "appName",
|
|
232
416
|
message: "App name (shown in Shopify admin)",
|
|
233
|
-
initial: projectName
|
|
234
|
-
}, { onCancel });
|
|
235
|
-
|
|
236
|
-
const { appType } = await prompts({
|
|
237
|
-
type: "select",
|
|
238
|
-
name: "appType",
|
|
239
|
-
message: "What kind of app are you building?",
|
|
240
|
-
choices: [
|
|
241
|
-
{ title: "Public app — list on the Shopify App Store", value: "public" },
|
|
242
|
-
{ title: "Custom app — built for a single store", value: "custom" },
|
|
243
|
-
],
|
|
244
|
-
}, { onCancel });
|
|
245
|
-
|
|
246
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
247
|
-
section("Shopify Setup");
|
|
248
|
-
|
|
249
|
-
const hasShopifyCli = hasCommand("shopify");
|
|
250
|
-
|
|
251
|
-
// Build choices based on what's available
|
|
252
|
-
const shopifyChoices = [];
|
|
253
|
-
if (hasShopifyCli) {
|
|
254
|
-
shopifyChoices.push(
|
|
255
|
-
{ title: "Create a new app via Shopify CLI", value: "cli-create" },
|
|
256
|
-
{ title: "Link an existing app via Shopify CLI", value: "cli-link" },
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
shopifyChoices.push(
|
|
260
|
-
{ title: `Enter credentials manually ${c.dim}(Client ID + Secret)${c.reset}`, value: "manual" },
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
const { shopifySetup } = await prompts({
|
|
264
|
-
type: "select",
|
|
265
|
-
name: "shopifySetup",
|
|
266
|
-
message: "How would you like to connect your Shopify app?",
|
|
267
|
-
choices: shopifyChoices,
|
|
417
|
+
initial: projectName.replace(/[-_.]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
268
418
|
}, { onCancel });
|
|
269
419
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (shopifySetup === "cli-create" || shopifySetup === "cli-link") {
|
|
273
|
-
// ── Use Shopify CLI to create/link the app ────────────────────
|
|
274
|
-
// We need a temp directory with a shopify.app.toml for the CLI to work
|
|
275
|
-
const tmpDir = path.resolve(process.cwd(), `__shopify_setup_${Date.now()}`);
|
|
276
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
// Ensure logged in
|
|
280
|
-
console.log();
|
|
281
|
-
info("Logging into Shopify CLI...");
|
|
282
|
-
info("A browser window will open — sign in to your Partner account.");
|
|
283
|
-
console.log();
|
|
284
|
-
await execInteractive("shopify auth login", tmpDir);
|
|
285
|
-
ok("Logged into Shopify");
|
|
286
|
-
|
|
287
|
-
// Run config link — it handles both create and link interactively
|
|
288
|
-
console.log();
|
|
289
|
-
if (shopifySetup === "cli-create") {
|
|
290
|
-
info("Creating a new Shopify app...");
|
|
291
|
-
info(`Select ${c.bold}"Create a new app"${c.reset} when prompted.`);
|
|
292
|
-
} else {
|
|
293
|
-
info("Linking an existing Shopify app...");
|
|
294
|
-
info("Select your app from the list when prompted.");
|
|
295
|
-
}
|
|
296
|
-
console.log();
|
|
297
|
-
await execInteractive("shopify app config link", tmpDir);
|
|
298
|
-
|
|
299
|
-
// Parse the generated TOML to get client_id
|
|
300
|
-
const tomlFiles = fs.readdirSync(tmpDir).filter((f) => f.endsWith(".toml"));
|
|
301
|
-
let parsedKey = null;
|
|
302
|
-
for (const f of tomlFiles) {
|
|
303
|
-
parsedKey = parseTomlClientId(path.join(tmpDir, f));
|
|
304
|
-
if (parsedKey) break;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (parsedKey) {
|
|
308
|
-
apiKey = parsedKey;
|
|
309
|
-
ok(`Client ID: ${c.cyan}${apiKey}${c.reset}`);
|
|
310
|
-
} else {
|
|
311
|
-
warn("Could not read Client ID from TOML");
|
|
312
|
-
const res = await prompts({
|
|
313
|
-
type: "text",
|
|
314
|
-
name: "apiKey",
|
|
315
|
-
message: "Paste your Client ID (API Key)",
|
|
316
|
-
validate: (v) => (v.trim() ? true : "Required"),
|
|
317
|
-
}, { onCancel });
|
|
318
|
-
apiKey = res.apiKey;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// API Secret is never in the TOML — always need to ask
|
|
322
|
-
console.log();
|
|
323
|
-
info("The API Secret is not stored in config files for security.");
|
|
324
|
-
info(`Find it at: ${c.cyan}https://partners.shopify.com${c.reset} → Apps → your app → Client credentials`);
|
|
325
|
-
console.log();
|
|
326
|
-
const { secret } = await prompts({
|
|
327
|
-
type: "password",
|
|
328
|
-
name: "secret",
|
|
329
|
-
message: "Paste your Client Secret (API Secret)",
|
|
330
|
-
validate: (v) => (v.trim() ? true : "Required — find it in Partner Dashboard → Apps → Client credentials"),
|
|
331
|
-
}, { onCancel });
|
|
332
|
-
apiSecret = secret;
|
|
333
|
-
|
|
334
|
-
} finally {
|
|
335
|
-
// Clean up temp directory
|
|
336
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
337
|
-
}
|
|
338
|
-
} else {
|
|
339
|
-
// ── Manual entry ──────────────────────────────────────────────
|
|
340
|
-
const creds = await prompts([
|
|
341
|
-
{
|
|
342
|
-
type: "text",
|
|
343
|
-
name: "apiKey",
|
|
344
|
-
message: `Shopify API Key ${c.dim}(Client ID)${c.reset}`,
|
|
345
|
-
validate: (v) => (v.trim() ? true : "Required"),
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
type: "password",
|
|
349
|
-
name: "apiSecret",
|
|
350
|
-
message: "Shopify API Secret",
|
|
351
|
-
validate: (v) => (v.trim() ? true : "Required"),
|
|
352
|
-
},
|
|
353
|
-
], { onCancel });
|
|
354
|
-
apiKey = creds.apiKey;
|
|
355
|
-
apiSecret = creds.apiSecret;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Scope presets (like Shopify CLI template selection)
|
|
420
|
+
// ── API scopes ────────────────────────────────────────────────────
|
|
359
421
|
const { scopeChoice } = await prompts({
|
|
360
422
|
type: "select",
|
|
361
423
|
name: "scopeChoice",
|
|
@@ -382,34 +444,97 @@ async function getConfig(args) {
|
|
|
382
444
|
scopes = scopeChoice;
|
|
383
445
|
}
|
|
384
446
|
|
|
447
|
+
// ── Check for directory conflict ──────────────────────────────────
|
|
448
|
+
const outputDir = path.resolve(process.cwd(), projectName);
|
|
449
|
+
if (fs.existsSync(outputDir)) {
|
|
450
|
+
const { overwrite } = await prompts({
|
|
451
|
+
type: "confirm",
|
|
452
|
+
name: "overwrite",
|
|
453
|
+
message: `Directory "${projectName}" already exists. Overwrite?`,
|
|
454
|
+
initial: false,
|
|
455
|
+
}, { onCancel });
|
|
456
|
+
if (!overwrite) { console.log("\n Cancelled.\n"); process.exit(0); }
|
|
457
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
461
|
+
section("Scaffolding");
|
|
462
|
+
|
|
463
|
+
// Build initial config (credentials filled in later after Shopify app creation)
|
|
464
|
+
const config = {
|
|
465
|
+
projectName,
|
|
466
|
+
appName,
|
|
467
|
+
language,
|
|
468
|
+
scopes,
|
|
469
|
+
apiKey: "",
|
|
470
|
+
apiSecret: "",
|
|
471
|
+
projectId: "",
|
|
472
|
+
appUrl: "",
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
info("Creating project files...");
|
|
476
|
+
const fileCount = scaffold(outputDir, config);
|
|
477
|
+
ok(`Created ${fileCount} files in ${c.cyan}${projectName}/${c.reset}`);
|
|
478
|
+
|
|
479
|
+
if (language === "typescript") {
|
|
480
|
+
info(`${c.dim}Backend: TypeScript (functions/src/*.ts)${c.reset}`);
|
|
481
|
+
} else {
|
|
482
|
+
info(`${c.dim}Backend: JavaScript (functions/src/*.js)${c.reset}`);
|
|
483
|
+
}
|
|
484
|
+
info(`${c.dim}Frontend: 4 pages — Dashboard, Products, Settings, Components${c.reset}`);
|
|
485
|
+
|
|
385
486
|
// ═══════════════════════════════════════════════════════════════════
|
|
386
487
|
section("Firebase Setup");
|
|
387
488
|
|
|
388
|
-
|
|
389
|
-
|
|
489
|
+
// ── Ensure Firebase CLI ───────────────────────────────────────────
|
|
490
|
+
if (!hasCommand("firebase")) {
|
|
491
|
+
info("Firebase CLI not found — installing...");
|
|
492
|
+
try {
|
|
493
|
+
await exec("npm install -g firebase-tools");
|
|
494
|
+
ok("Firebase CLI installed");
|
|
495
|
+
} catch {
|
|
496
|
+
warn("Could not install Firebase CLI automatically");
|
|
497
|
+
info("Install manually: npm i -g firebase-tools");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
390
500
|
|
|
391
|
-
if (
|
|
392
|
-
|
|
501
|
+
if (hasCommand("firebase")) {
|
|
502
|
+
// ── Firebase login ──────────────────────────────────────────────
|
|
503
|
+
info("Checking Firebase authentication...");
|
|
504
|
+
const projects = listFirebaseProjects();
|
|
505
|
+
if (projects.length > 0) {
|
|
506
|
+
ok("Firebase authenticated");
|
|
507
|
+
} else {
|
|
508
|
+
info("Opening browser for Firebase login...");
|
|
509
|
+
try {
|
|
510
|
+
await execInteractive("firebase login");
|
|
511
|
+
ok("Logged into Firebase");
|
|
512
|
+
} catch {
|
|
513
|
+
warn("Firebase login failed — run 'firebase login' manually later");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
393
516
|
|
|
394
|
-
|
|
517
|
+
// ── Project selection ───────────────────────────────────────────
|
|
518
|
+
const freshProjects = listFirebaseProjects();
|
|
519
|
+
const fbChoices = [
|
|
395
520
|
{ title: `${c.cyan}[create a new project]${c.reset}`, value: "__create__" },
|
|
396
521
|
];
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
for (const p of
|
|
400
|
-
|
|
522
|
+
|
|
523
|
+
if (freshProjects.length > 0) {
|
|
524
|
+
for (const p of freshProjects) {
|
|
525
|
+
fbChoices.push({
|
|
401
526
|
title: `${p.displayName} ${c.dim}(${p.projectId})${c.reset}`,
|
|
402
527
|
value: p.projectId,
|
|
403
528
|
});
|
|
404
529
|
}
|
|
405
530
|
}
|
|
406
|
-
|
|
531
|
+
fbChoices.push({ title: `${c.dim}[enter project ID manually]${c.reset}`, value: "__manual__" });
|
|
407
532
|
|
|
408
533
|
const { firebaseChoice } = await prompts({
|
|
409
534
|
type: "select",
|
|
410
535
|
name: "firebaseChoice",
|
|
411
536
|
message: "Select a Firebase project",
|
|
412
|
-
choices,
|
|
537
|
+
choices: fbChoices,
|
|
413
538
|
}, { onCancel });
|
|
414
539
|
|
|
415
540
|
if (firebaseChoice === "__create__") {
|
|
@@ -417,7 +542,7 @@ async function getConfig(args) {
|
|
|
417
542
|
type: "text",
|
|
418
543
|
name: "newProjectId",
|
|
419
544
|
message: "New project ID",
|
|
420
|
-
initial: projectName,
|
|
545
|
+
initial: projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 30),
|
|
421
546
|
validate: (v) => {
|
|
422
547
|
if (!v.trim()) return "Required";
|
|
423
548
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(v)) return "Only lowercase letters, numbers, and hyphens";
|
|
@@ -430,17 +555,16 @@ async function getConfig(args) {
|
|
|
430
555
|
const created = await createFirebaseProject(newProjectId, appName);
|
|
431
556
|
if (created) {
|
|
432
557
|
ok(`Project created: ${c.cyan}${newProjectId}${c.reset}`);
|
|
433
|
-
projectId = newProjectId;
|
|
558
|
+
config.projectId = newProjectId;
|
|
434
559
|
} else {
|
|
435
560
|
warn("Could not create project automatically");
|
|
436
|
-
info("Create one at https://console.firebase.google.com");
|
|
437
561
|
const { manualId } = await prompts({
|
|
438
562
|
type: "text",
|
|
439
563
|
name: "manualId",
|
|
440
564
|
message: "Firebase Project ID",
|
|
441
565
|
validate: (v) => (v.trim() ? true : "Required"),
|
|
442
566
|
}, { onCancel });
|
|
443
|
-
projectId = manualId;
|
|
567
|
+
config.projectId = manualId;
|
|
444
568
|
}
|
|
445
569
|
} else if (firebaseChoice === "__manual__") {
|
|
446
570
|
const { manualId } = await prompts({
|
|
@@ -449,190 +573,265 @@ async function getConfig(args) {
|
|
|
449
573
|
message: "Firebase Project ID",
|
|
450
574
|
validate: (v) => (v.trim() ? true : "Required"),
|
|
451
575
|
}, { onCancel });
|
|
452
|
-
projectId = manualId;
|
|
576
|
+
config.projectId = manualId;
|
|
453
577
|
} else {
|
|
454
|
-
projectId = firebaseChoice;
|
|
455
|
-
ok(`Using project: ${c.cyan}${projectId}${c.reset}`);
|
|
578
|
+
config.projectId = firebaseChoice;
|
|
579
|
+
ok(`Using project: ${c.cyan}${config.projectId}${c.reset}`);
|
|
456
580
|
}
|
|
457
|
-
} else {
|
|
458
|
-
// No Firebase CLI — manual setup
|
|
459
|
-
const { firebaseSetup } = await prompts({
|
|
460
|
-
type: "select",
|
|
461
|
-
name: "firebaseSetup",
|
|
462
|
-
message: "How would you like to set up Firebase?",
|
|
463
|
-
choices: [
|
|
464
|
-
{ title: "Create a new project — opens Firebase Console", value: "create" },
|
|
465
|
-
{ title: "Enter project ID manually", value: "manual" },
|
|
466
|
-
],
|
|
467
|
-
}, { onCancel });
|
|
468
581
|
|
|
469
|
-
|
|
470
|
-
console.log();
|
|
471
|
-
info("Opening Firebase Console...");
|
|
472
|
-
info("Create a new project and note the Project ID");
|
|
473
|
-
console.log();
|
|
474
|
-
openBrowser("https://console.firebase.google.com");
|
|
475
|
-
}
|
|
582
|
+
config.appUrl = `https://${config.projectId}.web.app`;
|
|
476
583
|
|
|
584
|
+
// ── Write .firebaserc now that we have projectId ────────────────
|
|
585
|
+
const firebaserc = JSON.stringify({ projects: { default: config.projectId } }, null, 2);
|
|
586
|
+
fs.writeFileSync(path.join(outputDir, ".firebaserc"), firebaserc + "\n");
|
|
587
|
+
|
|
588
|
+
// ── Provision Firebase services ─────────────────────────────────
|
|
589
|
+
await provisionFirebase(config, {
|
|
590
|
+
skipProvision: !!args["skip-provision"],
|
|
591
|
+
firestoreRegion: args["firestore-region"],
|
|
592
|
+
nonInteractive: false,
|
|
593
|
+
cwd: outputDir,
|
|
594
|
+
});
|
|
595
|
+
} else {
|
|
596
|
+
// No Firebase CLI available
|
|
477
597
|
const { manualId } = await prompts({
|
|
478
598
|
type: "text",
|
|
479
599
|
name: "manualId",
|
|
480
|
-
message: "Firebase Project ID",
|
|
600
|
+
message: "Firebase Project ID (create at console.firebase.google.com)",
|
|
481
601
|
validate: (v) => (v.trim() ? true : "Required"),
|
|
482
602
|
}, { onCancel });
|
|
483
|
-
projectId = manualId;
|
|
484
|
-
|
|
603
|
+
config.projectId = manualId;
|
|
604
|
+
config.appUrl = `https://${config.projectId}.web.app`;
|
|
485
605
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
appType,
|
|
490
|
-
apiKey,
|
|
491
|
-
apiSecret,
|
|
492
|
-
scopes: scopes || "read_products",
|
|
493
|
-
projectId,
|
|
494
|
-
appUrl: `https://${projectId}.web.app`,
|
|
495
|
-
};
|
|
496
|
-
}
|
|
606
|
+
const firebaserc = JSON.stringify({ projects: { default: config.projectId } }, null, 2);
|
|
607
|
+
fs.writeFileSync(path.join(outputDir, ".firebaserc"), firebaserc + "\n");
|
|
608
|
+
}
|
|
497
609
|
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
// Recursively copy templates directory
|
|
501
|
-
copyDirSync(TEMPLATES_DIR, outputDir);
|
|
610
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
611
|
+
section("Shopify App Setup");
|
|
502
612
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (fs.existsSync(src)) {
|
|
512
|
-
fs.renameSync(src, dest);
|
|
613
|
+
// ── Ensure Shopify CLI ────────────────────────────────────────────
|
|
614
|
+
if (!hasCommand("shopify")) {
|
|
615
|
+
info("Shopify CLI not found — installing...");
|
|
616
|
+
try {
|
|
617
|
+
await exec("npm install -g @shopify/cli");
|
|
618
|
+
ok("Shopify CLI installed");
|
|
619
|
+
} catch {
|
|
620
|
+
warn("Could not install Shopify CLI");
|
|
513
621
|
}
|
|
514
622
|
}
|
|
515
623
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
624
|
+
if (hasCommand("shopify")) {
|
|
625
|
+
const { shopifyAction } = await prompts({
|
|
626
|
+
type: "select",
|
|
627
|
+
name: "shopifyAction",
|
|
628
|
+
message: "Shopify app setup",
|
|
629
|
+
choices: [
|
|
630
|
+
{ title: "Create a new app + link it (recommended)", value: "create" },
|
|
631
|
+
{ title: "Link an existing app", value: "link" },
|
|
632
|
+
{ title: "Skip — I'll configure manually later", value: "skip" },
|
|
633
|
+
],
|
|
634
|
+
}, { onCancel });
|
|
525
635
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
636
|
+
if (shopifyAction !== "skip") {
|
|
637
|
+
// ── Login ──────────────────────────────────────────────────
|
|
638
|
+
console.log();
|
|
639
|
+
info("Logging into Shopify...");
|
|
640
|
+
info("A browser window will open — sign in to your Partner account.");
|
|
641
|
+
console.log();
|
|
642
|
+
try {
|
|
643
|
+
await execInteractive("shopify auth login");
|
|
644
|
+
ok("Logged into Shopify");
|
|
645
|
+
} catch {
|
|
646
|
+
warn("Shopify login failed — continuing with manual setup");
|
|
647
|
+
}
|
|
531
648
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
649
|
+
// ── Create / Link app via Shopify CLI ──────────────────────
|
|
650
|
+
// Must `cd` into project dir — Shopify CLI uses process.cwd(),
|
|
651
|
+
// not spawn's cwd option, for TOML generation.
|
|
652
|
+
console.log();
|
|
653
|
+
if (shopifyAction === "create") {
|
|
654
|
+
info(`Creating a new Shopify app: ${c.cyan}${appName}${c.reset}`);
|
|
655
|
+
info(`Select ${c.bold}"Create a new app"${c.reset} when prompted by the CLI.`);
|
|
656
|
+
} else {
|
|
657
|
+
info("Select your existing app from the list.");
|
|
658
|
+
}
|
|
659
|
+
console.log();
|
|
660
|
+
|
|
661
|
+
// Use `cd` to ensure Shopify CLI writes files in the project directory
|
|
662
|
+
const cdCmd = process.platform === "win32"
|
|
663
|
+
? `cd /d "${outputDir}" && shopify app config link`
|
|
664
|
+
: `cd "${outputDir}" && shopify app config link`;
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
await execInteractive(cdCmd);
|
|
668
|
+
} catch {
|
|
669
|
+
// Shopify CLI may exit non-zero even after creating/linking the app
|
|
670
|
+
// (e.g. "directory doesn't have a package.json" warning)
|
|
671
|
+
warn("Shopify CLI exited with a warning");
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ── Parse client_id from any TOML (runs regardless of exit code) ──
|
|
675
|
+
// Check project dir first, then parent dir (Shopify CLI fallback location)
|
|
676
|
+
const dirsToCheck = [outputDir, path.dirname(outputDir)];
|
|
677
|
+
for (const dir of dirsToCheck) {
|
|
678
|
+
if (config.apiKey) break;
|
|
679
|
+
try {
|
|
680
|
+
const tomlFiles = fs.readdirSync(dir).filter((f) => f.endsWith(".toml"));
|
|
681
|
+
for (const f of tomlFiles) {
|
|
682
|
+
const filePath = path.join(dir, f);
|
|
683
|
+
const clientId = parseTomlField(filePath, "client_id");
|
|
684
|
+
if (clientId && clientId !== "{{API_KEY}}" && clientId.length > 5) {
|
|
685
|
+
config.apiKey = clientId;
|
|
686
|
+
ok(`Client ID: ${c.cyan}${config.apiKey}${c.reset}`);
|
|
687
|
+
|
|
688
|
+
// If TOML was in parent dir (Shopify CLI bug), move it to project dir
|
|
689
|
+
if (dir !== outputDir) {
|
|
690
|
+
const destPath = path.join(outputDir, f);
|
|
691
|
+
try {
|
|
692
|
+
// Don't overwrite our template — just read the client_id
|
|
693
|
+
if (!fs.existsSync(destPath)) {
|
|
694
|
+
fs.renameSync(filePath, destPath);
|
|
695
|
+
} else {
|
|
696
|
+
// Clean up the stray TOML from parent dir
|
|
697
|
+
fs.unlinkSync(filePath);
|
|
698
|
+
}
|
|
699
|
+
info(`${c.dim}Picked up credentials from Shopify CLI${c.reset}`);
|
|
700
|
+
} catch {}
|
|
701
|
+
}
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
} catch {}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (!config.apiKey) {
|
|
709
|
+
console.log();
|
|
710
|
+
info("Could not read Client ID from Shopify CLI output.");
|
|
711
|
+
const res = await prompts({
|
|
712
|
+
type: "text",
|
|
713
|
+
name: "apiKey",
|
|
714
|
+
message: "Paste your Client ID (from Partner Dashboard → Apps)",
|
|
715
|
+
validate: (v) => (v.trim() ? true : "Required"),
|
|
716
|
+
}, { onCancel });
|
|
717
|
+
config.apiKey = res.apiKey;
|
|
718
|
+
}
|
|
538
719
|
}
|
|
539
|
-
fs.writeFileSync(filePath, content);
|
|
540
|
-
}
|
|
541
720
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
721
|
+
// ── Get API Secret (never in TOML) ──────────────────────────────
|
|
722
|
+
if (!config.apiKey) {
|
|
723
|
+
console.log();
|
|
724
|
+
info(`Find credentials at: ${c.cyan}https://partners.shopify.com${c.reset} → Apps → Client credentials`);
|
|
725
|
+
const res = await prompts({
|
|
726
|
+
type: "text",
|
|
727
|
+
name: "apiKey",
|
|
728
|
+
message: `Client ID ${c.dim}(API Key)${c.reset}`,
|
|
729
|
+
validate: (v) => (v.trim() ? true : "Required"),
|
|
730
|
+
}, { onCancel });
|
|
731
|
+
config.apiKey = res.apiKey;
|
|
732
|
+
}
|
|
551
733
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
734
|
+
console.log();
|
|
735
|
+
info("The API Secret is not stored in config files for security.");
|
|
736
|
+
info(`Find it at: ${c.cyan}https://partners.shopify.com${c.reset} → your app → Client credentials`);
|
|
737
|
+
console.log();
|
|
738
|
+
const { apiSecret } = await prompts({
|
|
739
|
+
type: "password",
|
|
740
|
+
name: "apiSecret",
|
|
741
|
+
message: "Client Secret (API Secret)",
|
|
742
|
+
validate: (v) => (v.trim() ? true : "Required"),
|
|
743
|
+
}, { onCancel });
|
|
744
|
+
config.apiSecret = apiSecret;
|
|
559
745
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
746
|
+
} else {
|
|
747
|
+
// No Shopify CLI — full manual entry
|
|
748
|
+
info(`Enter your Shopify app credentials (from ${c.cyan}partners.shopify.com${c.reset})`);
|
|
749
|
+
const creds = await prompts([
|
|
750
|
+
{
|
|
751
|
+
type: "text",
|
|
752
|
+
name: "apiKey",
|
|
753
|
+
message: `Client ID ${c.dim}(API Key)${c.reset}`,
|
|
754
|
+
validate: (v) => (v.trim() ? true : "Required"),
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
type: "password",
|
|
758
|
+
name: "apiSecret",
|
|
759
|
+
message: "Client Secret (API Secret)",
|
|
760
|
+
validate: (v) => (v.trim() ? true : "Required"),
|
|
761
|
+
},
|
|
762
|
+
], { onCancel });
|
|
763
|
+
config.apiKey = creds.apiKey;
|
|
764
|
+
config.apiSecret = creds.apiSecret;
|
|
765
|
+
}
|
|
567
766
|
|
|
568
|
-
//
|
|
569
|
-
|
|
570
|
-
countFiles(outputDir, () => count++);
|
|
571
|
-
return count;
|
|
572
|
-
}
|
|
767
|
+
// ── Write final credentials to files ──────────────────────────────
|
|
768
|
+
updateCredentials(outputDir, config);
|
|
573
769
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
770
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
771
|
+
section("Installing & Building");
|
|
772
|
+
|
|
773
|
+
// ── npm install ───────────────────────────────────────────────────
|
|
774
|
+
info("Installing dependencies...");
|
|
775
|
+
const functionsDir = path.join(outputDir, "functions");
|
|
776
|
+
try {
|
|
777
|
+
await exec("npm install", functionsDir);
|
|
778
|
+
ok("Dependencies installed");
|
|
779
|
+
} catch {
|
|
780
|
+
warn(`npm install failed — run manually: cd ${projectName}/functions && npm install`);
|
|
584
781
|
}
|
|
585
|
-
}
|
|
586
782
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
783
|
+
// ── TypeScript build ──────────────────────────────────────────────
|
|
784
|
+
if (language === "typescript") {
|
|
785
|
+
info("Building TypeScript...");
|
|
786
|
+
try {
|
|
787
|
+
await exec("npm run build", functionsDir);
|
|
788
|
+
ok("TypeScript compiled successfully");
|
|
789
|
+
} catch {
|
|
790
|
+
warn("Build failed — run manually: cd functions && npm run build");
|
|
593
791
|
}
|
|
594
792
|
}
|
|
595
|
-
}
|
|
596
793
|
|
|
597
|
-
//
|
|
598
|
-
|
|
599
|
-
const args = parseArgs(argv);
|
|
794
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
795
|
+
section("Finishing Up");
|
|
600
796
|
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
797
|
+
// ── Git init ──────────────────────────────────────────────────────
|
|
798
|
+
info("Initializing git...");
|
|
799
|
+
if (hasCommand("git")) {
|
|
800
|
+
try {
|
|
801
|
+
await exec("git init", outputDir);
|
|
802
|
+
await exec("git add -A", outputDir);
|
|
803
|
+
await exec('git commit -m "Initial scaffold from create-shopify-firebase-app"', outputDir);
|
|
804
|
+
ok("Git repository initialized");
|
|
805
|
+
} catch {
|
|
806
|
+
warn("Git init failed — initialize manually if needed");
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
warn("Git not found — skipping");
|
|
605
810
|
}
|
|
606
811
|
|
|
607
|
-
//
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
812
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
813
|
+
printSuccess(config);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ─── CI / non-interactive mode ──────────────────────────────────────────
|
|
817
|
+
async function runCI(args) {
|
|
818
|
+
const config = {
|
|
819
|
+
projectName: args.projectName || "my-shopify-app",
|
|
820
|
+
appName: args["app-name"] || args.projectName || "My Shopify App",
|
|
821
|
+
language: args.language === "javascript" ? "javascript" : "typescript",
|
|
822
|
+
apiKey: args["api-key"],
|
|
823
|
+
apiSecret: args["api-secret"],
|
|
824
|
+
scopes: args.scopes || "read_products",
|
|
825
|
+
projectId: args["project-id"],
|
|
826
|
+
appUrl: `https://${args["project-id"]}.web.app`,
|
|
827
|
+
};
|
|
615
828
|
|
|
616
|
-
// Collect config
|
|
617
|
-
const config = await getConfig(args);
|
|
618
829
|
const outputDir = path.resolve(process.cwd(), config.projectName);
|
|
619
830
|
|
|
620
|
-
// Check if directory exists
|
|
621
831
|
if (fs.existsSync(outputDir)) {
|
|
622
|
-
const { overwrite } = await prompts({
|
|
623
|
-
type: "confirm",
|
|
624
|
-
name: "overwrite",
|
|
625
|
-
message: `Directory "${config.projectName}" already exists. Overwrite?`,
|
|
626
|
-
initial: false,
|
|
627
|
-
});
|
|
628
|
-
if (!overwrite) {
|
|
629
|
-
console.log("\n Cancelled.\n");
|
|
630
|
-
process.exit(0);
|
|
631
|
-
}
|
|
632
832
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
633
833
|
}
|
|
634
834
|
|
|
635
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
636
835
|
section("Setting Up");
|
|
637
836
|
|
|
638
837
|
info("Scaffolding project...");
|
|
@@ -644,69 +843,111 @@ export async function run(argv) {
|
|
|
644
843
|
try {
|
|
645
844
|
await exec("npm install", functionsDir);
|
|
646
845
|
ok("Dependencies installed");
|
|
647
|
-
} catch
|
|
648
|
-
warn(
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
info("Building TypeScript...");
|
|
652
|
-
try {
|
|
653
|
-
await exec("npm run build", functionsDir);
|
|
654
|
-
ok("TypeScript compiled successfully");
|
|
655
|
-
} catch (e) {
|
|
656
|
-
warn("Build failed — run manually: cd functions && npm run build");
|
|
846
|
+
} catch {
|
|
847
|
+
warn("npm install failed");
|
|
657
848
|
}
|
|
658
849
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
info("Firebase CLI not found — installing globally...");
|
|
850
|
+
if (config.language === "typescript") {
|
|
851
|
+
info("Building TypeScript...");
|
|
662
852
|
try {
|
|
663
|
-
await exec("npm
|
|
664
|
-
ok("
|
|
665
|
-
} catch
|
|
666
|
-
warn("
|
|
667
|
-
info("Install manually: npm i -g firebase-tools");
|
|
668
|
-
info(`Then run: cd ${config.projectName} && firebase use ${config.projectId}`);
|
|
853
|
+
await exec("npm run build", functionsDir);
|
|
854
|
+
ok("TypeScript compiled");
|
|
855
|
+
} catch {
|
|
856
|
+
warn("Build failed");
|
|
669
857
|
}
|
|
670
858
|
}
|
|
671
|
-
|
|
672
|
-
|
|
859
|
+
|
|
860
|
+
if (hasCommand("firebase") && !args["skip-provision"]) {
|
|
861
|
+
info("Setting up Firebase...");
|
|
673
862
|
await provisionFirebase(config, {
|
|
674
863
|
skipProvision: !!args["skip-provision"],
|
|
675
864
|
firestoreRegion: args["firestore-region"],
|
|
676
|
-
nonInteractive:
|
|
865
|
+
nonInteractive: true,
|
|
677
866
|
cwd: outputDir,
|
|
678
867
|
});
|
|
679
868
|
}
|
|
680
869
|
|
|
681
|
-
info("Checking Shopify CLI...");
|
|
682
|
-
if (hasCommand("shopify")) {
|
|
683
|
-
ok("Shopify CLI detected");
|
|
684
|
-
} else {
|
|
685
|
-
info("Shopify CLI not found — installing globally...");
|
|
686
|
-
try {
|
|
687
|
-
await exec("npm install -g @shopify/cli");
|
|
688
|
-
ok("Shopify CLI installed");
|
|
689
|
-
} catch (e) {
|
|
690
|
-
warn("Could not install Shopify CLI automatically");
|
|
691
|
-
info("Install manually: npm i -g @shopify/cli");
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
info("Initializing git...");
|
|
696
870
|
if (hasCommand("git")) {
|
|
871
|
+
info("Initializing git...");
|
|
697
872
|
try {
|
|
698
873
|
await exec("git init", outputDir);
|
|
699
874
|
await exec("git add -A", outputDir);
|
|
700
875
|
await exec('git commit -m "Initial scaffold from create-shopify-firebase-app"', outputDir);
|
|
701
|
-
ok("Git
|
|
702
|
-
} catch {
|
|
703
|
-
|
|
876
|
+
ok("Git initialized");
|
|
877
|
+
} catch {}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
printSuccess(config);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ─── Distribution flow ──────────────────────────────────────────────────
|
|
884
|
+
async function distributeFlow() {
|
|
885
|
+
console.log();
|
|
886
|
+
console.log(` ${c.green}${c.bold}🛍️ + 🔥${c.reset} ${c.bold}App Distribution${c.reset}`);
|
|
887
|
+
|
|
888
|
+
const tomlPath = path.resolve(process.cwd(), "shopify.app.toml");
|
|
889
|
+
|
|
890
|
+
if (!fs.existsSync(tomlPath)) {
|
|
891
|
+
// No app configured — offer to set one up
|
|
892
|
+
console.log();
|
|
893
|
+
fail("No shopify.app.toml found in the current directory.");
|
|
894
|
+
info("Run this command from your app's root directory.");
|
|
895
|
+
console.log();
|
|
896
|
+
|
|
897
|
+
if (hasCommand("shopify")) {
|
|
898
|
+
const { shouldLink } = await prompts({
|
|
899
|
+
type: "confirm",
|
|
900
|
+
name: "shouldLink",
|
|
901
|
+
message: "Would you like to link a Shopify app now?",
|
|
902
|
+
initial: true,
|
|
903
|
+
}, { onCancel });
|
|
904
|
+
|
|
905
|
+
if (shouldLink) {
|
|
906
|
+
try {
|
|
907
|
+
await execInteractive("shopify auth login");
|
|
908
|
+
await execInteractive("shopify app config link");
|
|
909
|
+
ok("App linked successfully");
|
|
910
|
+
} catch {
|
|
911
|
+
fail("Could not link app");
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
} else {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
} else {
|
|
918
|
+
info("Install Shopify CLI: npm i -g @shopify/cli");
|
|
919
|
+
return;
|
|
704
920
|
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const clientId = parseTomlField(tomlPath, "client_id");
|
|
924
|
+
const appName = parseTomlField(tomlPath, "name");
|
|
925
|
+
|
|
926
|
+
section("Distribution Checklist");
|
|
927
|
+
|
|
928
|
+
console.log(` ${c.bold}App: ${c.cyan}${appName || "Unknown"}${c.reset}`);
|
|
929
|
+
if (clientId) console.log(` ${c.bold}Client ID: ${c.dim}${clientId}${c.reset}`);
|
|
930
|
+
console.log();
|
|
931
|
+
|
|
932
|
+
console.log(` ${c.bold}Before submitting to the App Store:${c.reset}`);
|
|
933
|
+
console.log();
|
|
934
|
+
console.log(` ${c.cyan}1.${c.reset} Deploy your app: ${c.cyan}firebase deploy${c.reset}`);
|
|
935
|
+
console.log(` ${c.cyan}2.${c.reset} Test on a development store`);
|
|
936
|
+
console.log(` ${c.cyan}3.${c.reset} Add your privacy policy URL`);
|
|
937
|
+
console.log(` ${c.cyan}4.${c.reset} Add your app listing details (description, screenshots)`);
|
|
938
|
+
console.log(` ${c.cyan}5.${c.reset} Submit for review`);
|
|
939
|
+
console.log();
|
|
940
|
+
|
|
941
|
+
if (clientId) {
|
|
942
|
+
info("Opening Partner Dashboard → Distribution...");
|
|
943
|
+
openBrowser(`https://partners.shopify.com/apps/${clientId}/distribution`);
|
|
705
944
|
} else {
|
|
706
|
-
|
|
945
|
+
info(`Open: ${c.cyan}https://partners.shopify.com${c.reset} → Apps → your app → Distribution`);
|
|
707
946
|
}
|
|
708
947
|
|
|
709
|
-
|
|
948
|
+
console.log();
|
|
949
|
+
console.log(` ${c.dim}Docs: https://shopify.dev/docs/apps/launch${c.reset}`);
|
|
950
|
+
console.log();
|
|
710
951
|
}
|
|
711
952
|
|
|
712
953
|
// ─── Success output ──────────────────────────────────────────────────────
|
|
@@ -714,7 +955,15 @@ function printSuccess(config) {
|
|
|
714
955
|
console.log();
|
|
715
956
|
console.log(` ${c.green}${c.bold}✔ All done!${c.reset} Your Shopify + Firebase app is ready.`);
|
|
716
957
|
console.log();
|
|
717
|
-
console.log(` ${c.bold}
|
|
958
|
+
console.log(` ${c.bold}Your app includes:${c.reset}`);
|
|
959
|
+
console.log(` ${c.green}✔${c.reset} Dashboard — store info + quick stats`);
|
|
960
|
+
console.log(` ${c.green}✔${c.reset} Products — search + detail view`);
|
|
961
|
+
console.log(` ${c.green}✔${c.reset} Settings — form with Firestore persistence`);
|
|
962
|
+
console.log(` ${c.green}✔${c.reset} Components — Polaris reference with copy-paste code`);
|
|
963
|
+
console.log(` ${c.green}✔${c.reset} App Bridge — navigation, toasts, modals, resource picker`);
|
|
964
|
+
console.log(` ${c.green}✔${c.reset} 4 Cloud Functions — auth, api, webhooks, proxy`);
|
|
965
|
+
console.log();
|
|
966
|
+
console.log(` ${c.bold}Deploy:${c.reset}`);
|
|
718
967
|
console.log();
|
|
719
968
|
console.log(` ${c.cyan}cd ${config.projectName}${c.reset}`);
|
|
720
969
|
console.log(` ${c.cyan}firebase deploy${c.reset}`);
|
|
@@ -723,11 +972,12 @@ function printSuccess(config) {
|
|
|
723
972
|
console.log();
|
|
724
973
|
console.log(` ${c.cyan}${config.appUrl}/auth?shop=YOUR-STORE.myshopify.com${c.reset}`);
|
|
725
974
|
console.log();
|
|
726
|
-
console.log(` ${c.bold}
|
|
975
|
+
console.log(` ${c.bold}Go live:${c.reset}`);
|
|
727
976
|
console.log();
|
|
728
|
-
console.log(` ${c.cyan}shopify
|
|
977
|
+
console.log(` ${c.cyan}npx create-shopify-firebase-app --distribute${c.reset}`);
|
|
729
978
|
console.log();
|
|
730
979
|
console.log(` ${c.dim}─────────────────────────────────────────${c.reset}`);
|
|
980
|
+
console.log(` ${c.dim}Language: ${config.language}${c.reset}`);
|
|
731
981
|
console.log(` ${c.dim}App URL: ${config.appUrl}${c.reset}`);
|
|
732
982
|
console.log(` ${c.dim}Firebase: ${config.projectId}${c.reset}`);
|
|
733
983
|
console.log(` ${c.dim}Scopes: ${config.scopes}${c.reset}`);
|
|
@@ -739,8 +989,8 @@ function printHelp() {
|
|
|
739
989
|
console.log(`
|
|
740
990
|
${c.bold}create-shopify-firebase-app${c.reset}
|
|
741
991
|
|
|
742
|
-
|
|
743
|
-
|
|
992
|
+
Build Shopify apps for free. Serverless, zero-framework.
|
|
993
|
+
The easiest way to build Shopify apps on Firebase.
|
|
744
994
|
|
|
745
995
|
${c.bold}Usage:${c.reset}
|
|
746
996
|
|
|
@@ -748,49 +998,47 @@ function printHelp() {
|
|
|
748
998
|
|
|
749
999
|
${c.bold}Options:${c.reset}
|
|
750
1000
|
|
|
1001
|
+
--help, -h Show this help
|
|
1002
|
+
--version, -v Show version
|
|
1003
|
+
--distribute Open distribution dashboard for your app
|
|
1004
|
+
|
|
1005
|
+
${c.bold}CI / non-interactive:${c.reset}
|
|
1006
|
+
|
|
751
1007
|
--api-key=KEY Shopify API Key (client_id)
|
|
752
1008
|
--api-secret=SECRET Shopify API Secret
|
|
753
1009
|
--project-id=ID Firebase Project ID
|
|
754
1010
|
--scopes=SCOPES API scopes (default: read_products)
|
|
1011
|
+
--language=LANG typescript or javascript (default: typescript)
|
|
755
1012
|
--app-name=NAME App name shown in Shopify admin
|
|
756
|
-
--help, -h Show this help
|
|
757
|
-
--version, -v Show version
|
|
758
|
-
|
|
759
|
-
${c.bold}Firebase provisioning:${c.reset}
|
|
760
|
-
|
|
761
1013
|
--skip-provision Skip Firebase service provisioning
|
|
762
|
-
--firestore-region=LOC Firestore region (e.g.
|
|
1014
|
+
--firestore-region=LOC Firestore region (e.g. us-central1)
|
|
763
1015
|
|
|
764
1016
|
${c.bold}Examples:${c.reset}
|
|
765
1017
|
|
|
766
|
-
${c.dim}# Interactive
|
|
1018
|
+
${c.dim}# Interactive — guided wizard${c.reset}
|
|
767
1019
|
npx create-shopify-firebase-app
|
|
768
1020
|
|
|
769
1021
|
${c.dim}# With project name${c.reset}
|
|
770
1022
|
npx create-shopify-firebase-app my-app
|
|
771
1023
|
|
|
772
|
-
${c.dim}#
|
|
1024
|
+
${c.dim}# CI / non-interactive${c.reset}
|
|
773
1025
|
npx create-shopify-firebase-app my-app \\
|
|
774
|
-
--api-key=abc123 \\
|
|
775
|
-
--api-secret=secret \\
|
|
1026
|
+
--api-key=abc123 --api-secret=secret \\
|
|
776
1027
|
--project-id=my-firebase-project
|
|
777
1028
|
|
|
778
|
-
${c.dim}#
|
|
779
|
-
npx create-shopify-firebase-app
|
|
780
|
-
--api-key=abc123 \\
|
|
781
|
-
--api-secret=secret \\
|
|
782
|
-
--project-id=my-firebase-project \\
|
|
783
|
-
--firestore-region=asia-south1
|
|
1029
|
+
${c.dim}# Go live — open distribution page${c.reset}
|
|
1030
|
+
npx create-shopify-firebase-app --distribute
|
|
784
1031
|
|
|
785
1032
|
${c.bold}What you get:${c.reset}
|
|
786
1033
|
|
|
787
|
-
✔
|
|
788
|
-
✔
|
|
789
|
-
✔
|
|
790
|
-
✔
|
|
791
|
-
✔
|
|
792
|
-
✔
|
|
793
|
-
✔
|
|
794
|
-
✔ Auto-
|
|
1034
|
+
✔ 4 pages — Dashboard, Products, Settings, Polaris Components
|
|
1035
|
+
✔ App Bridge — embedded admin with navigation, toasts, modals
|
|
1036
|
+
✔ Firebase v2 Cloud Functions — 4 independent, auto-scaling
|
|
1037
|
+
✔ Shopify API 2026-01 — OAuth, webhooks, GDPR
|
|
1038
|
+
✔ Firestore — sessions, settings, app data
|
|
1039
|
+
✔ TypeScript or JavaScript — your choice
|
|
1040
|
+
✔ Firebase Hosting — $0/month for up to 25K installed stores
|
|
1041
|
+
✔ Auto-installs Firebase CLI + Shopify CLI
|
|
1042
|
+
✔ Distribution helper — go live in minutes
|
|
795
1043
|
`);
|
|
796
1044
|
}
|