appfunnel 0.6.0 → 0.7.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 +344 -248
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/templates/default/appfunnel.config.ts +15 -0
- package/templates/default/locales/en.json +3 -0
- package/templates/default/src/app.css +1 -0
- package/templates/default/src/funnel.tsx +9 -0
- package/templates/default/src/pages/index.tsx +37 -0
- package/templates/default/src/pages/loading.tsx +76 -0
- package/templates/default/src/pages/result.tsx +38 -0
- package/templates/default/template.json +10 -0
- package/templates/default/tsconfig.json +16 -0
package/dist/index.js
CHANGED
|
@@ -175,28 +175,308 @@ var init_projects = __esm({
|
|
|
175
175
|
}
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
+
// src/lib/api.ts
|
|
179
|
+
async function apiFetch(path, options) {
|
|
180
|
+
const { token, apiBaseUrl, ...fetchOpts } = options;
|
|
181
|
+
const base = apiBaseUrl || DEFAULT_API_BASE2;
|
|
182
|
+
const url = `${base}${path}`;
|
|
183
|
+
const isFormData = fetchOpts.body instanceof FormData;
|
|
184
|
+
const headers = {
|
|
185
|
+
Authorization: token,
|
|
186
|
+
...fetchOpts.headers || {}
|
|
187
|
+
};
|
|
188
|
+
if (!isFormData) {
|
|
189
|
+
headers["Content-Type"] = "application/json";
|
|
190
|
+
}
|
|
191
|
+
const response = await fetch(url, {
|
|
192
|
+
...fetchOpts,
|
|
193
|
+
headers
|
|
194
|
+
});
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
const body = await response.text().catch(() => "");
|
|
197
|
+
let message = `API request failed: ${response.status} ${response.statusText}`;
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(body);
|
|
200
|
+
if (parsed.error) message = parsed.error;
|
|
201
|
+
if (parsed.message) message = parsed.message;
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
const error2 = new CLIError("API_ERROR", message);
|
|
205
|
+
error2.statusCode = response.status;
|
|
206
|
+
throw error2;
|
|
207
|
+
}
|
|
208
|
+
return response;
|
|
209
|
+
}
|
|
210
|
+
async function fetchPrices(projectId, storePriceIds, options) {
|
|
211
|
+
if (storePriceIds.length === 0) return /* @__PURE__ */ new Map();
|
|
212
|
+
const response = await apiFetch(`/project/${projectId}/headless/prices`, {
|
|
213
|
+
...options,
|
|
214
|
+
method: "POST",
|
|
215
|
+
body: JSON.stringify({ storePriceIds })
|
|
216
|
+
});
|
|
217
|
+
const data = await response.json();
|
|
218
|
+
return new Map(Object.entries(data.prices || {}));
|
|
219
|
+
}
|
|
220
|
+
async function fetchStores(projectId, options) {
|
|
221
|
+
const response = await apiFetch(`/project/${projectId}/stores`, {
|
|
222
|
+
...options,
|
|
223
|
+
method: "GET"
|
|
224
|
+
});
|
|
225
|
+
const data = await response.json();
|
|
226
|
+
return data.data || [];
|
|
227
|
+
}
|
|
228
|
+
async function fetchStorePrices(projectId, storeId, options) {
|
|
229
|
+
const response = await apiFetch(
|
|
230
|
+
`/project/${projectId}/stores/${storeId}/prices`,
|
|
231
|
+
{ ...options, method: "GET" }
|
|
232
|
+
);
|
|
233
|
+
const data = await response.json();
|
|
234
|
+
return data.data || [];
|
|
235
|
+
}
|
|
236
|
+
async function publishBuild(projectId, funnelId, manifest, assets, options, promote) {
|
|
237
|
+
const formData = new FormData();
|
|
238
|
+
formData.set("manifest", JSON.stringify(manifest));
|
|
239
|
+
if (funnelId) {
|
|
240
|
+
formData.set("funnelId", funnelId);
|
|
241
|
+
}
|
|
242
|
+
if (promote) {
|
|
243
|
+
formData.set("promote", "true");
|
|
244
|
+
}
|
|
245
|
+
for (const asset of assets) {
|
|
246
|
+
formData.append(
|
|
247
|
+
"assets",
|
|
248
|
+
new Blob([new Uint8Array(asset.content)], { type: asset.contentType }),
|
|
249
|
+
asset.path
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const response = await apiFetch(`/project/${projectId}/headless/publish`, {
|
|
254
|
+
...options,
|
|
255
|
+
method: "POST",
|
|
256
|
+
body: formData
|
|
257
|
+
});
|
|
258
|
+
return await response.json();
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (err instanceof CLIError && err.code === "API_ERROR") {
|
|
261
|
+
if (err.statusCode === 413) {
|
|
262
|
+
throw new CLIError(
|
|
263
|
+
"BUNDLE_TOO_LARGE",
|
|
264
|
+
err.message,
|
|
265
|
+
"Reduce page bundle sizes. Check for large dependencies."
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (err.statusCode === 409) {
|
|
269
|
+
throw new CLIError(
|
|
270
|
+
"FUNNEL_NOT_HEADLESS",
|
|
271
|
+
err.message,
|
|
272
|
+
"Remove funnelId from config to create a new headless funnel."
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
throw new CLIError("PUBLISH_FAILED", err.message);
|
|
276
|
+
}
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
var DEFAULT_API_BASE2;
|
|
281
|
+
var init_api = __esm({
|
|
282
|
+
"src/lib/api.ts"() {
|
|
283
|
+
"use strict";
|
|
284
|
+
init_errors();
|
|
285
|
+
DEFAULT_API_BASE2 = "https://api.appfunnel.net";
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
178
289
|
// src/commands/init.ts
|
|
179
290
|
var init_exports = {};
|
|
180
291
|
__export(init_exports, {
|
|
181
292
|
initCommand: () => initCommand
|
|
182
293
|
});
|
|
183
|
-
import {
|
|
184
|
-
import { join as join2 } from "path";
|
|
294
|
+
import { cpSync, existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync } from "fs";
|
|
295
|
+
import { join as join2, dirname } from "path";
|
|
296
|
+
import { fileURLToPath } from "url";
|
|
185
297
|
import pc4 from "picocolors";
|
|
186
|
-
|
|
298
|
+
import select2 from "@inquirer/select";
|
|
299
|
+
import input from "@inquirer/input";
|
|
300
|
+
function getTemplatesDir() {
|
|
301
|
+
const dir = join2(__dirname, "..", "templates");
|
|
302
|
+
if (!existsSync(dir)) {
|
|
303
|
+
throw new Error(`Templates directory not found at ${dir}`);
|
|
304
|
+
}
|
|
305
|
+
return dir;
|
|
306
|
+
}
|
|
307
|
+
function listTemplates() {
|
|
308
|
+
const root = getTemplatesDir();
|
|
309
|
+
return readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
310
|
+
const configPath = join2(root, d.name, "template.json");
|
|
311
|
+
const config = existsSync(configPath) ? JSON.parse(readFileSync2(configPath, "utf-8")) : { name: d.name, description: "", products: [] };
|
|
312
|
+
return { dir: d.name, config };
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function formatInterval(interval, count) {
|
|
316
|
+
if (!interval) return " one-time";
|
|
317
|
+
const label = INTERVAL_LABELS[interval]?.[count];
|
|
318
|
+
if (label) return `/${label}`;
|
|
319
|
+
return `/${count} ${interval}${count > 1 ? "s" : ""}`;
|
|
320
|
+
}
|
|
321
|
+
function formatPrice(price) {
|
|
322
|
+
const amount = (price.amount / 100).toFixed(2);
|
|
323
|
+
const currency = price.currency.toUpperCase();
|
|
324
|
+
const interval = formatInterval(price.interval, price.intervalCount);
|
|
325
|
+
const productName = price.name || "Unnamed product";
|
|
326
|
+
const priceName = price.priceName ? ` \u2014 ${price.priceName}` : "";
|
|
327
|
+
return `${currency} ${amount}${interval} ${pc4.dim(`${productName}${priceName}`)} ${pc4.dim(`(${price.id})`)}`;
|
|
328
|
+
}
|
|
329
|
+
function formatStoreName(store) {
|
|
330
|
+
const test = store.isTestMode ? pc4.yellow(" (test)") : "";
|
|
331
|
+
return `${store.name || store.type}${test}`;
|
|
332
|
+
}
|
|
333
|
+
async function initCommand() {
|
|
187
334
|
const creds = requireAuth();
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
335
|
+
const apiOpts = { token: creds.token };
|
|
336
|
+
const name = await input({
|
|
337
|
+
message: "Funnel name",
|
|
338
|
+
validate: (value) => {
|
|
339
|
+
if (!value.trim()) return "Name is required";
|
|
340
|
+
if (!/^[a-z0-9-]+$/.test(value.trim()))
|
|
341
|
+
return "Use lowercase letters, numbers, and hyphens only";
|
|
342
|
+
if (existsSync(join2(process.cwd(), value.trim())))
|
|
343
|
+
return `Directory '${value.trim()}' already exists`;
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
const dir = join2(process.cwd(), name.trim());
|
|
193
348
|
const projectId = await promptForProject(creds.token);
|
|
194
349
|
const projects = await fetchProjects(creds.token);
|
|
195
350
|
const project = projects.find((p) => p.id === projectId);
|
|
351
|
+
const templates = listTemplates();
|
|
352
|
+
const selectedDir = await select2({
|
|
353
|
+
message: "Choose a template",
|
|
354
|
+
choices: templates.map((t) => ({
|
|
355
|
+
name: t.config.description ? `${t.config.name} \u2014 ${pc4.dim(t.config.description)}` : t.config.name,
|
|
356
|
+
value: t.dir
|
|
357
|
+
}))
|
|
358
|
+
});
|
|
359
|
+
const chosen = templates.find((t) => t.dir === selectedDir);
|
|
360
|
+
const templateConfig = chosen.config;
|
|
361
|
+
const templateDir = join2(getTemplatesDir(), chosen.dir);
|
|
362
|
+
const productBindings = [];
|
|
363
|
+
if (templateConfig.products.length > 0) {
|
|
364
|
+
const storesSpinner = spinner("Fetching stores...");
|
|
365
|
+
const stores = await fetchStores(projectId, apiOpts);
|
|
366
|
+
storesSpinner.stop();
|
|
367
|
+
if (stores.length === 0) {
|
|
368
|
+
warn("No stores found for this project.");
|
|
369
|
+
info("Products will use empty IDs \u2014 create a store in the dashboard and update appfunnel.config.ts.");
|
|
370
|
+
} else {
|
|
371
|
+
let store;
|
|
372
|
+
if (stores.length === 1) {
|
|
373
|
+
store = stores[0];
|
|
374
|
+
info(`Using store: ${formatStoreName(store)}`);
|
|
375
|
+
} else {
|
|
376
|
+
const storeId = await select2({
|
|
377
|
+
message: "Choose a store",
|
|
378
|
+
choices: stores.map((s2) => ({
|
|
379
|
+
name: formatStoreName(s2),
|
|
380
|
+
value: s2.id
|
|
381
|
+
}))
|
|
382
|
+
});
|
|
383
|
+
store = stores.find((s2) => s2.id === storeId);
|
|
384
|
+
}
|
|
385
|
+
const pricesSpinner = spinner("Fetching prices...");
|
|
386
|
+
const prices = await fetchStorePrices(projectId, store.id, apiOpts);
|
|
387
|
+
pricesSpinner.stop();
|
|
388
|
+
if (prices.length === 0) {
|
|
389
|
+
warn("No prices found in this store.");
|
|
390
|
+
info("Import prices in the dashboard, then update appfunnel.config.ts.");
|
|
391
|
+
} else {
|
|
392
|
+
console.log();
|
|
393
|
+
info(`Configuring ${templateConfig.products.length} product(s)...`);
|
|
394
|
+
console.log();
|
|
395
|
+
for (const product of templateConfig.products) {
|
|
396
|
+
if (product.description) {
|
|
397
|
+
console.log(` ${pc4.dim(product.description)}`);
|
|
398
|
+
}
|
|
399
|
+
const storePriceId = await select2({
|
|
400
|
+
message: `${product.label} \u2014 select a price`,
|
|
401
|
+
choices: prices.map((p) => ({
|
|
402
|
+
name: formatPrice(p),
|
|
403
|
+
value: p.id
|
|
404
|
+
}))
|
|
405
|
+
});
|
|
406
|
+
const binding = {
|
|
407
|
+
id: product.id,
|
|
408
|
+
name: product.id,
|
|
409
|
+
storePriceId
|
|
410
|
+
};
|
|
411
|
+
if (product.paidTrial) {
|
|
412
|
+
const trialStorePriceId = await select2({
|
|
413
|
+
message: `${product.label} \u2014 select a trial price`,
|
|
414
|
+
choices: [
|
|
415
|
+
...prices.map((p) => ({
|
|
416
|
+
name: formatPrice(p),
|
|
417
|
+
value: p.id
|
|
418
|
+
})),
|
|
419
|
+
{ name: pc4.dim("Skip (no trial)"), value: "" }
|
|
420
|
+
]
|
|
421
|
+
});
|
|
422
|
+
if (trialStorePriceId) {
|
|
423
|
+
binding.trialStorePriceId = trialStorePriceId;
|
|
424
|
+
const trialDaysStr = await select2({
|
|
425
|
+
message: `${product.label} \u2014 trial duration`,
|
|
426
|
+
choices: [
|
|
427
|
+
{ name: "3 days", value: "3" },
|
|
428
|
+
{ name: "7 days", value: "7" },
|
|
429
|
+
{ name: "14 days", value: "14" },
|
|
430
|
+
{ name: "30 days", value: "30" },
|
|
431
|
+
{ name: "90 days", value: "90" }
|
|
432
|
+
]
|
|
433
|
+
});
|
|
434
|
+
binding.trialDays = parseInt(trialDaysStr, 10);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
productBindings.push(binding);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
196
442
|
const s = spinner(`Creating ${name}...`);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
443
|
+
cpSync(templateDir, dir, { recursive: true });
|
|
444
|
+
const templateJsonPath = join2(dir, "template.json");
|
|
445
|
+
if (existsSync(templateJsonPath)) {
|
|
446
|
+
const { unlinkSync } = await import("fs");
|
|
447
|
+
unlinkSync(templateJsonPath);
|
|
448
|
+
}
|
|
449
|
+
const configPath = join2(dir, "appfunnel.config.ts");
|
|
450
|
+
if (existsSync(configPath)) {
|
|
451
|
+
let config = readFileSync2(configPath, "utf-8");
|
|
452
|
+
config = config.replace("__PROJECT_ID__", projectId);
|
|
453
|
+
config = config.replace("__NAME__", name);
|
|
454
|
+
if (productBindings.length > 0) {
|
|
455
|
+
const itemsStr = productBindings.map((b) => {
|
|
456
|
+
const fields = [
|
|
457
|
+
` { id: '${b.id}'`,
|
|
458
|
+
`name: '${b.name}'`,
|
|
459
|
+
`storePriceId: '${b.storePriceId}'`
|
|
460
|
+
];
|
|
461
|
+
if (b.trialDays) fields.push(`trialDays: ${b.trialDays}`);
|
|
462
|
+
if (b.trialStorePriceId)
|
|
463
|
+
fields.push(`trialStorePriceId: '${b.trialStorePriceId}'`);
|
|
464
|
+
return fields.join(", ") + " }";
|
|
465
|
+
}).join(",\n");
|
|
466
|
+
const defaultId = productBindings[0].id;
|
|
467
|
+
config = config.replace(
|
|
468
|
+
/products:\s*\{[^}]*items:\s*\[[^\]]*\][^}]*\}/s,
|
|
469
|
+
`products: {
|
|
470
|
+
items: [
|
|
471
|
+
${itemsStr},
|
|
472
|
+
],
|
|
473
|
+
defaultId: '${defaultId}',
|
|
474
|
+
}`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
writeFileSync2(configPath, config);
|
|
478
|
+
}
|
|
479
|
+
const sdkVersion = `^${"0.7.0"}`;
|
|
200
480
|
writeFileSync2(
|
|
201
481
|
join2(dir, "package.json"),
|
|
202
482
|
JSON.stringify(
|
|
@@ -211,12 +491,12 @@ async function initCommand(name) {
|
|
|
211
491
|
publish: "appfunnel publish"
|
|
212
492
|
},
|
|
213
493
|
dependencies: {
|
|
214
|
-
"@appfunnel-dev/sdk":
|
|
494
|
+
"@appfunnel-dev/sdk": sdkVersion,
|
|
215
495
|
react: "^18.3.0",
|
|
216
496
|
"react-dom": "^18.3.0"
|
|
217
497
|
},
|
|
218
498
|
devDependencies: {
|
|
219
|
-
appfunnel:
|
|
499
|
+
appfunnel: sdkVersion,
|
|
220
500
|
typescript: "^5.4.0",
|
|
221
501
|
"@types/react": "^18.2.0",
|
|
222
502
|
"@types/react-dom": "^18.2.0",
|
|
@@ -230,112 +510,6 @@ async function initCommand(name) {
|
|
|
230
510
|
2
|
|
231
511
|
) + "\n"
|
|
232
512
|
);
|
|
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
513
|
s.stop();
|
|
340
514
|
console.log();
|
|
341
515
|
success(`Created ${pc4.bold(name)} for project ${pc4.bold(project.name)}`);
|
|
@@ -345,12 +519,21 @@ dist
|
|
|
345
519
|
console.log(` ${pc4.dim("appfunnel dev")}`);
|
|
346
520
|
console.log();
|
|
347
521
|
}
|
|
522
|
+
var __dirname, INTERVAL_LABELS;
|
|
348
523
|
var init_init = __esm({
|
|
349
524
|
"src/commands/init.ts"() {
|
|
350
525
|
"use strict";
|
|
351
526
|
init_logger();
|
|
352
527
|
init_auth();
|
|
353
528
|
init_projects();
|
|
529
|
+
init_api();
|
|
530
|
+
__dirname = dirname(fileURLToPath(import.meta.url));
|
|
531
|
+
INTERVAL_LABELS = {
|
|
532
|
+
day: { 1: "day", 7: "week", 14: "2 weeks", 30: "month" },
|
|
533
|
+
week: { 1: "week", 2: "2 weeks", 4: "month", 12: "quarter", 52: "year" },
|
|
534
|
+
month: { 1: "month", 3: "quarter", 6: "6 months", 12: "year" },
|
|
535
|
+
year: { 1: "year" }
|
|
536
|
+
};
|
|
354
537
|
}
|
|
355
538
|
});
|
|
356
539
|
|
|
@@ -449,7 +632,7 @@ async function whoamiCommand() {
|
|
|
449
632
|
const creds = requireAuth();
|
|
450
633
|
const spin = spinner("Verifying credentials\u2026");
|
|
451
634
|
try {
|
|
452
|
-
const response = await fetch(`${
|
|
635
|
+
const response = await fetch(`${DEFAULT_API_BASE3}/user`, {
|
|
453
636
|
headers: {
|
|
454
637
|
Authorization: creds.token,
|
|
455
638
|
"Content-Type": "application/json"
|
|
@@ -480,19 +663,19 @@ async function whoamiCommand() {
|
|
|
480
663
|
);
|
|
481
664
|
}
|
|
482
665
|
}
|
|
483
|
-
var
|
|
666
|
+
var DEFAULT_API_BASE3;
|
|
484
667
|
var init_whoami = __esm({
|
|
485
668
|
"src/commands/whoami.ts"() {
|
|
486
669
|
"use strict";
|
|
487
670
|
init_auth();
|
|
488
671
|
init_errors();
|
|
489
672
|
init_logger();
|
|
490
|
-
|
|
673
|
+
DEFAULT_API_BASE3 = "https://api.appfunnel.net";
|
|
491
674
|
}
|
|
492
675
|
});
|
|
493
676
|
|
|
494
677
|
// src/lib/config.ts
|
|
495
|
-
import { existsSync as existsSync2, readFileSync as
|
|
678
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
496
679
|
import { join as join3, resolve } from "path";
|
|
497
680
|
async function loadConfig(cwd) {
|
|
498
681
|
const configPath = join3(cwd, CONFIG_FILE);
|
|
@@ -504,7 +687,7 @@ async function loadConfig(cwd) {
|
|
|
504
687
|
);
|
|
505
688
|
}
|
|
506
689
|
const { transform } = await import("esbuild");
|
|
507
|
-
const raw =
|
|
690
|
+
const raw = readFileSync3(configPath, "utf-8");
|
|
508
691
|
const result = await transform(raw, {
|
|
509
692
|
loader: "ts",
|
|
510
693
|
format: "esm",
|
|
@@ -536,10 +719,10 @@ var init_config = __esm({
|
|
|
536
719
|
});
|
|
537
720
|
|
|
538
721
|
// src/lib/version.ts
|
|
539
|
-
import { readFileSync as
|
|
722
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
540
723
|
import { join as join4 } from "path";
|
|
541
724
|
function checkVersionCompatibility(cwd) {
|
|
542
|
-
const cliVersion = "0.
|
|
725
|
+
const cliVersion = "0.7.0";
|
|
543
726
|
const sdkVersion = getSdkVersion(cwd);
|
|
544
727
|
const [cliMajor, cliMinor] = cliVersion.split(".").map(Number);
|
|
545
728
|
const [sdkMajor, sdkMinor] = sdkVersion.split(".").map(Number);
|
|
@@ -560,7 +743,7 @@ function getSdkVersion(cwd) {
|
|
|
560
743
|
"sdk",
|
|
561
744
|
"package.json"
|
|
562
745
|
);
|
|
563
|
-
const pkg = JSON.parse(
|
|
746
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
564
747
|
return pkg.version;
|
|
565
748
|
} catch {
|
|
566
749
|
throw new CLIError(
|
|
@@ -578,7 +761,7 @@ var init_version = __esm({
|
|
|
578
761
|
});
|
|
579
762
|
|
|
580
763
|
// src/extract/pages.ts
|
|
581
|
-
import { readdirSync, readFileSync as
|
|
764
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync5, existsSync as existsSync3 } from "fs";
|
|
582
765
|
import { join as join5, basename } from "path";
|
|
583
766
|
function scanPages(cwd) {
|
|
584
767
|
const pagesDir = resolvePagesDir(cwd);
|
|
@@ -589,7 +772,7 @@ function scanPages(cwd) {
|
|
|
589
772
|
"Create src/pages/ and add at least one .tsx page file."
|
|
590
773
|
);
|
|
591
774
|
}
|
|
592
|
-
const files =
|
|
775
|
+
const files = readdirSync2(pagesDir).filter((f) => f.endsWith(".tsx") && !f.startsWith("_")).map((f) => basename(f, ".tsx")).sort();
|
|
593
776
|
if (files.length === 0) {
|
|
594
777
|
throw new CLIError(
|
|
595
778
|
"NO_PAGES",
|
|
@@ -605,7 +788,7 @@ async function extractPageDefinitions(cwd, pageKeys) {
|
|
|
605
788
|
const result = {};
|
|
606
789
|
for (const key of pageKeys) {
|
|
607
790
|
const filePath = join5(pagesDir, `${key}.tsx`);
|
|
608
|
-
const source =
|
|
791
|
+
const source = readFileSync5(filePath, "utf-8");
|
|
609
792
|
const definition = extractDefinePage(ts, source, filePath);
|
|
610
793
|
if (definition) {
|
|
611
794
|
result[key] = definition;
|
|
@@ -959,7 +1142,7 @@ var init_html = __esm({
|
|
|
959
1142
|
|
|
960
1143
|
// src/vite/plugin.ts
|
|
961
1144
|
import { resolve as resolve2, join as join7 } from "path";
|
|
962
|
-
import { existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as
|
|
1145
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
963
1146
|
function appfunnelPlugin(options) {
|
|
964
1147
|
const { cwd, config, isDev } = options;
|
|
965
1148
|
let pages = options.pages;
|
|
@@ -980,7 +1163,7 @@ function appfunnelPlugin(options) {
|
|
|
980
1163
|
return {
|
|
981
1164
|
name: "appfunnel",
|
|
982
1165
|
config() {
|
|
983
|
-
|
|
1166
|
+
mkdirSync2(appfunnelDir, { recursive: true });
|
|
984
1167
|
writeFileSync3(htmlPath, generateHtml(config.name || "AppFunnel"));
|
|
985
1168
|
return {
|
|
986
1169
|
// Don't let Vite auto-serve index.html — we handle it ourselves
|
|
@@ -1057,7 +1240,7 @@ function appfunnelPlugin(options) {
|
|
|
1057
1240
|
return next();
|
|
1058
1241
|
}
|
|
1059
1242
|
try {
|
|
1060
|
-
const rawHtml =
|
|
1243
|
+
const rawHtml = readFileSync6(htmlPath, "utf-8");
|
|
1061
1244
|
const html = await devServer.transformIndexHtml(req.url || "/", rawHtml);
|
|
1062
1245
|
res.statusCode = 200;
|
|
1063
1246
|
res.setHeader("Content-Type", "text/html");
|
|
@@ -1085,104 +1268,12 @@ var init_plugin = __esm({
|
|
|
1085
1268
|
}
|
|
1086
1269
|
});
|
|
1087
1270
|
|
|
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
1271
|
// src/commands/dev.ts
|
|
1181
1272
|
var dev_exports = {};
|
|
1182
1273
|
__export(dev_exports, {
|
|
1183
1274
|
devCommand: () => devCommand
|
|
1184
1275
|
});
|
|
1185
|
-
import { readFileSync as
|
|
1276
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
1186
1277
|
import { join as join8 } from "path";
|
|
1187
1278
|
import pc6 from "picocolors";
|
|
1188
1279
|
async function devCommand(options) {
|
|
@@ -1198,7 +1289,7 @@ async function devCommand(options) {
|
|
|
1198
1289
|
const projectId = await promptForProject(creds.token);
|
|
1199
1290
|
config.projectId = projectId;
|
|
1200
1291
|
const configPath = join8(cwd, "appfunnel.config.ts");
|
|
1201
|
-
const configSource =
|
|
1292
|
+
const configSource = readFileSync7(configPath, "utf-8");
|
|
1202
1293
|
let updated;
|
|
1203
1294
|
if (/projectId:\s*['"]/.test(configSource)) {
|
|
1204
1295
|
updated = configSource.replace(
|
|
@@ -1320,7 +1411,7 @@ __export(build_exports, {
|
|
|
1320
1411
|
});
|
|
1321
1412
|
import { resolve as resolve3, join as join9 } from "path";
|
|
1322
1413
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1323
|
-
import { readFileSync as
|
|
1414
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, statSync, readdirSync as readdirSync3 } from "fs";
|
|
1324
1415
|
import pc7 from "picocolors";
|
|
1325
1416
|
async function buildCommand() {
|
|
1326
1417
|
const cwd = process.cwd();
|
|
@@ -1439,7 +1530,7 @@ async function buildCommand() {
|
|
|
1439
1530
|
console.log(` ${pc7.dim("Pages:")} ${pageKeys.length}`);
|
|
1440
1531
|
console.log(` ${pc7.dim("Size:")} ${formatSize(totalSize)}`);
|
|
1441
1532
|
console.log();
|
|
1442
|
-
for (const asset of assets
|
|
1533
|
+
for (const asset of assets) {
|
|
1443
1534
|
const sizeStr = formatSize(asset.size);
|
|
1444
1535
|
const isOver = asset.size > MAX_PAGE_SIZE;
|
|
1445
1536
|
console.log(` ${isOver ? pc7.yellow("!") : pc7.dim("\xB7")} ${pc7.dim(asset.path)} ${isOver ? pc7.yellow(sizeStr) : pc7.dim(sizeStr)}`);
|
|
@@ -1531,7 +1622,7 @@ function validateConditionVariables(condition, pageKey, allVariables) {
|
|
|
1531
1622
|
function collectAssets(outDir) {
|
|
1532
1623
|
const assets = [];
|
|
1533
1624
|
function walk(dir, prefix = "") {
|
|
1534
|
-
for (const entry of
|
|
1625
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
1535
1626
|
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1536
1627
|
const fullPath = join9(dir, entry.name);
|
|
1537
1628
|
if (entry.isDirectory()) {
|
|
@@ -1551,7 +1642,7 @@ function formatSize(bytes) {
|
|
|
1551
1642
|
}
|
|
1552
1643
|
function getSdkVersion2(cwd) {
|
|
1553
1644
|
try {
|
|
1554
|
-
const pkg = JSON.parse(
|
|
1645
|
+
const pkg = JSON.parse(readFileSync8(join9(cwd, "node_modules", "@appfunnel", "sdk", "package.json"), "utf-8"));
|
|
1555
1646
|
return pkg.version;
|
|
1556
1647
|
} catch {
|
|
1557
1648
|
return "0.0.0";
|
|
@@ -1584,10 +1675,10 @@ var init_build = __esm({
|
|
|
1584
1675
|
|
|
1585
1676
|
// src/lib/config-patch.ts
|
|
1586
1677
|
import { join as join10 } from "path";
|
|
1587
|
-
import { readFileSync as
|
|
1678
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
1588
1679
|
function patchConfigFunnelId(cwd, funnelId) {
|
|
1589
1680
|
const configPath = join10(cwd, "appfunnel.config.ts");
|
|
1590
|
-
let content =
|
|
1681
|
+
let content = readFileSync9(configPath, "utf-8");
|
|
1591
1682
|
if (content.includes("funnelId")) return;
|
|
1592
1683
|
const patched = content.replace(
|
|
1593
1684
|
/(projectId:\s*['"][^'"]+['"],?\s*\n)/,
|
|
@@ -1610,7 +1701,7 @@ __export(publish_exports, {
|
|
|
1610
1701
|
publishCommand: () => publishCommand
|
|
1611
1702
|
});
|
|
1612
1703
|
import { resolve as resolve4, join as join11 } from "path";
|
|
1613
|
-
import { readFileSync as
|
|
1704
|
+
import { readFileSync as readFileSync10, existsSync as existsSync6 } from "fs";
|
|
1614
1705
|
import pc8 from "picocolors";
|
|
1615
1706
|
function getMimeType(path) {
|
|
1616
1707
|
const ext = path.substring(path.lastIndexOf("."));
|
|
@@ -1621,7 +1712,7 @@ function formatSize2(bytes) {
|
|
|
1621
1712
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1622
1713
|
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
1623
1714
|
}
|
|
1624
|
-
async function publishCommand() {
|
|
1715
|
+
async function publishCommand(options) {
|
|
1625
1716
|
const cwd = process.cwd();
|
|
1626
1717
|
const creds = requireAuth();
|
|
1627
1718
|
checkVersionCompatibility(cwd);
|
|
@@ -1643,7 +1734,7 @@ async function publishCommand() {
|
|
|
1643
1734
|
"Run 'appfunnel build' first."
|
|
1644
1735
|
);
|
|
1645
1736
|
}
|
|
1646
|
-
const manifest = JSON.parse(
|
|
1737
|
+
const manifest = JSON.parse(readFileSync10(manifestPath, "utf-8"));
|
|
1647
1738
|
const assets = manifest.assets || [];
|
|
1648
1739
|
const s = spinner("Preparing assets...");
|
|
1649
1740
|
const assetPayloads = [];
|
|
@@ -1659,7 +1750,7 @@ async function publishCommand() {
|
|
|
1659
1750
|
"Run 'appfunnel build' to regenerate."
|
|
1660
1751
|
);
|
|
1661
1752
|
}
|
|
1662
|
-
const content =
|
|
1753
|
+
const content = readFileSync10(fullPath);
|
|
1663
1754
|
totalBytes += content.length;
|
|
1664
1755
|
assetPayloads.push({
|
|
1665
1756
|
path: asset.path,
|
|
@@ -1674,7 +1765,8 @@ async function publishCommand() {
|
|
|
1674
1765
|
config.funnelId || "",
|
|
1675
1766
|
manifest,
|
|
1676
1767
|
assetPayloads,
|
|
1677
|
-
{ token: creds.token }
|
|
1768
|
+
{ token: creds.token },
|
|
1769
|
+
options?.promote
|
|
1678
1770
|
);
|
|
1679
1771
|
s.stop();
|
|
1680
1772
|
if (result.created && result.funnelId) {
|
|
@@ -1682,14 +1774,18 @@ async function publishCommand() {
|
|
|
1682
1774
|
info(`Funnel created \u2014 funnelId added to appfunnel.config.ts`);
|
|
1683
1775
|
}
|
|
1684
1776
|
console.log();
|
|
1685
|
-
success("Published successfully");
|
|
1777
|
+
success(result.activated ? "Published and activated" : "Published successfully");
|
|
1686
1778
|
console.log();
|
|
1687
|
-
console.log(` ${pc8.dim("Build ID:")}
|
|
1779
|
+
console.log(` ${pc8.dim("Build ID:")} ${result.buildId}`);
|
|
1688
1780
|
if (result.funnelId && !config.funnelId) {
|
|
1689
|
-
console.log(` ${pc8.dim("Funnel:")}
|
|
1781
|
+
console.log(` ${pc8.dim("Funnel:")} ${result.funnelId}`);
|
|
1782
|
+
}
|
|
1783
|
+
console.log(` ${pc8.dim("Dashboard:")} ${pc8.cyan(result.dashboardUrl)}`);
|
|
1784
|
+
console.log(` ${pc8.dim("Assets:")} ${assets.length} files ${pc8.dim(`(${formatSize2(totalBytes)})`)}`);
|
|
1785
|
+
if (!result.activated) {
|
|
1786
|
+
console.log();
|
|
1787
|
+
console.log(` ${pc8.dim("Tip:")} Use ${pc8.cyan("--promote")} to activate immediately, or promote from the dashboard.`);
|
|
1690
1788
|
}
|
|
1691
|
-
console.log(` ${pc8.dim("URL:")} ${pc8.cyan(result.url)}`);
|
|
1692
|
-
console.log(` ${pc8.dim("Assets:")} ${assets.length} files ${pc8.dim(`(${formatSize2(totalBytes)})`)}`);
|
|
1693
1789
|
console.log();
|
|
1694
1790
|
}
|
|
1695
1791
|
var MIME_TYPES;
|
|
@@ -1722,10 +1818,10 @@ init_errors();
|
|
|
1722
1818
|
import { Command } from "commander";
|
|
1723
1819
|
import pc9 from "picocolors";
|
|
1724
1820
|
var program = new Command();
|
|
1725
|
-
program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.
|
|
1726
|
-
program.command("init
|
|
1821
|
+
program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.7.0");
|
|
1822
|
+
program.command("init").description("Create a new AppFunnel project").action(async () => {
|
|
1727
1823
|
const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1728
|
-
await initCommand2(
|
|
1824
|
+
await initCommand2();
|
|
1729
1825
|
});
|
|
1730
1826
|
program.command("login").description("Authenticate with AppFunnel").action(async () => {
|
|
1731
1827
|
const { loginCommand: loginCommand2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
@@ -1743,9 +1839,9 @@ program.command("build").description("Build the funnel for production").action(a
|
|
|
1743
1839
|
const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_build(), build_exports));
|
|
1744
1840
|
await buildCommand2();
|
|
1745
1841
|
});
|
|
1746
|
-
program.command("publish").description("Publish the build to AppFunnel").action(async () => {
|
|
1842
|
+
program.command("publish").description("Publish the build to AppFunnel").option("--promote", "Activate the build immediately after publishing").action(async (options) => {
|
|
1747
1843
|
const { publishCommand: publishCommand2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
|
|
1748
|
-
await publishCommand2();
|
|
1844
|
+
await publishCommand2({ promote: options.promote });
|
|
1749
1845
|
});
|
|
1750
1846
|
program.hook("postAction", () => {
|
|
1751
1847
|
});
|