appfunnel 0.5.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 +660 -316
- package/dist/index.js.map +1 -1
- package/package.json +6 -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
|
|
|
@@ -364,7 +547,7 @@ import { randomUUID } from "crypto";
|
|
|
364
547
|
import open from "open";
|
|
365
548
|
async function loginCommand() {
|
|
366
549
|
const state = randomUUID();
|
|
367
|
-
return new Promise((
|
|
550
|
+
return new Promise((resolve5, reject) => {
|
|
368
551
|
const server = createServer((req, res) => {
|
|
369
552
|
const url = new URL(req.url || "/", `http://localhost`);
|
|
370
553
|
if (url.pathname !== "/callback") {
|
|
@@ -403,7 +586,7 @@ async function loginCommand() {
|
|
|
403
586
|
spinner2.stop();
|
|
404
587
|
success(`Logged in as ${email || userId}`);
|
|
405
588
|
server.close();
|
|
406
|
-
|
|
589
|
+
resolve5();
|
|
407
590
|
});
|
|
408
591
|
server.listen(0, "127.0.0.1", () => {
|
|
409
592
|
const addr = server.address();
|
|
@@ -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;
|
|
@@ -694,6 +877,7 @@ var init_pages = __esm({
|
|
|
694
877
|
|
|
695
878
|
// src/vite/entry.ts
|
|
696
879
|
import { join as join6 } from "path";
|
|
880
|
+
import { existsSync as existsSync4 } from "fs";
|
|
697
881
|
function generateEntrySource(options) {
|
|
698
882
|
const { config, pages, pagesDir, funnelTsxPath, isDev } = options;
|
|
699
883
|
const pageKeys = Object.keys(pages);
|
|
@@ -721,76 +905,197 @@ function generateEntrySource(options) {
|
|
|
721
905
|
for (const [key, def] of Object.entries(pages)) {
|
|
722
906
|
slugMap[def.slug || key] = key;
|
|
723
907
|
}
|
|
908
|
+
const priceDataCode = isDev ? options.priceData && options.priceData.size > 0 ? `const priceData = new Map(${JSON.stringify([...options.priceData.entries()])});` : `const priceData = undefined;` : `const priceData = (() => {
|
|
909
|
+
const rd = typeof window !== 'undefined' && window.__APPFUNNEL_DATA__;
|
|
910
|
+
if (!rd || !rd.products?.items) return undefined;
|
|
911
|
+
// Build price map from server-injected product data
|
|
912
|
+
const map = new Map();
|
|
913
|
+
for (const item of rd.products.items) {
|
|
914
|
+
if (item.priceData && item.storePriceId) map.set(item.storePriceId, item.priceData);
|
|
915
|
+
if (item.trialPriceData && item.trialStorePriceId) map.set(item.trialStorePriceId, item.trialPriceData);
|
|
916
|
+
}
|
|
917
|
+
return map.size > 0 ? map : undefined;
|
|
918
|
+
})();`;
|
|
724
919
|
const trackingCode = isDev ? `
|
|
725
920
|
// Dev mode: mock tracking \u2014 log events to console
|
|
726
|
-
const originalFetch = globalThis.fetch;
|
|
727
921
|
globalThis.__APPFUNNEL_DEV__ = true;
|
|
728
922
|
` : "";
|
|
923
|
+
const appCssPath = join6(pagesDir, "..", "app.css").replace(/\\/g, "/");
|
|
924
|
+
const hasAppCss = existsSync4(join6(pagesDir, "..", "app.css"));
|
|
729
925
|
return `
|
|
730
|
-
import { StrictMode, lazy, Suspense, useState, useEffect,
|
|
926
|
+
import { StrictMode, Component, lazy, Suspense, useState, useCallback, useEffect, useTransition, useDeferredValue, useSyncExternalStore } from 'react'
|
|
731
927
|
import { createRoot } from 'react-dom/client'
|
|
732
|
-
import { FunnelProvider } from '@appfunnel-dev/sdk
|
|
928
|
+
import { FunnelProvider, useNavigation } from '@appfunnel-dev/sdk'
|
|
929
|
+
${hasAppCss ? `import '${appCssPath}'` : ""}
|
|
733
930
|
import FunnelWrapper from '${funnelTsxPath.replace(/\\/g, "/")}'
|
|
734
931
|
|
|
735
932
|
${trackingCode}
|
|
736
933
|
|
|
737
|
-
const
|
|
934
|
+
const pageComponents = {
|
|
738
935
|
${pageImports}
|
|
739
936
|
}
|
|
740
937
|
|
|
938
|
+
${priceDataCode}
|
|
939
|
+
|
|
741
940
|
const config = ${JSON.stringify(fullConfig, null, 2)}
|
|
742
941
|
|
|
743
|
-
const slugToKey = ${JSON.stringify(slugMap)}
|
|
744
942
|
const keyToSlug = ${JSON.stringify(
|
|
745
943
|
Object.fromEntries(Object.entries(slugMap).map(([s, k]) => [k, s]))
|
|
746
944
|
)}
|
|
945
|
+
const slugToKey = ${JSON.stringify(slugMap)}
|
|
946
|
+
|
|
947
|
+
const DEV_CAMPAIGN_SLUG = 'campaign'
|
|
948
|
+
const DEFAULT_INITIAL = '${config.initialPageKey || pageKeys[0] || "index"}'
|
|
747
949
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
950
|
+
/**
|
|
951
|
+
* Parse the URL to extract basePath, campaignSlug, and initial page.
|
|
952
|
+
*
|
|
953
|
+
* URL pattern: /f/<campaignSlug>[/<pageSlug>]
|
|
954
|
+
*
|
|
955
|
+
* In dev, redirects bare / to /f/<projectId> so the URL matches production.
|
|
956
|
+
*/
|
|
957
|
+
function parseUrl() {
|
|
958
|
+
const parts = window.location.pathname.split('/').filter(Boolean)
|
|
959
|
+
|
|
960
|
+
// /f/<campaignSlug>[/<pageSlug>]
|
|
961
|
+
if (parts[0] === 'f' && parts.length >= 2) {
|
|
962
|
+
const campaignSlug = parts[1]
|
|
963
|
+
const pageSlug = parts[2] || ''
|
|
964
|
+
const pageKey = pageSlug ? (slugToKey[pageSlug] || '') : ''
|
|
965
|
+
return {
|
|
966
|
+
basePath: '/f/' + campaignSlug,
|
|
967
|
+
campaignSlug,
|
|
968
|
+
initialPage: pageKey || DEFAULT_INITIAL,
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Bare URL \u2192 redirect to /f/<slug> in dev
|
|
973
|
+
${isDev ? `
|
|
974
|
+
window.history.replaceState(null, '', '/f/' + DEV_CAMPAIGN_SLUG)
|
|
975
|
+
return {
|
|
976
|
+
basePath: '/f/' + DEV_CAMPAIGN_SLUG,
|
|
977
|
+
campaignSlug: DEV_CAMPAIGN_SLUG,
|
|
978
|
+
initialPage: DEFAULT_INITIAL,
|
|
979
|
+
}` : `
|
|
980
|
+
return {
|
|
981
|
+
basePath: '',
|
|
982
|
+
campaignSlug: '',
|
|
983
|
+
initialPage: DEFAULT_INITIAL,
|
|
984
|
+
}`}
|
|
751
985
|
}
|
|
752
986
|
|
|
753
|
-
|
|
754
|
-
|
|
987
|
+
const { basePath, campaignSlug, initialPage } = parseUrl()
|
|
988
|
+
|
|
989
|
+
${isDev ? `
|
|
990
|
+
class ErrorBoundary extends Component {
|
|
991
|
+
constructor(props) {
|
|
992
|
+
super(props)
|
|
993
|
+
this.state = { error: null }
|
|
994
|
+
}
|
|
995
|
+
static getDerivedStateFromError(error) {
|
|
996
|
+
return { error }
|
|
997
|
+
}
|
|
998
|
+
componentDidCatch(error, info) {
|
|
999
|
+
console.error('[AppFunnel] Render error:', error, info)
|
|
1000
|
+
}
|
|
1001
|
+
render() {
|
|
1002
|
+
if (this.state.error) {
|
|
1003
|
+
return (
|
|
1004
|
+
<div style={{ padding: '2rem', fontFamily: 'monospace' }}>
|
|
1005
|
+
<h2 style={{ color: 'red' }}>AppFunnel Error</h2>
|
|
1006
|
+
<pre style={{ whiteSpace: 'pre-wrap', color: '#333' }}>{this.state.error.message}</pre>
|
|
1007
|
+
<pre style={{ whiteSpace: 'pre-wrap', color: '#666', fontSize: '12px' }}>{this.state.error.stack}</pre>
|
|
1008
|
+
</div>
|
|
1009
|
+
)
|
|
1010
|
+
}
|
|
1011
|
+
return this.props.children
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
` : ""}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* PageRenderer lives inside FunnelProvider so it can use SDK hooks.
|
|
1018
|
+
* Subscribes to the router \u2014 re-renders when the page changes.
|
|
1019
|
+
* Uses useTransition to keep showing the current page while the next one loads.
|
|
1020
|
+
*/
|
|
1021
|
+
function PageRenderer() {
|
|
1022
|
+
const { currentPage, goToPage } = useNavigation()
|
|
1023
|
+
const routerPageKey = currentPage?.key || ''
|
|
1024
|
+
|
|
1025
|
+
// Track the displayed page separately so we can transition smoothly
|
|
1026
|
+
const [displayedKey, setDisplayedKey] = useState(routerPageKey)
|
|
1027
|
+
const [isPending, startTransition] = useTransition()
|
|
1028
|
+
|
|
1029
|
+
// When the router's page changes, transition to the new page
|
|
1030
|
+
useEffect(() => {
|
|
1031
|
+
if (routerPageKey && routerPageKey !== displayedKey) {
|
|
1032
|
+
startTransition(() => {
|
|
1033
|
+
setDisplayedKey(routerPageKey)
|
|
1034
|
+
})
|
|
1035
|
+
}
|
|
1036
|
+
}, [routerPageKey, displayedKey])
|
|
1037
|
+
|
|
1038
|
+
// Sync URL with current page
|
|
1039
|
+
const slug = currentPage?.slug || routerPageKey
|
|
1040
|
+
useEffect(() => {
|
|
1041
|
+
const expectedPath = basePath ? basePath + '/' + slug : '/' + slug
|
|
1042
|
+
if (slug && window.location.pathname !== expectedPath) {
|
|
1043
|
+
window.history.pushState(null, '', expectedPath)
|
|
1044
|
+
}
|
|
1045
|
+
}, [slug])
|
|
755
1046
|
|
|
1047
|
+
// Handle browser back/forward
|
|
756
1048
|
useEffect(() => {
|
|
757
1049
|
const handlePopState = () => {
|
|
758
1050
|
const path = window.location.pathname.split('/').filter(Boolean).pop() || ''
|
|
759
|
-
const
|
|
760
|
-
if (
|
|
761
|
-
|
|
1051
|
+
const key = slugToKey[path]
|
|
1052
|
+
if (key && key !== routerPageKey) {
|
|
1053
|
+
goToPage(key)
|
|
762
1054
|
}
|
|
763
1055
|
}
|
|
764
1056
|
window.addEventListener('popstate', handlePopState)
|
|
765
1057
|
return () => window.removeEventListener('popstate', handlePopState)
|
|
766
|
-
}, [
|
|
1058
|
+
}, [routerPageKey, goToPage])
|
|
767
1059
|
|
|
768
|
-
|
|
769
|
-
useEffect(() => {
|
|
770
|
-
window.__APPFUNNEL_NAVIGATE__ = (pageKey) => {
|
|
771
|
-
setCurrentPage(pageKey)
|
|
772
|
-
const slug = keyToSlug[pageKey] || pageKey
|
|
773
|
-
window.history.pushState(null, '', '/' + slug)
|
|
774
|
-
}
|
|
775
|
-
return () => { delete window.__APPFUNNEL_NAVIGATE__ }
|
|
776
|
-
}, [])
|
|
777
|
-
|
|
778
|
-
const PageComponent = pages[currentPage]
|
|
1060
|
+
const PageComponent = pageComponents[displayedKey]
|
|
779
1061
|
|
|
780
1062
|
if (!PageComponent) {
|
|
781
|
-
return <div style={{ padding: '2rem', color: 'red' }}>Page not found: {
|
|
1063
|
+
return <div style={{ padding: '2rem', color: 'red' }}>Page not found: {displayedKey}</div>
|
|
782
1064
|
}
|
|
783
1065
|
|
|
1066
|
+
return (
|
|
1067
|
+
<Suspense fallback={null}>
|
|
1068
|
+
<PageComponent />
|
|
1069
|
+
</Suspense>
|
|
1070
|
+
)
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Runtime data injected by the server (production only)
|
|
1074
|
+
const __rd = typeof window !== 'undefined' && window.__APPFUNNEL_DATA__
|
|
1075
|
+
|
|
1076
|
+
function App() {
|
|
1077
|
+
// In production, merge server-injected integrations into config
|
|
1078
|
+
const runtimeConfig = __rd && __rd.integrations
|
|
1079
|
+
? { ...config, integrations: { ...config.integrations, ...__rd.integrations } }
|
|
1080
|
+
: config
|
|
1081
|
+
|
|
1082
|
+
const sessionData = __rd ? {
|
|
1083
|
+
campaignId: __rd.campaignId || '',
|
|
1084
|
+
funnelId: __rd.funnelId || config.funnelId || '',
|
|
1085
|
+
experimentId: __rd.experimentId || null,
|
|
1086
|
+
} : undefined
|
|
1087
|
+
|
|
784
1088
|
return (
|
|
785
1089
|
<FunnelProvider
|
|
786
|
-
config={
|
|
787
|
-
initialPage={
|
|
788
|
-
|
|
1090
|
+
config={runtimeConfig}
|
|
1091
|
+
initialPage={initialPage}
|
|
1092
|
+
basePath={basePath}
|
|
1093
|
+
campaignSlug={campaignSlug}
|
|
1094
|
+
priceData={priceData}
|
|
1095
|
+
sessionData={sessionData}
|
|
789
1096
|
>
|
|
790
1097
|
<FunnelWrapper>
|
|
791
|
-
<
|
|
792
|
-
<PageComponent />
|
|
793
|
-
</Suspense>
|
|
1098
|
+
<PageRenderer />
|
|
794
1099
|
</FunnelWrapper>
|
|
795
1100
|
</FunnelProvider>
|
|
796
1101
|
)
|
|
@@ -798,9 +1103,14 @@ function App() {
|
|
|
798
1103
|
|
|
799
1104
|
createRoot(document.getElementById('root')).render(
|
|
800
1105
|
<StrictMode>
|
|
801
|
-
<
|
|
1106
|
+
${isDev ? "<ErrorBoundary>" : ""}
|
|
1107
|
+
<App />
|
|
1108
|
+
${isDev ? "</ErrorBoundary>" : ""}
|
|
802
1109
|
</StrictMode>
|
|
803
1110
|
)
|
|
1111
|
+
|
|
1112
|
+
// Reveal body (the host page may set opacity:0 for a loading transition)
|
|
1113
|
+
document.body.style.opacity = '1'
|
|
804
1114
|
`;
|
|
805
1115
|
}
|
|
806
1116
|
var init_entry = __esm({
|
|
@@ -832,54 +1142,66 @@ var init_html = __esm({
|
|
|
832
1142
|
|
|
833
1143
|
// src/vite/plugin.ts
|
|
834
1144
|
import { resolve as resolve2, join as join7 } from "path";
|
|
835
|
-
import { existsSync as
|
|
1145
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
836
1146
|
function appfunnelPlugin(options) {
|
|
837
1147
|
const { cwd, config, isDev } = options;
|
|
838
1148
|
let pages = options.pages;
|
|
839
1149
|
const pagesDir = resolve2(cwd, "src", "pages");
|
|
840
1150
|
const funnelTsxPath = resolve2(cwd, "src", "funnel.tsx");
|
|
841
|
-
|
|
1151
|
+
const appfunnelDir = join7(cwd, APPFUNNEL_DIR);
|
|
1152
|
+
const htmlPath = join7(appfunnelDir, "index.html");
|
|
842
1153
|
function getEntrySource() {
|
|
843
1154
|
return generateEntrySource({
|
|
844
1155
|
config,
|
|
845
1156
|
pages,
|
|
846
1157
|
pagesDir,
|
|
847
1158
|
funnelTsxPath,
|
|
848
|
-
isDev
|
|
1159
|
+
isDev,
|
|
1160
|
+
priceData: options.priceData
|
|
849
1161
|
});
|
|
850
1162
|
}
|
|
851
1163
|
return {
|
|
852
1164
|
name: "appfunnel",
|
|
853
1165
|
config() {
|
|
1166
|
+
mkdirSync2(appfunnelDir, { recursive: true });
|
|
1167
|
+
writeFileSync3(htmlPath, generateHtml(config.name || "AppFunnel"));
|
|
854
1168
|
return {
|
|
1169
|
+
// Don't let Vite auto-serve index.html — we handle it ourselves
|
|
1170
|
+
appType: "custom",
|
|
855
1171
|
resolve: {
|
|
856
1172
|
alias: {
|
|
857
1173
|
"@": resolve2(cwd, "src")
|
|
858
1174
|
}
|
|
859
1175
|
},
|
|
860
|
-
// Ensure we can import .tsx files
|
|
861
1176
|
esbuild: {
|
|
862
1177
|
jsx: "automatic"
|
|
863
1178
|
},
|
|
864
1179
|
optimizeDeps: {
|
|
865
|
-
include: ["react", "react-dom", "react/jsx-runtime"]
|
|
1180
|
+
include: ["react", "react-dom", "react/jsx-runtime"],
|
|
1181
|
+
force: true
|
|
866
1182
|
}
|
|
867
1183
|
};
|
|
868
1184
|
},
|
|
869
1185
|
resolveId(id) {
|
|
870
|
-
if (id === VIRTUAL_ENTRY_ID) {
|
|
1186
|
+
if (id === VIRTUAL_ENTRY_ID || id === "/" + VIRTUAL_ENTRY_ID) {
|
|
871
1187
|
return RESOLVED_VIRTUAL_ENTRY_ID;
|
|
872
1188
|
}
|
|
873
1189
|
return null;
|
|
874
1190
|
},
|
|
875
|
-
load(id) {
|
|
1191
|
+
async load(id) {
|
|
876
1192
|
if (id === RESOLVED_VIRTUAL_ENTRY_ID) {
|
|
877
|
-
|
|
1193
|
+
const { transform } = await import("esbuild");
|
|
1194
|
+
const source = getEntrySource();
|
|
1195
|
+
const result = await transform(source, {
|
|
1196
|
+
loader: "tsx",
|
|
1197
|
+
jsx: "automatic",
|
|
1198
|
+
sourcefile: "appfunnel-entry.tsx"
|
|
1199
|
+
});
|
|
1200
|
+
return result.code;
|
|
878
1201
|
}
|
|
879
1202
|
return null;
|
|
880
1203
|
},
|
|
881
1204
|
configureServer(devServer) {
|
|
882
|
-
server = devServer;
|
|
883
1205
|
const watcher = devServer.watcher;
|
|
884
1206
|
watcher.add(pagesDir);
|
|
885
1207
|
const handlePagesChange = async () => {
|
|
@@ -903,7 +1225,7 @@ function appfunnelPlugin(options) {
|
|
|
903
1225
|
}
|
|
904
1226
|
});
|
|
905
1227
|
const configPath = join7(cwd, "appfunnel.config.ts");
|
|
906
|
-
if (
|
|
1228
|
+
if (existsSync5(configPath)) {
|
|
907
1229
|
watcher.add(configPath);
|
|
908
1230
|
watcher.on("change", (file) => {
|
|
909
1231
|
if (file === configPath) {
|
|
@@ -912,33 +1234,37 @@ function appfunnelPlugin(options) {
|
|
|
912
1234
|
});
|
|
913
1235
|
}
|
|
914
1236
|
return () => {
|
|
915
|
-
devServer.middlewares.use((req, res, next) => {
|
|
916
|
-
|
|
1237
|
+
devServer.middlewares.use(async (req, res, next) => {
|
|
1238
|
+
const url = req.url?.split("?")[0] || "";
|
|
1239
|
+
if (url.includes(".") || url.startsWith("/@") || url.startsWith("/node_modules")) {
|
|
917
1240
|
return next();
|
|
918
1241
|
}
|
|
919
|
-
|
|
920
|
-
|
|
1242
|
+
try {
|
|
1243
|
+
const rawHtml = readFileSync6(htmlPath, "utf-8");
|
|
1244
|
+
const html = await devServer.transformIndexHtml(req.url || "/", rawHtml);
|
|
921
1245
|
res.statusCode = 200;
|
|
922
1246
|
res.setHeader("Content-Type", "text/html");
|
|
923
|
-
res.end(
|
|
924
|
-
}
|
|
1247
|
+
res.end(html);
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
next(err);
|
|
1250
|
+
}
|
|
925
1251
|
});
|
|
926
1252
|
};
|
|
927
1253
|
},
|
|
928
|
-
// For production build: inject the HTML as the input
|
|
929
1254
|
transformIndexHtml(html) {
|
|
930
1255
|
return html;
|
|
931
1256
|
}
|
|
932
1257
|
};
|
|
933
1258
|
}
|
|
934
|
-
var VIRTUAL_ENTRY_ID, RESOLVED_VIRTUAL_ENTRY_ID;
|
|
1259
|
+
var VIRTUAL_ENTRY_ID, RESOLVED_VIRTUAL_ENTRY_ID, APPFUNNEL_DIR;
|
|
935
1260
|
var init_plugin = __esm({
|
|
936
1261
|
"src/vite/plugin.ts"() {
|
|
937
1262
|
"use strict";
|
|
938
1263
|
init_entry();
|
|
939
1264
|
init_html();
|
|
940
1265
|
VIRTUAL_ENTRY_ID = "@appfunnel/entry";
|
|
941
|
-
RESOLVED_VIRTUAL_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID;
|
|
1266
|
+
RESOLVED_VIRTUAL_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID + ".tsx";
|
|
1267
|
+
APPFUNNEL_DIR = ".appfunnel";
|
|
942
1268
|
}
|
|
943
1269
|
});
|
|
944
1270
|
|
|
@@ -947,7 +1273,7 @@ var dev_exports = {};
|
|
|
947
1273
|
__export(dev_exports, {
|
|
948
1274
|
devCommand: () => devCommand
|
|
949
1275
|
});
|
|
950
|
-
import { readFileSync as
|
|
1276
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
951
1277
|
import { join as join8 } from "path";
|
|
952
1278
|
import pc6 from "picocolors";
|
|
953
1279
|
async function devCommand(options) {
|
|
@@ -963,13 +1289,22 @@ async function devCommand(options) {
|
|
|
963
1289
|
const projectId = await promptForProject(creds.token);
|
|
964
1290
|
config.projectId = projectId;
|
|
965
1291
|
const configPath = join8(cwd, "appfunnel.config.ts");
|
|
966
|
-
const configSource =
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1292
|
+
const configSource = readFileSync7(configPath, "utf-8");
|
|
1293
|
+
let updated;
|
|
1294
|
+
if (/projectId:\s*['"]/.test(configSource)) {
|
|
1295
|
+
updated = configSource.replace(
|
|
1296
|
+
/projectId:\s*['"].*?['"]/,
|
|
1297
|
+
`projectId: '${projectId}'`
|
|
1298
|
+
);
|
|
1299
|
+
} else {
|
|
1300
|
+
updated = configSource.replace(
|
|
1301
|
+
/(defineConfig\(\{[\t ]*\n)/,
|
|
1302
|
+
`$1 projectId: '${projectId}',
|
|
1303
|
+
`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
971
1306
|
if (updated !== configSource) {
|
|
972
|
-
|
|
1307
|
+
writeFileSync4(configPath, updated);
|
|
973
1308
|
success(`Updated projectId in appfunnel.config.ts`);
|
|
974
1309
|
} else {
|
|
975
1310
|
warn(`Could not auto-update appfunnel.config.ts \u2014 add projectId: '${projectId}' manually.`);
|
|
@@ -980,8 +1315,43 @@ async function devCommand(options) {
|
|
|
980
1315
|
let pages = await extractPageDefinitions(cwd, pageKeys);
|
|
981
1316
|
s2.stop();
|
|
982
1317
|
info(`Found ${pageKeys.length} pages: ${pageKeys.join(", ")}`);
|
|
1318
|
+
let priceData = /* @__PURE__ */ new Map();
|
|
1319
|
+
if (config.projectId && config.products?.items?.length) {
|
|
1320
|
+
try {
|
|
1321
|
+
const storePriceIds = [
|
|
1322
|
+
...new Set(
|
|
1323
|
+
config.products.items.flatMap(
|
|
1324
|
+
(item) => [item.storePriceId, item.trialStorePriceId].filter(Boolean)
|
|
1325
|
+
)
|
|
1326
|
+
)
|
|
1327
|
+
];
|
|
1328
|
+
info(`Fetching ${storePriceIds.length} store prices: ${storePriceIds.join(", ")}`);
|
|
1329
|
+
const s3 = spinner("Fetching store prices...");
|
|
1330
|
+
priceData = await fetchPrices(config.projectId, storePriceIds, { token: creds.token });
|
|
1331
|
+
s3.stop();
|
|
1332
|
+
const missingIds = storePriceIds.filter((id) => !priceData.has(id));
|
|
1333
|
+
if (missingIds.length > 0) {
|
|
1334
|
+
error(`Missing store prices: ${missingIds.join(", ")}`);
|
|
1335
|
+
error("Make sure these storePriceId values in your config match prices in your project.");
|
|
1336
|
+
process.exit(1);
|
|
1337
|
+
}
|
|
1338
|
+
success(`Fetched ${priceData.size}/${storePriceIds.length} store prices`);
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
error(`Failed to fetch store prices: ${err.message}`);
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
983
1344
|
const { createServer: createServer2 } = await import("vite");
|
|
984
1345
|
const react = await import("@vitejs/plugin-react");
|
|
1346
|
+
let tailwindPlugin = null;
|
|
1347
|
+
try {
|
|
1348
|
+
const { createRequire } = await import("module");
|
|
1349
|
+
const require2 = createRequire(join8(cwd, "package.json"));
|
|
1350
|
+
const tailwindPath = require2.resolve("@tailwindcss/vite");
|
|
1351
|
+
const tailwindVite = await import(tailwindPath);
|
|
1352
|
+
tailwindPlugin = tailwindVite.default;
|
|
1353
|
+
} catch {
|
|
1354
|
+
}
|
|
985
1355
|
const server = await createServer2({
|
|
986
1356
|
root: cwd,
|
|
987
1357
|
server: {
|
|
@@ -990,11 +1360,13 @@ async function devCommand(options) {
|
|
|
990
1360
|
},
|
|
991
1361
|
plugins: [
|
|
992
1362
|
react.default(),
|
|
1363
|
+
...tailwindPlugin ? [tailwindPlugin()] : [],
|
|
993
1364
|
appfunnelPlugin({
|
|
994
1365
|
cwd,
|
|
995
1366
|
config,
|
|
996
1367
|
pages,
|
|
997
1368
|
isDev: true,
|
|
1369
|
+
priceData,
|
|
998
1370
|
async onPagesChange() {
|
|
999
1371
|
pageKeys = scanPages(cwd);
|
|
1000
1372
|
pages = await extractPageDefinitions(cwd, pageKeys);
|
|
@@ -1028,6 +1400,7 @@ var init_dev = __esm({
|
|
|
1028
1400
|
init_pages();
|
|
1029
1401
|
init_plugin();
|
|
1030
1402
|
init_projects();
|
|
1403
|
+
init_api();
|
|
1031
1404
|
}
|
|
1032
1405
|
});
|
|
1033
1406
|
|
|
@@ -1036,8 +1409,9 @@ var build_exports = {};
|
|
|
1036
1409
|
__export(build_exports, {
|
|
1037
1410
|
buildCommand: () => buildCommand
|
|
1038
1411
|
});
|
|
1039
|
-
import { resolve as
|
|
1040
|
-
import {
|
|
1412
|
+
import { resolve as resolve3, join as join9 } from "path";
|
|
1413
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1414
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, statSync, readdirSync as readdirSync3 } from "fs";
|
|
1041
1415
|
import pc7 from "picocolors";
|
|
1042
1416
|
async function buildCommand() {
|
|
1043
1417
|
const cwd = process.cwd();
|
|
@@ -1058,15 +1432,25 @@ async function buildCommand() {
|
|
|
1058
1432
|
s.stop();
|
|
1059
1433
|
validateRoutes(config, pages, pageKeys);
|
|
1060
1434
|
info(`Building ${pageKeys.length} pages...`);
|
|
1061
|
-
const outDir =
|
|
1435
|
+
const outDir = resolve3(cwd, "dist");
|
|
1062
1436
|
const { build } = await import("vite");
|
|
1063
1437
|
const react = await import("@vitejs/plugin-react");
|
|
1064
|
-
|
|
1438
|
+
let tailwindPlugin = null;
|
|
1439
|
+
try {
|
|
1440
|
+
const { createRequire } = await import("module");
|
|
1441
|
+
const require2 = createRequire(join9(cwd, "package.json"));
|
|
1442
|
+
const tailwindPath = require2.resolve("@tailwindcss/vite");
|
|
1443
|
+
const tailwindVite = await import(tailwindPath);
|
|
1444
|
+
tailwindPlugin = tailwindVite.default;
|
|
1445
|
+
} catch {
|
|
1446
|
+
}
|
|
1447
|
+
const htmlPath = resolve3(cwd, "index.html");
|
|
1065
1448
|
const htmlContent = generateHtml(config.name || "AppFunnel");
|
|
1066
|
-
|
|
1449
|
+
writeFileSync5(htmlPath, htmlContent);
|
|
1067
1450
|
try {
|
|
1068
1451
|
await build({
|
|
1069
1452
|
root: cwd,
|
|
1453
|
+
base: "./",
|
|
1070
1454
|
build: {
|
|
1071
1455
|
outDir,
|
|
1072
1456
|
emptyOutDir: true,
|
|
@@ -1087,6 +1471,7 @@ async function buildCommand() {
|
|
|
1087
1471
|
},
|
|
1088
1472
|
plugins: [
|
|
1089
1473
|
react.default(),
|
|
1474
|
+
...tailwindPlugin ? [tailwindPlugin()] : [],
|
|
1090
1475
|
appfunnelPlugin({
|
|
1091
1476
|
cwd,
|
|
1092
1477
|
config,
|
|
@@ -1122,9 +1507,12 @@ async function buildCommand() {
|
|
|
1122
1507
|
const totalSize = assets.reduce((sum, a) => sum + a.size, 0);
|
|
1123
1508
|
const manifest = {
|
|
1124
1509
|
version: 1,
|
|
1510
|
+
buildHash: randomUUID2(),
|
|
1125
1511
|
sdkVersion: getSdkVersion2(cwd),
|
|
1126
1512
|
projectId: config.projectId,
|
|
1127
1513
|
funnelId: config.funnelId,
|
|
1514
|
+
name: config.name,
|
|
1515
|
+
initialPageKey: config.initialPageKey,
|
|
1128
1516
|
pages: { ...config.pages, ...mergedPages },
|
|
1129
1517
|
routes: { ...config.routes, ...mergedRoutes },
|
|
1130
1518
|
responses: config.responses || {},
|
|
@@ -1134,15 +1522,15 @@ async function buildCommand() {
|
|
|
1134
1522
|
assets,
|
|
1135
1523
|
totalSize
|
|
1136
1524
|
};
|
|
1137
|
-
|
|
1525
|
+
writeFileSync5(join9(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
1138
1526
|
console.log();
|
|
1139
1527
|
success("Build complete");
|
|
1140
1528
|
console.log();
|
|
1141
|
-
console.log(` ${pc7.dim("Output:")}
|
|
1529
|
+
console.log(` ${pc7.dim("Output:")} dist/`);
|
|
1142
1530
|
console.log(` ${pc7.dim("Pages:")} ${pageKeys.length}`);
|
|
1143
1531
|
console.log(` ${pc7.dim("Size:")} ${formatSize(totalSize)}`);
|
|
1144
1532
|
console.log();
|
|
1145
|
-
for (const asset of assets
|
|
1533
|
+
for (const asset of assets) {
|
|
1146
1534
|
const sizeStr = formatSize(asset.size);
|
|
1147
1535
|
const isOver = asset.size > MAX_PAGE_SIZE;
|
|
1148
1536
|
console.log(` ${isOver ? pc7.yellow("!") : pc7.dim("\xB7")} ${pc7.dim(asset.path)} ${isOver ? pc7.yellow(sizeStr) : pc7.dim(sizeStr)}`);
|
|
@@ -1234,7 +1622,7 @@ function validateConditionVariables(condition, pageKey, allVariables) {
|
|
|
1234
1622
|
function collectAssets(outDir) {
|
|
1235
1623
|
const assets = [];
|
|
1236
1624
|
function walk(dir, prefix = "") {
|
|
1237
|
-
for (const entry of
|
|
1625
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
1238
1626
|
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1239
1627
|
const fullPath = join9(dir, entry.name);
|
|
1240
1628
|
if (entry.isDirectory()) {
|
|
@@ -1254,7 +1642,7 @@ function formatSize(bytes) {
|
|
|
1254
1642
|
}
|
|
1255
1643
|
function getSdkVersion2(cwd) {
|
|
1256
1644
|
try {
|
|
1257
|
-
const pkg = JSON.parse(
|
|
1645
|
+
const pkg = JSON.parse(readFileSync8(join9(cwd, "node_modules", "@appfunnel", "sdk", "package.json"), "utf-8"));
|
|
1258
1646
|
return pkg.version;
|
|
1259
1647
|
} catch {
|
|
1260
1648
|
return "0.0.0";
|
|
@@ -1285,83 +1673,25 @@ var init_build = __esm({
|
|
|
1285
1673
|
}
|
|
1286
1674
|
});
|
|
1287
1675
|
|
|
1288
|
-
// src/lib/
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
...fetchOpts,
|
|
1303
|
-
headers
|
|
1304
|
-
});
|
|
1305
|
-
if (!response.ok) {
|
|
1306
|
-
const body = await response.text().catch(() => "");
|
|
1307
|
-
let message = `API request failed: ${response.status} ${response.statusText}`;
|
|
1308
|
-
try {
|
|
1309
|
-
const parsed = JSON.parse(body);
|
|
1310
|
-
if (parsed.error) message = parsed.error;
|
|
1311
|
-
if (parsed.message) message = parsed.message;
|
|
1312
|
-
} catch {
|
|
1313
|
-
}
|
|
1314
|
-
const error2 = new CLIError("API_ERROR", message);
|
|
1315
|
-
error2.statusCode = response.status;
|
|
1316
|
-
throw error2;
|
|
1317
|
-
}
|
|
1318
|
-
return response;
|
|
1319
|
-
}
|
|
1320
|
-
async function publishBuild(projectId, funnelId, manifest, assets, options) {
|
|
1321
|
-
const formData = new FormData();
|
|
1322
|
-
formData.set("manifest", JSON.stringify(manifest));
|
|
1323
|
-
formData.set("funnelId", funnelId);
|
|
1324
|
-
for (const asset of assets) {
|
|
1325
|
-
formData.append(
|
|
1326
|
-
"assets",
|
|
1327
|
-
new Blob([new Uint8Array(asset.content)], { type: asset.contentType }),
|
|
1328
|
-
asset.path
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
try {
|
|
1332
|
-
const response = await apiFetch(`/project/${projectId}/headless/publish`, {
|
|
1333
|
-
...options,
|
|
1334
|
-
method: "POST",
|
|
1335
|
-
body: formData
|
|
1336
|
-
});
|
|
1337
|
-
return await response.json();
|
|
1338
|
-
} catch (err) {
|
|
1339
|
-
if (err instanceof CLIError && err.code === "API_ERROR") {
|
|
1340
|
-
if (err.statusCode === 413) {
|
|
1341
|
-
throw new CLIError(
|
|
1342
|
-
"BUNDLE_TOO_LARGE",
|
|
1343
|
-
err.message,
|
|
1344
|
-
"Reduce page bundle sizes. Check for large dependencies."
|
|
1345
|
-
);
|
|
1346
|
-
}
|
|
1347
|
-
if (err.statusCode === 409) {
|
|
1348
|
-
throw new CLIError(
|
|
1349
|
-
"FUNNEL_NOT_HEADLESS",
|
|
1350
|
-
err.message,
|
|
1351
|
-
"Create a new headless funnel from the dashboard, or remove funnelId from config."
|
|
1352
|
-
);
|
|
1353
|
-
}
|
|
1354
|
-
throw new CLIError("PUBLISH_FAILED", err.message);
|
|
1355
|
-
}
|
|
1356
|
-
throw err;
|
|
1676
|
+
// src/lib/config-patch.ts
|
|
1677
|
+
import { join as join10 } from "path";
|
|
1678
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
1679
|
+
function patchConfigFunnelId(cwd, funnelId) {
|
|
1680
|
+
const configPath = join10(cwd, "appfunnel.config.ts");
|
|
1681
|
+
let content = readFileSync9(configPath, "utf-8");
|
|
1682
|
+
if (content.includes("funnelId")) return;
|
|
1683
|
+
const patched = content.replace(
|
|
1684
|
+
/(projectId:\s*['"][^'"]+['"],?\s*\n)/,
|
|
1685
|
+
`$1 funnelId: '${funnelId}',
|
|
1686
|
+
`
|
|
1687
|
+
);
|
|
1688
|
+
if (patched !== content) {
|
|
1689
|
+
writeFileSync6(configPath, patched, "utf-8");
|
|
1357
1690
|
}
|
|
1358
1691
|
}
|
|
1359
|
-
var
|
|
1360
|
-
|
|
1361
|
-
"src/lib/api.ts"() {
|
|
1692
|
+
var init_config_patch = __esm({
|
|
1693
|
+
"src/lib/config-patch.ts"() {
|
|
1362
1694
|
"use strict";
|
|
1363
|
-
init_errors();
|
|
1364
|
-
DEFAULT_API_BASE3 = "https://api.appfunnel.net";
|
|
1365
1695
|
}
|
|
1366
1696
|
});
|
|
1367
1697
|
|
|
@@ -1370,34 +1700,49 @@ var publish_exports = {};
|
|
|
1370
1700
|
__export(publish_exports, {
|
|
1371
1701
|
publishCommand: () => publishCommand
|
|
1372
1702
|
});
|
|
1373
|
-
import { resolve as
|
|
1374
|
-
import { readFileSync as
|
|
1703
|
+
import { resolve as resolve4, join as join11 } from "path";
|
|
1704
|
+
import { readFileSync as readFileSync10, existsSync as existsSync6 } from "fs";
|
|
1375
1705
|
import pc8 from "picocolors";
|
|
1376
1706
|
function getMimeType(path) {
|
|
1377
1707
|
const ext = path.substring(path.lastIndexOf("."));
|
|
1378
1708
|
return MIME_TYPES[ext] || "application/octet-stream";
|
|
1379
1709
|
}
|
|
1380
|
-
|
|
1710
|
+
function formatSize2(bytes) {
|
|
1711
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1712
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1713
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
1714
|
+
}
|
|
1715
|
+
async function publishCommand(options) {
|
|
1381
1716
|
const cwd = process.cwd();
|
|
1382
1717
|
const creds = requireAuth();
|
|
1383
1718
|
checkVersionCompatibility(cwd);
|
|
1384
1719
|
const config = await loadConfig(cwd);
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
1720
|
+
const projectId = config.projectId;
|
|
1721
|
+
if (!projectId) {
|
|
1722
|
+
throw new CLIError(
|
|
1723
|
+
"CONFIG_NOT_FOUND",
|
|
1724
|
+
"No projectId in appfunnel.config.ts.",
|
|
1725
|
+
"Add projectId to your config. You can find it in the dashboard."
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
const outDir = resolve4(cwd, "dist");
|
|
1729
|
+
const manifestPath = join11(outDir, "manifest.json");
|
|
1730
|
+
if (!existsSync6(manifestPath)) {
|
|
1388
1731
|
throw new CLIError(
|
|
1389
1732
|
"BUILD_NOT_FOUND",
|
|
1390
1733
|
"No build output found.",
|
|
1391
1734
|
"Run 'appfunnel build' first."
|
|
1392
1735
|
);
|
|
1393
1736
|
}
|
|
1394
|
-
const manifest = JSON.parse(
|
|
1737
|
+
const manifest = JSON.parse(readFileSync10(manifestPath, "utf-8"));
|
|
1395
1738
|
const assets = manifest.assets || [];
|
|
1396
|
-
const s = spinner("
|
|
1739
|
+
const s = spinner("Preparing assets...");
|
|
1397
1740
|
const assetPayloads = [];
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1741
|
+
let totalBytes = 0;
|
|
1742
|
+
for (let i = 0; i < assets.length; i++) {
|
|
1743
|
+
const asset = assets[i];
|
|
1744
|
+
const fullPath = join11(outDir, asset.path);
|
|
1745
|
+
if (!existsSync6(fullPath)) {
|
|
1401
1746
|
s.stop();
|
|
1402
1747
|
throw new CLIError(
|
|
1403
1748
|
"BUILD_NOT_FOUND",
|
|
@@ -1405,44 +1750,42 @@ async function publishCommand() {
|
|
|
1405
1750
|
"Run 'appfunnel build' to regenerate."
|
|
1406
1751
|
);
|
|
1407
1752
|
}
|
|
1753
|
+
const content = readFileSync10(fullPath);
|
|
1754
|
+
totalBytes += content.length;
|
|
1408
1755
|
assetPayloads.push({
|
|
1409
1756
|
path: asset.path,
|
|
1410
|
-
content
|
|
1757
|
+
content,
|
|
1411
1758
|
contentType: getMimeType(asset.path)
|
|
1412
1759
|
});
|
|
1760
|
+
s.text = `Preparing assets... ${i + 1}/${assets.length} ${pc8.dim(`(${formatSize2(totalBytes)})`)}`;
|
|
1413
1761
|
}
|
|
1414
|
-
|
|
1415
|
-
const funnelId = config.funnelId;
|
|
1416
|
-
if (!projectId) {
|
|
1417
|
-
s.stop();
|
|
1418
|
-
throw new CLIError(
|
|
1419
|
-
"CONFIG_NOT_FOUND",
|
|
1420
|
-
"No projectId in appfunnel.config.ts.",
|
|
1421
|
-
"Add projectId to your config. You can find it in the dashboard."
|
|
1422
|
-
);
|
|
1423
|
-
}
|
|
1424
|
-
if (!funnelId) {
|
|
1425
|
-
s.stop();
|
|
1426
|
-
throw new CLIError(
|
|
1427
|
-
"CONFIG_NOT_FOUND",
|
|
1428
|
-
"No funnelId in appfunnel.config.ts.",
|
|
1429
|
-
"Add funnelId to your config, or create a new funnel from the dashboard."
|
|
1430
|
-
);
|
|
1431
|
-
}
|
|
1762
|
+
s.text = `Uploading ${assets.length} assets ${pc8.dim(`(${formatSize2(totalBytes)})`)}`;
|
|
1432
1763
|
const result = await publishBuild(
|
|
1433
1764
|
projectId,
|
|
1434
|
-
funnelId,
|
|
1765
|
+
config.funnelId || "",
|
|
1435
1766
|
manifest,
|
|
1436
1767
|
assetPayloads,
|
|
1437
|
-
{ token: creds.token }
|
|
1768
|
+
{ token: creds.token },
|
|
1769
|
+
options?.promote
|
|
1438
1770
|
);
|
|
1439
1771
|
s.stop();
|
|
1772
|
+
if (result.created && result.funnelId) {
|
|
1773
|
+
patchConfigFunnelId(cwd, result.funnelId);
|
|
1774
|
+
info(`Funnel created \u2014 funnelId added to appfunnel.config.ts`);
|
|
1775
|
+
}
|
|
1440
1776
|
console.log();
|
|
1441
|
-
success("Published successfully");
|
|
1777
|
+
success(result.activated ? "Published and activated" : "Published successfully");
|
|
1442
1778
|
console.log();
|
|
1443
|
-
console.log(` ${pc8.dim("Build ID:")}
|
|
1444
|
-
|
|
1445
|
-
|
|
1779
|
+
console.log(` ${pc8.dim("Build ID:")} ${result.buildId}`);
|
|
1780
|
+
if (result.funnelId && !config.funnelId) {
|
|
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.`);
|
|
1788
|
+
}
|
|
1446
1789
|
console.log();
|
|
1447
1790
|
}
|
|
1448
1791
|
var MIME_TYPES;
|
|
@@ -1455,6 +1798,7 @@ var init_publish = __esm({
|
|
|
1455
1798
|
init_version();
|
|
1456
1799
|
init_api();
|
|
1457
1800
|
init_errors();
|
|
1801
|
+
init_config_patch();
|
|
1458
1802
|
MIME_TYPES = {
|
|
1459
1803
|
".js": "application/javascript",
|
|
1460
1804
|
".css": "text/css",
|
|
@@ -1474,10 +1818,10 @@ init_errors();
|
|
|
1474
1818
|
import { Command } from "commander";
|
|
1475
1819
|
import pc9 from "picocolors";
|
|
1476
1820
|
var program = new Command();
|
|
1477
|
-
program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.
|
|
1478
|
-
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 () => {
|
|
1479
1823
|
const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1480
|
-
await initCommand2(
|
|
1824
|
+
await initCommand2();
|
|
1481
1825
|
});
|
|
1482
1826
|
program.command("login").description("Authenticate with AppFunnel").action(async () => {
|
|
1483
1827
|
const { loginCommand: loginCommand2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
@@ -1495,9 +1839,9 @@ program.command("build").description("Build the funnel for production").action(a
|
|
|
1495
1839
|
const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_build(), build_exports));
|
|
1496
1840
|
await buildCommand2();
|
|
1497
1841
|
});
|
|
1498
|
-
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) => {
|
|
1499
1843
|
const { publishCommand: publishCommand2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
|
|
1500
|
-
await publishCommand2();
|
|
1844
|
+
await publishCommand2({ promote: options.promote });
|
|
1501
1845
|
});
|
|
1502
1846
|
program.hook("postAction", () => {
|
|
1503
1847
|
});
|