appfunnel 0.6.0 → 0.8.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/dist/index.js +396 -253
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/templates/default/appfunnel.config.ts +17 -0
- package/templates/default/locales/en.json +3 -0
- package/templates/default/src/app.css +1 -0
- package/templates/default/src/components/ConsentDrawer.tsx +70 -0
- package/templates/default/src/components/Header.tsx +37 -0
- package/templates/default/src/components/paywall/PaymentCheckoutDialog.tsx +76 -0
- package/templates/default/src/funnel.tsx +9 -0
- package/templates/default/src/pages/birthday.tsx +66 -0
- package/templates/default/src/pages/download.tsx +67 -0
- package/templates/default/src/pages/email.tsx +95 -0
- package/templates/default/src/pages/intro.tsx +109 -0
- package/templates/default/src/pages/multi-select.tsx +79 -0
- package/templates/default/src/pages/name.tsx +48 -0
- package/templates/default/src/pages/paywall.tsx +191 -0
- package/templates/default/src/pages/single-select.tsx +61 -0
- package/templates/default/src/pages/upsell.tsx +158 -0
- package/templates/default/template.json +10 -0
- package/templates/default/tsconfig.json +16 -0
package/dist/index.js
CHANGED
|
@@ -21,9 +21,7 @@ function formatError(err) {
|
|
|
21
21
|
return lines.join("\n");
|
|
22
22
|
}
|
|
23
23
|
function formatWarning(code, message, hint) {
|
|
24
|
-
const lines = [
|
|
25
|
-
`${pc.yellow("WARNING")} ${pc.dim(`[${code}]`)}: ${message}`
|
|
26
|
-
];
|
|
24
|
+
const lines = [`${pc.yellow("WARNING")} ${pc.dim(`[${code}]`)}: ${message}`];
|
|
27
25
|
if (hint) {
|
|
28
26
|
lines.push(` ${pc.dim("Hint:")} ${hint}`);
|
|
29
27
|
}
|
|
@@ -175,28 +173,314 @@ var init_projects = __esm({
|
|
|
175
173
|
}
|
|
176
174
|
});
|
|
177
175
|
|
|
176
|
+
// src/lib/api.ts
|
|
177
|
+
async function apiFetch(path, options) {
|
|
178
|
+
const { token, apiBaseUrl, ...fetchOpts } = options;
|
|
179
|
+
const base = apiBaseUrl || DEFAULT_API_BASE2;
|
|
180
|
+
const url = `${base}${path}`;
|
|
181
|
+
const isFormData = fetchOpts.body instanceof FormData;
|
|
182
|
+
const headers = {
|
|
183
|
+
Authorization: token,
|
|
184
|
+
...fetchOpts.headers || {}
|
|
185
|
+
};
|
|
186
|
+
if (!isFormData) {
|
|
187
|
+
headers["Content-Type"] = "application/json";
|
|
188
|
+
}
|
|
189
|
+
const response = await fetch(url, {
|
|
190
|
+
...fetchOpts,
|
|
191
|
+
headers
|
|
192
|
+
});
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
const body = await response.text().catch(() => "");
|
|
195
|
+
let message = `API request failed: ${response.status} ${response.statusText}`;
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(body);
|
|
198
|
+
if (parsed.error) message = parsed.error;
|
|
199
|
+
if (parsed.message) message = parsed.message;
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
const error2 = new CLIError("API_ERROR", message);
|
|
203
|
+
error2.statusCode = response.status;
|
|
204
|
+
throw error2;
|
|
205
|
+
}
|
|
206
|
+
return response;
|
|
207
|
+
}
|
|
208
|
+
async function fetchPrices(projectId, storePriceIds, options) {
|
|
209
|
+
if (storePriceIds.length === 0) return /* @__PURE__ */ new Map();
|
|
210
|
+
const response = await apiFetch(`/project/${projectId}/headless/prices`, {
|
|
211
|
+
...options,
|
|
212
|
+
method: "POST",
|
|
213
|
+
body: JSON.stringify({ storePriceIds })
|
|
214
|
+
});
|
|
215
|
+
const data = await response.json();
|
|
216
|
+
return new Map(Object.entries(data.prices || {}));
|
|
217
|
+
}
|
|
218
|
+
async function fetchStores(projectId, options) {
|
|
219
|
+
const response = await apiFetch(`/project/${projectId}/stores`, {
|
|
220
|
+
...options,
|
|
221
|
+
method: "GET"
|
|
222
|
+
});
|
|
223
|
+
const data = await response.json();
|
|
224
|
+
return data.data || [];
|
|
225
|
+
}
|
|
226
|
+
async function fetchStorePrices(projectId, storeId, options) {
|
|
227
|
+
const response = await apiFetch(
|
|
228
|
+
`/project/${projectId}/stores/${storeId}/prices`,
|
|
229
|
+
{ ...options, method: "GET" }
|
|
230
|
+
);
|
|
231
|
+
const data = await response.json();
|
|
232
|
+
return data.data || [];
|
|
233
|
+
}
|
|
234
|
+
async function publishBuild(projectId, funnelId, manifest, assets, options, promote) {
|
|
235
|
+
const formData = new FormData();
|
|
236
|
+
formData.set("manifest", JSON.stringify(manifest));
|
|
237
|
+
if (funnelId) {
|
|
238
|
+
formData.set("funnelId", funnelId);
|
|
239
|
+
}
|
|
240
|
+
if (promote) {
|
|
241
|
+
formData.set("promote", "true");
|
|
242
|
+
}
|
|
243
|
+
for (const asset of assets) {
|
|
244
|
+
formData.append(
|
|
245
|
+
"assets",
|
|
246
|
+
new Blob([new Uint8Array(asset.content)], { type: asset.contentType }),
|
|
247
|
+
asset.path
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const response = await apiFetch(`/project/${projectId}/headless/publish`, {
|
|
252
|
+
...options,
|
|
253
|
+
method: "POST",
|
|
254
|
+
body: formData
|
|
255
|
+
});
|
|
256
|
+
return await response.json();
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (err instanceof CLIError && err.code === "API_ERROR") {
|
|
259
|
+
if (err.statusCode === 413) {
|
|
260
|
+
throw new CLIError(
|
|
261
|
+
"BUNDLE_TOO_LARGE",
|
|
262
|
+
err.message,
|
|
263
|
+
"Reduce page bundle sizes. Check for large dependencies."
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (err.statusCode === 409) {
|
|
267
|
+
throw new CLIError(
|
|
268
|
+
"FUNNEL_NOT_HEADLESS",
|
|
269
|
+
err.message,
|
|
270
|
+
"Remove funnelId from config to create a new headless funnel."
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
throw new CLIError("PUBLISH_FAILED", err.message);
|
|
274
|
+
}
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
var DEFAULT_API_BASE2;
|
|
279
|
+
var init_api = __esm({
|
|
280
|
+
"src/lib/api.ts"() {
|
|
281
|
+
"use strict";
|
|
282
|
+
init_errors();
|
|
283
|
+
DEFAULT_API_BASE2 = "https://api.appfunnel.net";
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
178
287
|
// src/commands/init.ts
|
|
179
288
|
var init_exports = {};
|
|
180
289
|
__export(init_exports, {
|
|
181
290
|
initCommand: () => initCommand
|
|
182
291
|
});
|
|
183
|
-
import {
|
|
184
|
-
import { join as join2 } from "path";
|
|
292
|
+
import { cpSync, existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync } from "fs";
|
|
293
|
+
import { join as join2, dirname } from "path";
|
|
294
|
+
import { fileURLToPath } from "url";
|
|
185
295
|
import pc4 from "picocolors";
|
|
186
|
-
|
|
296
|
+
import select2 from "@inquirer/select";
|
|
297
|
+
import input from "@inquirer/input";
|
|
298
|
+
function getTemplatesDir() {
|
|
299
|
+
const dir = join2(__dirname, "..", "templates");
|
|
300
|
+
if (!existsSync(dir)) {
|
|
301
|
+
throw new Error(`Templates directory not found at ${dir}`);
|
|
302
|
+
}
|
|
303
|
+
return dir;
|
|
304
|
+
}
|
|
305
|
+
function listTemplates() {
|
|
306
|
+
const root = getTemplatesDir();
|
|
307
|
+
return readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
308
|
+
const configPath = join2(root, d.name, "template.json");
|
|
309
|
+
const config = existsSync(configPath) ? JSON.parse(readFileSync2(configPath, "utf-8")) : { name: d.name, description: "", products: [] };
|
|
310
|
+
return { dir: d.name, config };
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function formatInterval(interval, count) {
|
|
314
|
+
if (!interval) return " one-time";
|
|
315
|
+
const label = INTERVAL_LABELS[interval]?.[count];
|
|
316
|
+
if (label) return `/${label}`;
|
|
317
|
+
return `/${count} ${interval}${count > 1 ? "s" : ""}`;
|
|
318
|
+
}
|
|
319
|
+
function formatPrice(price) {
|
|
320
|
+
const amount = (price.amount / 100).toFixed(2);
|
|
321
|
+
const currency = price.currency.toUpperCase();
|
|
322
|
+
const interval = formatInterval(price.interval, price.intervalCount);
|
|
323
|
+
const productName = price.name || "Unnamed product";
|
|
324
|
+
const priceName = price.priceName ? ` \u2014 ${price.priceName}` : "";
|
|
325
|
+
return `${currency} ${amount}${interval} ${pc4.dim(`${productName}${priceName}`)} ${pc4.dim(`(${price.id})`)}`;
|
|
326
|
+
}
|
|
327
|
+
function formatStoreName(store) {
|
|
328
|
+
const test = store.isTestMode ? pc4.yellow(" (test)") : "";
|
|
329
|
+
return `${store.name || store.type}${test}`;
|
|
330
|
+
}
|
|
331
|
+
async function initCommand(nameArg) {
|
|
187
332
|
const creds = requireAuth();
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
333
|
+
const apiOpts = { token: creds.token };
|
|
334
|
+
const name = nameArg?.trim() || await input({
|
|
335
|
+
message: "Funnel name",
|
|
336
|
+
validate: (value) => {
|
|
337
|
+
if (!value.trim()) return "Name is required";
|
|
338
|
+
if (!/^[a-z0-9-]+$/.test(value.trim()))
|
|
339
|
+
return "Use lowercase letters, numbers, and hyphens only";
|
|
340
|
+
if (existsSync(join2(process.cwd(), value.trim())))
|
|
341
|
+
return `Directory '${value.trim()}' already exists`;
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
if (nameArg) {
|
|
346
|
+
if (!/^[a-z0-9-]+$/.test(name))
|
|
347
|
+
throw new Error("Name must be lowercase letters, numbers, and hyphens only");
|
|
348
|
+
if (existsSync(join2(process.cwd(), name)))
|
|
349
|
+
throw new Error(`Directory '${name}' already exists`);
|
|
192
350
|
}
|
|
351
|
+
const dir = join2(process.cwd(), name.trim());
|
|
193
352
|
const projectId = await promptForProject(creds.token);
|
|
194
353
|
const projects = await fetchProjects(creds.token);
|
|
195
354
|
const project = projects.find((p) => p.id === projectId);
|
|
355
|
+
const templates = listTemplates();
|
|
356
|
+
const selectedDir = await select2({
|
|
357
|
+
message: "Choose a template",
|
|
358
|
+
choices: templates.map((t) => ({
|
|
359
|
+
name: t.config.description ? `${t.config.name} \u2014 ${pc4.dim(t.config.description)}` : t.config.name,
|
|
360
|
+
value: t.dir
|
|
361
|
+
}))
|
|
362
|
+
});
|
|
363
|
+
const chosen = templates.find((t) => t.dir === selectedDir);
|
|
364
|
+
const templateConfig = chosen.config;
|
|
365
|
+
const templateDir = join2(getTemplatesDir(), chosen.dir);
|
|
366
|
+
const productBindings = [];
|
|
367
|
+
if (templateConfig.products.length > 0) {
|
|
368
|
+
const storesSpinner = spinner("Fetching stores...");
|
|
369
|
+
const stores = await fetchStores(projectId, apiOpts);
|
|
370
|
+
storesSpinner.stop();
|
|
371
|
+
if (stores.length === 0) {
|
|
372
|
+
warn("No stores found for this project.");
|
|
373
|
+
info("Products will use empty IDs \u2014 create a store in the dashboard and update appfunnel.config.ts.");
|
|
374
|
+
} else {
|
|
375
|
+
let store;
|
|
376
|
+
if (stores.length === 1) {
|
|
377
|
+
store = stores[0];
|
|
378
|
+
info(`Using store: ${formatStoreName(store)}`);
|
|
379
|
+
} else {
|
|
380
|
+
const storeId = await select2({
|
|
381
|
+
message: "Choose a store",
|
|
382
|
+
choices: stores.map((s2) => ({
|
|
383
|
+
name: formatStoreName(s2),
|
|
384
|
+
value: s2.id
|
|
385
|
+
}))
|
|
386
|
+
});
|
|
387
|
+
store = stores.find((s2) => s2.id === storeId);
|
|
388
|
+
}
|
|
389
|
+
const pricesSpinner = spinner("Fetching prices...");
|
|
390
|
+
const prices = await fetchStorePrices(projectId, store.id, apiOpts);
|
|
391
|
+
pricesSpinner.stop();
|
|
392
|
+
if (prices.length === 0) {
|
|
393
|
+
warn("No prices found in this store.");
|
|
394
|
+
info("Import prices in the dashboard, then update appfunnel.config.ts.");
|
|
395
|
+
} else {
|
|
396
|
+
console.log();
|
|
397
|
+
info(`Configuring ${templateConfig.products.length} product(s)...`);
|
|
398
|
+
console.log();
|
|
399
|
+
for (const product of templateConfig.products) {
|
|
400
|
+
if (product.description) {
|
|
401
|
+
console.log(` ${pc4.dim(product.description)}`);
|
|
402
|
+
}
|
|
403
|
+
const storePriceId = await select2({
|
|
404
|
+
message: `${product.label} \u2014 select a price`,
|
|
405
|
+
choices: prices.map((p) => ({
|
|
406
|
+
name: formatPrice(p),
|
|
407
|
+
value: p.id
|
|
408
|
+
}))
|
|
409
|
+
});
|
|
410
|
+
const binding = {
|
|
411
|
+
id: product.id,
|
|
412
|
+
name: product.id,
|
|
413
|
+
storePriceId
|
|
414
|
+
};
|
|
415
|
+
if (product.paidTrial) {
|
|
416
|
+
const trialStorePriceId = await select2({
|
|
417
|
+
message: `${product.label} \u2014 select a trial price`,
|
|
418
|
+
choices: [
|
|
419
|
+
...prices.map((p) => ({
|
|
420
|
+
name: formatPrice(p),
|
|
421
|
+
value: p.id
|
|
422
|
+
})),
|
|
423
|
+
{ name: pc4.dim("Skip (no trial)"), value: "" }
|
|
424
|
+
]
|
|
425
|
+
});
|
|
426
|
+
if (trialStorePriceId) {
|
|
427
|
+
binding.trialStorePriceId = trialStorePriceId;
|
|
428
|
+
const trialDaysStr = await select2({
|
|
429
|
+
message: `${product.label} \u2014 trial duration`,
|
|
430
|
+
choices: [
|
|
431
|
+
{ name: "3 days", value: "3" },
|
|
432
|
+
{ name: "7 days", value: "7" },
|
|
433
|
+
{ name: "14 days", value: "14" },
|
|
434
|
+
{ name: "30 days", value: "30" },
|
|
435
|
+
{ name: "90 days", value: "90" }
|
|
436
|
+
]
|
|
437
|
+
});
|
|
438
|
+
binding.trialDays = parseInt(trialDaysStr, 10);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
productBindings.push(binding);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
196
446
|
const s = spinner(`Creating ${name}...`);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
447
|
+
cpSync(templateDir, dir, { recursive: true });
|
|
448
|
+
const templateJsonPath = join2(dir, "template.json");
|
|
449
|
+
if (existsSync(templateJsonPath)) {
|
|
450
|
+
const { unlinkSync } = await import("fs");
|
|
451
|
+
unlinkSync(templateJsonPath);
|
|
452
|
+
}
|
|
453
|
+
const configPath = join2(dir, "appfunnel.config.ts");
|
|
454
|
+
if (existsSync(configPath)) {
|
|
455
|
+
let config = readFileSync2(configPath, "utf-8");
|
|
456
|
+
config = config.replace("__PROJECT_ID__", projectId);
|
|
457
|
+
config = config.replace("__NAME__", name);
|
|
458
|
+
if (productBindings.length > 0) {
|
|
459
|
+
const itemsStr = productBindings.map((b) => {
|
|
460
|
+
const fields = [
|
|
461
|
+
` { id: '${b.id}'`,
|
|
462
|
+
`name: '${b.name}'`,
|
|
463
|
+
`storePriceId: '${b.storePriceId}'`
|
|
464
|
+
];
|
|
465
|
+
if (b.trialDays) fields.push(`trialDays: ${b.trialDays}`);
|
|
466
|
+
if (b.trialStorePriceId)
|
|
467
|
+
fields.push(`trialStorePriceId: '${b.trialStorePriceId}'`);
|
|
468
|
+
return fields.join(", ") + " }";
|
|
469
|
+
}).join(",\n");
|
|
470
|
+
const defaultId = productBindings[0].id;
|
|
471
|
+
config = config.replace(
|
|
472
|
+
/products:\s*\{[^}]*items:\s*\[[^\]]*\][^}]*\}/s,
|
|
473
|
+
`products: {
|
|
474
|
+
items: [
|
|
475
|
+
${itemsStr},
|
|
476
|
+
],
|
|
477
|
+
defaultId: '${defaultId}',
|
|
478
|
+
}`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
writeFileSync2(configPath, config);
|
|
482
|
+
}
|
|
483
|
+
const sdkVersion = `^${"0.8.0"}`;
|
|
200
484
|
writeFileSync2(
|
|
201
485
|
join2(dir, "package.json"),
|
|
202
486
|
JSON.stringify(
|
|
@@ -211,12 +495,12 @@ async function initCommand(name) {
|
|
|
211
495
|
publish: "appfunnel publish"
|
|
212
496
|
},
|
|
213
497
|
dependencies: {
|
|
214
|
-
"@appfunnel-dev/sdk":
|
|
498
|
+
"@appfunnel-dev/sdk": sdkVersion,
|
|
215
499
|
react: "^18.3.0",
|
|
216
500
|
"react-dom": "^18.3.0"
|
|
217
501
|
},
|
|
218
502
|
devDependencies: {
|
|
219
|
-
appfunnel:
|
|
503
|
+
appfunnel: sdkVersion,
|
|
220
504
|
typescript: "^5.4.0",
|
|
221
505
|
"@types/react": "^18.2.0",
|
|
222
506
|
"@types/react-dom": "^18.2.0",
|
|
@@ -230,112 +514,6 @@ async function initCommand(name) {
|
|
|
230
514
|
2
|
|
231
515
|
) + "\n"
|
|
232
516
|
);
|
|
233
|
-
writeFileSync2(
|
|
234
|
-
join2(dir, "tsconfig.json"),
|
|
235
|
-
JSON.stringify(
|
|
236
|
-
{
|
|
237
|
-
compilerOptions: {
|
|
238
|
-
target: "ES2020",
|
|
239
|
-
module: "ESNext",
|
|
240
|
-
moduleResolution: "bundler",
|
|
241
|
-
jsx: "react-jsx",
|
|
242
|
-
strict: true,
|
|
243
|
-
esModuleInterop: true,
|
|
244
|
-
skipLibCheck: true,
|
|
245
|
-
paths: {
|
|
246
|
-
"@/*": ["./src/*"]
|
|
247
|
-
},
|
|
248
|
-
baseUrl: "."
|
|
249
|
-
},
|
|
250
|
-
include: ["src"]
|
|
251
|
-
},
|
|
252
|
-
null,
|
|
253
|
-
2
|
|
254
|
-
) + "\n"
|
|
255
|
-
);
|
|
256
|
-
writeFileSync2(join2(dir, "src", "app.css"), `@import "tailwindcss";
|
|
257
|
-
`);
|
|
258
|
-
writeFileSync2(
|
|
259
|
-
join2(dir, "appfunnel.config.ts"),
|
|
260
|
-
`import { defineConfig } from '@appfunnel-dev/sdk'
|
|
261
|
-
|
|
262
|
-
export default defineConfig({
|
|
263
|
-
projectId: '${projectId}',
|
|
264
|
-
name: '${name}',
|
|
265
|
-
defaultLocale: 'en',
|
|
266
|
-
|
|
267
|
-
responses: {
|
|
268
|
-
goal: { type: 'string' },
|
|
269
|
-
},
|
|
270
|
-
|
|
271
|
-
products: {
|
|
272
|
-
items: [],
|
|
273
|
-
},
|
|
274
|
-
})
|
|
275
|
-
`
|
|
276
|
-
);
|
|
277
|
-
writeFileSync2(
|
|
278
|
-
join2(dir, "src", "funnel.tsx"),
|
|
279
|
-
`import './app.css'
|
|
280
|
-
|
|
281
|
-
export default function Funnel({ children }: { children: React.ReactNode }) {
|
|
282
|
-
return (
|
|
283
|
-
<div className="min-h-screen">
|
|
284
|
-
{children}
|
|
285
|
-
</div>
|
|
286
|
-
)
|
|
287
|
-
}
|
|
288
|
-
`
|
|
289
|
-
);
|
|
290
|
-
writeFileSync2(
|
|
291
|
-
join2(dir, "src", "pages", "index.tsx"),
|
|
292
|
-
`import { definePage, useResponse, useNavigation } from '@appfunnel-dev/sdk'
|
|
293
|
-
|
|
294
|
-
export const page = definePage({
|
|
295
|
-
name: 'Landing',
|
|
296
|
-
type: 'default',
|
|
297
|
-
routes: [],
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
export default function Landing() {
|
|
301
|
-
const [goal, setGoal] = useResponse<string>('goal')
|
|
302
|
-
const { goToNextPage } = useNavigation()
|
|
303
|
-
|
|
304
|
-
return (
|
|
305
|
-
<div className="flex min-h-screen items-center justify-center p-4">
|
|
306
|
-
<div className="w-full max-w-md space-y-6">
|
|
307
|
-
<h1 className="text-3xl font-bold text-center">Welcome</h1>
|
|
308
|
-
<input
|
|
309
|
-
type="text"
|
|
310
|
-
value={goal}
|
|
311
|
-
onChange={(e) => setGoal(e.target.value)}
|
|
312
|
-
placeholder="What's your goal?"
|
|
313
|
-
className="w-full rounded-xl border p-4 text-lg"
|
|
314
|
-
/>
|
|
315
|
-
<button
|
|
316
|
-
onClick={goToNextPage}
|
|
317
|
-
disabled={!goal.trim()}
|
|
318
|
-
className="w-full rounded-xl bg-blue-600 py-4 text-lg font-bold text-white disabled:opacity-50"
|
|
319
|
-
>
|
|
320
|
-
Continue
|
|
321
|
-
</button>
|
|
322
|
-
</div>
|
|
323
|
-
</div>
|
|
324
|
-
)
|
|
325
|
-
}
|
|
326
|
-
`
|
|
327
|
-
);
|
|
328
|
-
writeFileSync2(
|
|
329
|
-
join2(dir, "locales", "en.json"),
|
|
330
|
-
JSON.stringify({ welcome: "Welcome" }, null, 2) + "\n"
|
|
331
|
-
);
|
|
332
|
-
writeFileSync2(
|
|
333
|
-
join2(dir, ".gitignore"),
|
|
334
|
-
`node_modules
|
|
335
|
-
dist
|
|
336
|
-
.appfunnel
|
|
337
|
-
`
|
|
338
|
-
);
|
|
339
517
|
s.stop();
|
|
340
518
|
console.log();
|
|
341
519
|
success(`Created ${pc4.bold(name)} for project ${pc4.bold(project.name)}`);
|
|
@@ -345,12 +523,21 @@ dist
|
|
|
345
523
|
console.log(` ${pc4.dim("appfunnel dev")}`);
|
|
346
524
|
console.log();
|
|
347
525
|
}
|
|
526
|
+
var __dirname, INTERVAL_LABELS;
|
|
348
527
|
var init_init = __esm({
|
|
349
528
|
"src/commands/init.ts"() {
|
|
350
529
|
"use strict";
|
|
351
530
|
init_logger();
|
|
352
531
|
init_auth();
|
|
353
532
|
init_projects();
|
|
533
|
+
init_api();
|
|
534
|
+
__dirname = dirname(fileURLToPath(import.meta.url));
|
|
535
|
+
INTERVAL_LABELS = {
|
|
536
|
+
day: { 1: "day", 7: "week", 14: "2 weeks", 30: "month" },
|
|
537
|
+
week: { 1: "week", 2: "2 weeks", 4: "month", 12: "quarter", 52: "year" },
|
|
538
|
+
month: { 1: "month", 3: "quarter", 6: "6 months", 12: "year" },
|
|
539
|
+
year: { 1: "year" }
|
|
540
|
+
};
|
|
354
541
|
}
|
|
355
542
|
});
|
|
356
543
|
|
|
@@ -449,7 +636,7 @@ async function whoamiCommand() {
|
|
|
449
636
|
const creds = requireAuth();
|
|
450
637
|
const spin = spinner("Verifying credentials\u2026");
|
|
451
638
|
try {
|
|
452
|
-
const response = await fetch(`${
|
|
639
|
+
const response = await fetch(`${DEFAULT_API_BASE3}/user`, {
|
|
453
640
|
headers: {
|
|
454
641
|
Authorization: creds.token,
|
|
455
642
|
"Content-Type": "application/json"
|
|
@@ -480,19 +667,19 @@ async function whoamiCommand() {
|
|
|
480
667
|
);
|
|
481
668
|
}
|
|
482
669
|
}
|
|
483
|
-
var
|
|
670
|
+
var DEFAULT_API_BASE3;
|
|
484
671
|
var init_whoami = __esm({
|
|
485
672
|
"src/commands/whoami.ts"() {
|
|
486
673
|
"use strict";
|
|
487
674
|
init_auth();
|
|
488
675
|
init_errors();
|
|
489
676
|
init_logger();
|
|
490
|
-
|
|
677
|
+
DEFAULT_API_BASE3 = "https://api.appfunnel.net";
|
|
491
678
|
}
|
|
492
679
|
});
|
|
493
680
|
|
|
494
681
|
// src/lib/config.ts
|
|
495
|
-
import { existsSync as existsSync2, readFileSync as
|
|
682
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
496
683
|
import { join as join3, resolve } from "path";
|
|
497
684
|
async function loadConfig(cwd) {
|
|
498
685
|
const configPath = join3(cwd, CONFIG_FILE);
|
|
@@ -504,7 +691,7 @@ async function loadConfig(cwd) {
|
|
|
504
691
|
);
|
|
505
692
|
}
|
|
506
693
|
const { transform } = await import("esbuild");
|
|
507
|
-
const raw =
|
|
694
|
+
const raw = readFileSync3(configPath, "utf-8");
|
|
508
695
|
const result = await transform(raw, {
|
|
509
696
|
loader: "ts",
|
|
510
697
|
format: "esm",
|
|
@@ -536,10 +723,10 @@ var init_config = __esm({
|
|
|
536
723
|
});
|
|
537
724
|
|
|
538
725
|
// src/lib/version.ts
|
|
539
|
-
import { readFileSync as
|
|
726
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
540
727
|
import { join as join4 } from "path";
|
|
541
728
|
function checkVersionCompatibility(cwd) {
|
|
542
|
-
const cliVersion = "0.
|
|
729
|
+
const cliVersion = "0.8.0";
|
|
543
730
|
const sdkVersion = getSdkVersion(cwd);
|
|
544
731
|
const [cliMajor, cliMinor] = cliVersion.split(".").map(Number);
|
|
545
732
|
const [sdkMajor, sdkMinor] = sdkVersion.split(".").map(Number);
|
|
@@ -560,7 +747,7 @@ function getSdkVersion(cwd) {
|
|
|
560
747
|
"sdk",
|
|
561
748
|
"package.json"
|
|
562
749
|
);
|
|
563
|
-
const pkg = JSON.parse(
|
|
750
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
564
751
|
return pkg.version;
|
|
565
752
|
} catch {
|
|
566
753
|
throw new CLIError(
|
|
@@ -578,7 +765,7 @@ var init_version = __esm({
|
|
|
578
765
|
});
|
|
579
766
|
|
|
580
767
|
// src/extract/pages.ts
|
|
581
|
-
import { readdirSync, readFileSync as
|
|
768
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync5, existsSync as existsSync3 } from "fs";
|
|
582
769
|
import { join as join5, basename } from "path";
|
|
583
770
|
function scanPages(cwd) {
|
|
584
771
|
const pagesDir = resolvePagesDir(cwd);
|
|
@@ -589,7 +776,7 @@ function scanPages(cwd) {
|
|
|
589
776
|
"Create src/pages/ and add at least one .tsx page file."
|
|
590
777
|
);
|
|
591
778
|
}
|
|
592
|
-
const files =
|
|
779
|
+
const files = readdirSync2(pagesDir).filter((f) => f.endsWith(".tsx") && !f.startsWith("_")).map((f) => basename(f, ".tsx")).sort();
|
|
593
780
|
if (files.length === 0) {
|
|
594
781
|
throw new CLIError(
|
|
595
782
|
"NO_PAGES",
|
|
@@ -605,7 +792,7 @@ async function extractPageDefinitions(cwd, pageKeys) {
|
|
|
605
792
|
const result = {};
|
|
606
793
|
for (const key of pageKeys) {
|
|
607
794
|
const filePath = join5(pagesDir, `${key}.tsx`);
|
|
608
|
-
const source =
|
|
795
|
+
const source = readFileSync5(filePath, "utf-8");
|
|
609
796
|
const definition = extractDefinePage(ts, source, filePath);
|
|
610
797
|
if (definition) {
|
|
611
798
|
result[key] = definition;
|
|
@@ -696,7 +883,7 @@ var init_pages = __esm({
|
|
|
696
883
|
import { join as join6 } from "path";
|
|
697
884
|
import { existsSync as existsSync4 } from "fs";
|
|
698
885
|
function generateEntrySource(options) {
|
|
699
|
-
const { config, pages, pagesDir, funnelTsxPath, isDev } = options;
|
|
886
|
+
const { config, pages, pagesDir, funnelTsxPath, isDev, translations } = options;
|
|
700
887
|
const pageKeys = Object.keys(pages);
|
|
701
888
|
const mergedPages = {};
|
|
702
889
|
const mergedRoutes = {};
|
|
@@ -754,6 +941,8 @@ ${pageImports}
|
|
|
754
941
|
|
|
755
942
|
${priceDataCode}
|
|
756
943
|
|
|
944
|
+
const translations = ${translations ? JSON.stringify(translations) : "undefined"}
|
|
945
|
+
|
|
757
946
|
const config = ${JSON.stringify(fullConfig, null, 2)}
|
|
758
947
|
|
|
759
948
|
const keyToSlug = ${JSON.stringify(
|
|
@@ -762,7 +951,7 @@ const keyToSlug = ${JSON.stringify(
|
|
|
762
951
|
const slugToKey = ${JSON.stringify(slugMap)}
|
|
763
952
|
|
|
764
953
|
const DEV_CAMPAIGN_SLUG = 'campaign'
|
|
765
|
-
const DEFAULT_INITIAL = '${config.initialPageKey
|
|
954
|
+
const DEFAULT_INITIAL = '${config.initialPageKey}'
|
|
766
955
|
|
|
767
956
|
/**
|
|
768
957
|
* Parse the URL to extract basePath, campaignSlug, and initial page.
|
|
@@ -910,6 +1099,7 @@ function App() {
|
|
|
910
1099
|
campaignSlug={campaignSlug}
|
|
911
1100
|
priceData={priceData}
|
|
912
1101
|
sessionData={sessionData}
|
|
1102
|
+
translations={translations}
|
|
913
1103
|
>
|
|
914
1104
|
<FunnelWrapper>
|
|
915
1105
|
<PageRenderer />
|
|
@@ -959,7 +1149,24 @@ var init_html = __esm({
|
|
|
959
1149
|
|
|
960
1150
|
// src/vite/plugin.ts
|
|
961
1151
|
import { resolve as resolve2, join as join7 } from "path";
|
|
962
|
-
import { existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as
|
|
1152
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
|
|
1153
|
+
function loadTranslations(cwd) {
|
|
1154
|
+
const localesDir = join7(cwd, "locales");
|
|
1155
|
+
if (!existsSync5(localesDir)) return void 0;
|
|
1156
|
+
const translations = {};
|
|
1157
|
+
let hasAny = false;
|
|
1158
|
+
for (const file of readdirSync3(localesDir)) {
|
|
1159
|
+
if (!file.endsWith(".json")) continue;
|
|
1160
|
+
const locale = file.replace(/\.json$/, "");
|
|
1161
|
+
try {
|
|
1162
|
+
const content = readFileSync6(join7(localesDir, file), "utf-8");
|
|
1163
|
+
translations[locale] = JSON.parse(content);
|
|
1164
|
+
hasAny = true;
|
|
1165
|
+
} catch {
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return hasAny ? translations : void 0;
|
|
1169
|
+
}
|
|
963
1170
|
function appfunnelPlugin(options) {
|
|
964
1171
|
const { cwd, config, isDev } = options;
|
|
965
1172
|
let pages = options.pages;
|
|
@@ -967,6 +1174,7 @@ function appfunnelPlugin(options) {
|
|
|
967
1174
|
const funnelTsxPath = resolve2(cwd, "src", "funnel.tsx");
|
|
968
1175
|
const appfunnelDir = join7(cwd, APPFUNNEL_DIR);
|
|
969
1176
|
const htmlPath = join7(appfunnelDir, "index.html");
|
|
1177
|
+
const localesDir = join7(cwd, "locales");
|
|
970
1178
|
function getEntrySource() {
|
|
971
1179
|
return generateEntrySource({
|
|
972
1180
|
config,
|
|
@@ -974,13 +1182,14 @@ function appfunnelPlugin(options) {
|
|
|
974
1182
|
pagesDir,
|
|
975
1183
|
funnelTsxPath,
|
|
976
1184
|
isDev,
|
|
977
|
-
priceData: options.priceData
|
|
1185
|
+
priceData: options.priceData,
|
|
1186
|
+
translations: loadTranslations(cwd)
|
|
978
1187
|
});
|
|
979
1188
|
}
|
|
980
1189
|
return {
|
|
981
1190
|
name: "appfunnel",
|
|
982
1191
|
config() {
|
|
983
|
-
|
|
1192
|
+
mkdirSync2(appfunnelDir, { recursive: true });
|
|
984
1193
|
writeFileSync3(htmlPath, generateHtml(config.name || "AppFunnel"));
|
|
985
1194
|
return {
|
|
986
1195
|
// Don't let Vite auto-serve index.html — we handle it ourselves
|
|
@@ -988,7 +1197,8 @@ function appfunnelPlugin(options) {
|
|
|
988
1197
|
resolve: {
|
|
989
1198
|
alias: {
|
|
990
1199
|
"@": resolve2(cwd, "src")
|
|
991
|
-
}
|
|
1200
|
+
},
|
|
1201
|
+
dedupe: ["react", "react-dom"]
|
|
992
1202
|
},
|
|
993
1203
|
esbuild: {
|
|
994
1204
|
jsx: "automatic"
|
|
@@ -1050,6 +1260,18 @@ function appfunnelPlugin(options) {
|
|
|
1050
1260
|
}
|
|
1051
1261
|
});
|
|
1052
1262
|
}
|
|
1263
|
+
if (existsSync5(localesDir)) {
|
|
1264
|
+
watcher.add(localesDir);
|
|
1265
|
+
watcher.on("change", (file) => {
|
|
1266
|
+
if (file.startsWith(localesDir) && file.endsWith(".json")) {
|
|
1267
|
+
const mod = devServer.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ENTRY_ID);
|
|
1268
|
+
if (mod) {
|
|
1269
|
+
devServer.moduleGraph.invalidateModule(mod);
|
|
1270
|
+
}
|
|
1271
|
+
devServer.ws.send({ type: "full-reload" });
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1053
1275
|
return () => {
|
|
1054
1276
|
devServer.middlewares.use(async (req, res, next) => {
|
|
1055
1277
|
const url = req.url?.split("?")[0] || "";
|
|
@@ -1057,7 +1279,7 @@ function appfunnelPlugin(options) {
|
|
|
1057
1279
|
return next();
|
|
1058
1280
|
}
|
|
1059
1281
|
try {
|
|
1060
|
-
const rawHtml =
|
|
1282
|
+
const rawHtml = readFileSync6(htmlPath, "utf-8");
|
|
1061
1283
|
const html = await devServer.transformIndexHtml(req.url || "/", rawHtml);
|
|
1062
1284
|
res.statusCode = 200;
|
|
1063
1285
|
res.setHeader("Content-Type", "text/html");
|
|
@@ -1085,104 +1307,12 @@ var init_plugin = __esm({
|
|
|
1085
1307
|
}
|
|
1086
1308
|
});
|
|
1087
1309
|
|
|
1088
|
-
// src/lib/api.ts
|
|
1089
|
-
async function apiFetch(path, options) {
|
|
1090
|
-
const { token, apiBaseUrl, ...fetchOpts } = options;
|
|
1091
|
-
const base = apiBaseUrl || DEFAULT_API_BASE3;
|
|
1092
|
-
const url = `${base}${path}`;
|
|
1093
|
-
const isFormData = fetchOpts.body instanceof FormData;
|
|
1094
|
-
const headers = {
|
|
1095
|
-
Authorization: token,
|
|
1096
|
-
...fetchOpts.headers || {}
|
|
1097
|
-
};
|
|
1098
|
-
if (!isFormData) {
|
|
1099
|
-
headers["Content-Type"] = "application/json";
|
|
1100
|
-
}
|
|
1101
|
-
const response = await fetch(url, {
|
|
1102
|
-
...fetchOpts,
|
|
1103
|
-
headers
|
|
1104
|
-
});
|
|
1105
|
-
if (!response.ok) {
|
|
1106
|
-
const body = await response.text().catch(() => "");
|
|
1107
|
-
let message = `API request failed: ${response.status} ${response.statusText}`;
|
|
1108
|
-
try {
|
|
1109
|
-
const parsed = JSON.parse(body);
|
|
1110
|
-
if (parsed.error) message = parsed.error;
|
|
1111
|
-
if (parsed.message) message = parsed.message;
|
|
1112
|
-
} catch {
|
|
1113
|
-
}
|
|
1114
|
-
const error2 = new CLIError("API_ERROR", message);
|
|
1115
|
-
error2.statusCode = response.status;
|
|
1116
|
-
throw error2;
|
|
1117
|
-
}
|
|
1118
|
-
return response;
|
|
1119
|
-
}
|
|
1120
|
-
async function fetchPrices(projectId, storePriceIds, options) {
|
|
1121
|
-
if (storePriceIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1122
|
-
const response = await apiFetch(`/project/${projectId}/headless/prices`, {
|
|
1123
|
-
...options,
|
|
1124
|
-
method: "POST",
|
|
1125
|
-
body: JSON.stringify({ storePriceIds })
|
|
1126
|
-
});
|
|
1127
|
-
const data = await response.json();
|
|
1128
|
-
return new Map(Object.entries(data.prices || {}));
|
|
1129
|
-
}
|
|
1130
|
-
async function publishBuild(projectId, funnelId, manifest, assets, options) {
|
|
1131
|
-
const formData = new FormData();
|
|
1132
|
-
formData.set("manifest", JSON.stringify(manifest));
|
|
1133
|
-
if (funnelId) {
|
|
1134
|
-
formData.set("funnelId", funnelId);
|
|
1135
|
-
}
|
|
1136
|
-
for (const asset of assets) {
|
|
1137
|
-
formData.append(
|
|
1138
|
-
"assets",
|
|
1139
|
-
new Blob([new Uint8Array(asset.content)], { type: asset.contentType }),
|
|
1140
|
-
asset.path
|
|
1141
|
-
);
|
|
1142
|
-
}
|
|
1143
|
-
try {
|
|
1144
|
-
const response = await apiFetch(`/project/${projectId}/headless/publish`, {
|
|
1145
|
-
...options,
|
|
1146
|
-
method: "POST",
|
|
1147
|
-
body: formData
|
|
1148
|
-
});
|
|
1149
|
-
return await response.json();
|
|
1150
|
-
} catch (err) {
|
|
1151
|
-
if (err instanceof CLIError && err.code === "API_ERROR") {
|
|
1152
|
-
if (err.statusCode === 413) {
|
|
1153
|
-
throw new CLIError(
|
|
1154
|
-
"BUNDLE_TOO_LARGE",
|
|
1155
|
-
err.message,
|
|
1156
|
-
"Reduce page bundle sizes. Check for large dependencies."
|
|
1157
|
-
);
|
|
1158
|
-
}
|
|
1159
|
-
if (err.statusCode === 409) {
|
|
1160
|
-
throw new CLIError(
|
|
1161
|
-
"FUNNEL_NOT_HEADLESS",
|
|
1162
|
-
err.message,
|
|
1163
|
-
"Remove funnelId from config to create a new headless funnel."
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
throw new CLIError("PUBLISH_FAILED", err.message);
|
|
1167
|
-
}
|
|
1168
|
-
throw err;
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
var DEFAULT_API_BASE3;
|
|
1172
|
-
var init_api = __esm({
|
|
1173
|
-
"src/lib/api.ts"() {
|
|
1174
|
-
"use strict";
|
|
1175
|
-
init_errors();
|
|
1176
|
-
DEFAULT_API_BASE3 = "https://api.appfunnel.net";
|
|
1177
|
-
}
|
|
1178
|
-
});
|
|
1179
|
-
|
|
1180
1310
|
// src/commands/dev.ts
|
|
1181
1311
|
var dev_exports = {};
|
|
1182
1312
|
__export(dev_exports, {
|
|
1183
1313
|
devCommand: () => devCommand
|
|
1184
1314
|
});
|
|
1185
|
-
import { readFileSync as
|
|
1315
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
1186
1316
|
import { join as join8 } from "path";
|
|
1187
1317
|
import pc6 from "picocolors";
|
|
1188
1318
|
async function devCommand(options) {
|
|
@@ -1198,7 +1328,7 @@ async function devCommand(options) {
|
|
|
1198
1328
|
const projectId = await promptForProject(creds.token);
|
|
1199
1329
|
config.projectId = projectId;
|
|
1200
1330
|
const configPath = join8(cwd, "appfunnel.config.ts");
|
|
1201
|
-
const configSource =
|
|
1331
|
+
const configSource = readFileSync7(configPath, "utf-8");
|
|
1202
1332
|
let updated;
|
|
1203
1333
|
if (/projectId:\s*['"]/.test(configSource)) {
|
|
1204
1334
|
updated = configSource.replace(
|
|
@@ -1320,7 +1450,7 @@ __export(build_exports, {
|
|
|
1320
1450
|
});
|
|
1321
1451
|
import { resolve as resolve3, join as join9 } from "path";
|
|
1322
1452
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1323
|
-
import { readFileSync as
|
|
1453
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, statSync, readdirSync as readdirSync4 } from "fs";
|
|
1324
1454
|
import pc7 from "picocolors";
|
|
1325
1455
|
async function buildCommand() {
|
|
1326
1456
|
const cwd = process.cwd();
|
|
@@ -1336,6 +1466,14 @@ async function buildCommand() {
|
|
|
1336
1466
|
"Run 'appfunnel dev' first to select a project, or add projectId manually."
|
|
1337
1467
|
);
|
|
1338
1468
|
}
|
|
1469
|
+
if (!config.initialPageKey) {
|
|
1470
|
+
s.stop();
|
|
1471
|
+
throw new CLIError(
|
|
1472
|
+
"MISSING_INITIAL_PAGE_KEY",
|
|
1473
|
+
"Missing initialPageKey in appfunnel.config.ts.",
|
|
1474
|
+
"Set initialPageKey to the page key (filename without .tsx) of your first page."
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1339
1477
|
const pageKeys = scanPages(cwd);
|
|
1340
1478
|
const pages = await extractPageDefinitions(cwd, pageKeys);
|
|
1341
1479
|
s.stop();
|
|
@@ -1439,7 +1577,7 @@ async function buildCommand() {
|
|
|
1439
1577
|
console.log(` ${pc7.dim("Pages:")} ${pageKeys.length}`);
|
|
1440
1578
|
console.log(` ${pc7.dim("Size:")} ${formatSize(totalSize)}`);
|
|
1441
1579
|
console.log();
|
|
1442
|
-
for (const asset of assets
|
|
1580
|
+
for (const asset of assets) {
|
|
1443
1581
|
const sizeStr = formatSize(asset.size);
|
|
1444
1582
|
const isOver = asset.size > MAX_PAGE_SIZE;
|
|
1445
1583
|
console.log(` ${isOver ? pc7.yellow("!") : pc7.dim("\xB7")} ${pc7.dim(asset.path)} ${isOver ? pc7.yellow(sizeStr) : pc7.dim(sizeStr)}`);
|
|
@@ -1531,7 +1669,7 @@ function validateConditionVariables(condition, pageKey, allVariables) {
|
|
|
1531
1669
|
function collectAssets(outDir) {
|
|
1532
1670
|
const assets = [];
|
|
1533
1671
|
function walk(dir, prefix = "") {
|
|
1534
|
-
for (const entry of
|
|
1672
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
1535
1673
|
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1536
1674
|
const fullPath = join9(dir, entry.name);
|
|
1537
1675
|
if (entry.isDirectory()) {
|
|
@@ -1551,7 +1689,7 @@ function formatSize(bytes) {
|
|
|
1551
1689
|
}
|
|
1552
1690
|
function getSdkVersion2(cwd) {
|
|
1553
1691
|
try {
|
|
1554
|
-
const pkg = JSON.parse(
|
|
1692
|
+
const pkg = JSON.parse(readFileSync8(join9(cwd, "node_modules", "@appfunnel-dev", "sdk", "package.json"), "utf-8"));
|
|
1555
1693
|
return pkg.version;
|
|
1556
1694
|
} catch {
|
|
1557
1695
|
return "0.0.0";
|
|
@@ -1584,10 +1722,10 @@ var init_build = __esm({
|
|
|
1584
1722
|
|
|
1585
1723
|
// src/lib/config-patch.ts
|
|
1586
1724
|
import { join as join10 } from "path";
|
|
1587
|
-
import { readFileSync as
|
|
1725
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
1588
1726
|
function patchConfigFunnelId(cwd, funnelId) {
|
|
1589
1727
|
const configPath = join10(cwd, "appfunnel.config.ts");
|
|
1590
|
-
let content =
|
|
1728
|
+
let content = readFileSync9(configPath, "utf-8");
|
|
1591
1729
|
if (content.includes("funnelId")) return;
|
|
1592
1730
|
const patched = content.replace(
|
|
1593
1731
|
/(projectId:\s*['"][^'"]+['"],?\s*\n)/,
|
|
@@ -1610,7 +1748,7 @@ __export(publish_exports, {
|
|
|
1610
1748
|
publishCommand: () => publishCommand
|
|
1611
1749
|
});
|
|
1612
1750
|
import { resolve as resolve4, join as join11 } from "path";
|
|
1613
|
-
import { readFileSync as
|
|
1751
|
+
import { readFileSync as readFileSync10, existsSync as existsSync6 } from "fs";
|
|
1614
1752
|
import pc8 from "picocolors";
|
|
1615
1753
|
function getMimeType(path) {
|
|
1616
1754
|
const ext = path.substring(path.lastIndexOf("."));
|
|
@@ -1621,7 +1759,7 @@ function formatSize2(bytes) {
|
|
|
1621
1759
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1622
1760
|
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
1623
1761
|
}
|
|
1624
|
-
async function publishCommand() {
|
|
1762
|
+
async function publishCommand(options) {
|
|
1625
1763
|
const cwd = process.cwd();
|
|
1626
1764
|
const creds = requireAuth();
|
|
1627
1765
|
checkVersionCompatibility(cwd);
|
|
@@ -1643,7 +1781,7 @@ async function publishCommand() {
|
|
|
1643
1781
|
"Run 'appfunnel build' first."
|
|
1644
1782
|
);
|
|
1645
1783
|
}
|
|
1646
|
-
const manifest = JSON.parse(
|
|
1784
|
+
const manifest = JSON.parse(readFileSync10(manifestPath, "utf-8"));
|
|
1647
1785
|
const assets = manifest.assets || [];
|
|
1648
1786
|
const s = spinner("Preparing assets...");
|
|
1649
1787
|
const assetPayloads = [];
|
|
@@ -1659,7 +1797,7 @@ async function publishCommand() {
|
|
|
1659
1797
|
"Run 'appfunnel build' to regenerate."
|
|
1660
1798
|
);
|
|
1661
1799
|
}
|
|
1662
|
-
const content =
|
|
1800
|
+
const content = readFileSync10(fullPath);
|
|
1663
1801
|
totalBytes += content.length;
|
|
1664
1802
|
assetPayloads.push({
|
|
1665
1803
|
path: asset.path,
|
|
@@ -1674,7 +1812,8 @@ async function publishCommand() {
|
|
|
1674
1812
|
config.funnelId || "",
|
|
1675
1813
|
manifest,
|
|
1676
1814
|
assetPayloads,
|
|
1677
|
-
{ token: creds.token }
|
|
1815
|
+
{ token: creds.token },
|
|
1816
|
+
options?.promote
|
|
1678
1817
|
);
|
|
1679
1818
|
s.stop();
|
|
1680
1819
|
if (result.created && result.funnelId) {
|
|
@@ -1682,14 +1821,18 @@ async function publishCommand() {
|
|
|
1682
1821
|
info(`Funnel created \u2014 funnelId added to appfunnel.config.ts`);
|
|
1683
1822
|
}
|
|
1684
1823
|
console.log();
|
|
1685
|
-
success("Published successfully");
|
|
1824
|
+
success(result.activated ? "Published and activated" : "Published successfully");
|
|
1686
1825
|
console.log();
|
|
1687
|
-
console.log(` ${pc8.dim("Build ID:")}
|
|
1826
|
+
console.log(` ${pc8.dim("Build ID:")} ${result.buildId}`);
|
|
1688
1827
|
if (result.funnelId && !config.funnelId) {
|
|
1689
|
-
console.log(` ${pc8.dim("Funnel:")}
|
|
1828
|
+
console.log(` ${pc8.dim("Funnel:")} ${result.funnelId}`);
|
|
1829
|
+
}
|
|
1830
|
+
console.log(` ${pc8.dim("Dashboard:")} ${pc8.cyan(result.dashboardUrl)}`);
|
|
1831
|
+
console.log(` ${pc8.dim("Assets:")} ${assets.length} files ${pc8.dim(`(${formatSize2(totalBytes)})`)}`);
|
|
1832
|
+
if (!result.activated) {
|
|
1833
|
+
console.log();
|
|
1834
|
+
console.log(` ${pc8.dim("Tip:")} Use ${pc8.cyan("--promote")} to activate immediately, or promote from the dashboard.`);
|
|
1690
1835
|
}
|
|
1691
|
-
console.log(` ${pc8.dim("URL:")} ${pc8.cyan(result.url)}`);
|
|
1692
|
-
console.log(` ${pc8.dim("Assets:")} ${assets.length} files ${pc8.dim(`(${formatSize2(totalBytes)})`)}`);
|
|
1693
1836
|
console.log();
|
|
1694
1837
|
}
|
|
1695
1838
|
var MIME_TYPES;
|
|
@@ -1722,8 +1865,8 @@ init_errors();
|
|
|
1722
1865
|
import { Command } from "commander";
|
|
1723
1866
|
import pc9 from "picocolors";
|
|
1724
1867
|
var program = new Command();
|
|
1725
|
-
program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.
|
|
1726
|
-
program.command("init
|
|
1868
|
+
program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.8.0");
|
|
1869
|
+
program.command("init").argument("[name]", "Project directory name").description("Create a new AppFunnel project").action(async (name) => {
|
|
1727
1870
|
const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1728
1871
|
await initCommand2(name);
|
|
1729
1872
|
});
|
|
@@ -1743,9 +1886,9 @@ program.command("build").description("Build the funnel for production").action(a
|
|
|
1743
1886
|
const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_build(), build_exports));
|
|
1744
1887
|
await buildCommand2();
|
|
1745
1888
|
});
|
|
1746
|
-
program.command("publish").description("Publish the build to AppFunnel").action(async () => {
|
|
1889
|
+
program.command("publish").description("Publish the build to AppFunnel").option("--promote", "Activate the build immediately after publishing").action(async (options) => {
|
|
1747
1890
|
const { publishCommand: publishCommand2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
|
|
1748
|
-
await publishCommand2();
|
|
1891
|
+
await publishCommand2({ promote: options.promote });
|
|
1749
1892
|
});
|
|
1750
1893
|
program.hook("postAction", () => {
|
|
1751
1894
|
});
|