appfunnel 0.11.0 → 0.13.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 CHANGED
@@ -231,49 +231,21 @@ async function fetchStorePrices(projectId, storeId, options) {
231
231
  const data = await response.json();
232
232
  return data.data || [];
233
233
  }
234
- async function publishBuild(projectId, funnelId, manifest, assets, options, promote) {
235
- const formData = new FormData();
236
- formData.set("manifest", JSON.stringify(manifest));
237
- if (funnelId) {
238
- formData.set("funnelId", funnelId);
239
- }
240
- if (promote) {
241
- formData.set("promote", "true");
242
- }
243
- for (const asset of assets) {
244
- formData.append(
245
- "assets",
246
- new Blob([new Uint8Array(asset.content)], { type: asset.contentType }),
247
- asset.path
248
- );
249
- }
250
- try {
251
- const response = await apiFetch(`/project/${projectId}/headless/publish`, {
252
- ...options,
253
- method: "POST",
254
- body: formData
255
- });
256
- return await response.json();
257
- } catch (err) {
258
- if (err instanceof CLIError && err.code === "API_ERROR") {
259
- if (err.statusCode === 413) {
260
- throw new CLIError(
261
- "BUNDLE_TOO_LARGE",
262
- err.message,
263
- "Reduce page bundle sizes. Check for large dependencies."
264
- );
265
- }
266
- if (err.statusCode === 409) {
267
- throw new CLIError(
268
- "FUNNEL_NOT_HEADLESS",
269
- err.message,
270
- "Remove funnelId from config to create a new headless funnel."
271
- );
272
- }
273
- throw new CLIError("PUBLISH_FAILED", err.message);
274
- }
275
- throw err;
276
- }
234
+ async function publishInit(projectId, body, options) {
235
+ const response = await apiFetch(`/project/${projectId}/headless/publish-init`, {
236
+ ...options,
237
+ method: "POST",
238
+ body: JSON.stringify(body)
239
+ });
240
+ return await response.json();
241
+ }
242
+ async function publishFinalize(projectId, body, options) {
243
+ const response = await apiFetch(`/project/${projectId}/headless/publish-finalize`, {
244
+ ...options,
245
+ method: "POST",
246
+ body: JSON.stringify(body)
247
+ });
248
+ return await response.json();
277
249
  }
278
250
  var DEFAULT_API_BASE2;
279
251
  var init_api = __esm({
@@ -455,6 +427,12 @@ async function initCommand(nameArg) {
455
427
  const { renameSync } = await import("fs");
456
428
  renameSync(gitignoreSrc, join2(dir, ".gitignore"));
457
429
  }
430
+ const readmePath = join2(dir, "README.md");
431
+ if (existsSync(readmePath)) {
432
+ let readme = readFileSync2(readmePath, "utf-8");
433
+ readme = readme.replace("__NAME__", name);
434
+ writeFileSync2(readmePath, readme);
435
+ }
458
436
  const configPath = join2(dir, "appfunnel.config.ts");
459
437
  if (existsSync(configPath)) {
460
438
  let config = readFileSync2(configPath, "utf-8");
@@ -485,7 +463,7 @@ ${itemsStr},
485
463
  }
486
464
  writeFileSync2(configPath, config);
487
465
  }
488
- const sdkVersion = `^${"0.11.0"}`;
466
+ const sdkVersion = `^${"0.13.0"}`;
489
467
  writeFileSync2(
490
468
  join2(dir, "package.json"),
491
469
  JSON.stringify(
@@ -731,7 +709,7 @@ var init_config = __esm({
731
709
  import { readFileSync as readFileSync4 } from "fs";
732
710
  import { join as join4 } from "path";
733
711
  function checkVersionCompatibility(cwd) {
734
- const cliVersion = "0.11.0";
712
+ const cliVersion = "0.13.0";
735
713
  const sdkVersion = getSdkVersion(cwd);
736
714
  const [cliMajor, cliMinor] = cliVersion.split(".").map(Number);
737
715
  const [sdkMajor, sdkMinor] = sdkVersion.split(".").map(Number);
@@ -1448,6 +1426,386 @@ var init_dev = __esm({
1448
1426
  }
1449
1427
  });
1450
1428
 
1429
+ // src/lib/computeFlow.ts
1430
+ function computeFunnelFlow(pages, initialPageKey) {
1431
+ const allPageKeys = new Set(Object.keys(pages));
1432
+ if (allPageKeys.size === 0) {
1433
+ return { initialPageKey, segments: [], terminalPageKeys: [], unreachablePageKeys: [] };
1434
+ }
1435
+ function getTargets(pageKey) {
1436
+ const routes = pages[pageKey]?.routes || [];
1437
+ const seen = /* @__PURE__ */ new Set();
1438
+ const targets = [];
1439
+ for (const route of routes) {
1440
+ if (allPageKeys.has(route.to) && !seen.has(route.to)) {
1441
+ seen.add(route.to);
1442
+ targets.push(route.to);
1443
+ }
1444
+ }
1445
+ return targets;
1446
+ }
1447
+ const reachable = /* @__PURE__ */ new Set();
1448
+ {
1449
+ const queue = [];
1450
+ if (allPageKeys.has(initialPageKey)) {
1451
+ reachable.add(initialPageKey);
1452
+ queue.push(initialPageKey);
1453
+ }
1454
+ while (queue.length > 0) {
1455
+ const pageKey = queue.shift();
1456
+ for (const target of getTargets(pageKey)) {
1457
+ if (!reachable.has(target)) {
1458
+ reachable.add(target);
1459
+ queue.push(target);
1460
+ }
1461
+ }
1462
+ }
1463
+ }
1464
+ const unreachablePageKeys = [];
1465
+ const terminalPageKeys = [];
1466
+ for (const pageKey of allPageKeys) {
1467
+ if (!reachable.has(pageKey)) {
1468
+ unreachablePageKeys.push(pageKey);
1469
+ continue;
1470
+ }
1471
+ if (getTargets(pageKey).length === 0) {
1472
+ terminalPageKeys.push(pageKey);
1473
+ }
1474
+ }
1475
+ function bfsForward(start) {
1476
+ const dist = /* @__PURE__ */ new Map();
1477
+ dist.set(start, 0);
1478
+ const queue = [start];
1479
+ while (queue.length > 0) {
1480
+ const current2 = queue.shift();
1481
+ const d = dist.get(current2);
1482
+ for (const target of getTargets(current2)) {
1483
+ if (!dist.has(target)) {
1484
+ dist.set(target, d + 1);
1485
+ queue.push(target);
1486
+ }
1487
+ }
1488
+ }
1489
+ return dist;
1490
+ }
1491
+ function findConvergence(armStarts) {
1492
+ if (armStarts.length === 0) return null;
1493
+ const armReachable = armStarts.map((s) => bfsForward(s));
1494
+ const armStartSet = new Set(armStarts);
1495
+ let candidates = /* @__PURE__ */ new Set();
1496
+ for (const key of armReachable[0].keys()) {
1497
+ if (!armStartSet.has(key)) candidates.add(key);
1498
+ }
1499
+ for (let i = 1; i < armReachable.length; i++) {
1500
+ const reachSet = new Set(armReachable[i].keys());
1501
+ candidates = new Set([...candidates].filter((k) => reachSet.has(k)));
1502
+ }
1503
+ if (candidates.size === 0) return null;
1504
+ let bestPage = null;
1505
+ let bestMinDist = Infinity;
1506
+ for (const pageKey of candidates) {
1507
+ const minDist = Math.min(...armReachable.map((r) => r.get(pageKey) ?? Infinity));
1508
+ if (minDist < bestMinDist) {
1509
+ bestMinDist = minDist;
1510
+ bestPage = pageKey;
1511
+ }
1512
+ }
1513
+ return bestPage;
1514
+ }
1515
+ function walkArm(start, convergence, globalVisited2) {
1516
+ if (convergence === null) {
1517
+ const pages2 = [];
1518
+ let current2 = start;
1519
+ while (true) {
1520
+ if (globalVisited2.has(current2)) break;
1521
+ globalVisited2.add(current2);
1522
+ pages2.push(current2);
1523
+ const targets = getTargets(current2);
1524
+ if (targets.length === 0) break;
1525
+ const next = targets.find((t) => !globalVisited2.has(t));
1526
+ if (!next) break;
1527
+ current2 = next;
1528
+ }
1529
+ return pages2;
1530
+ }
1531
+ const parent = /* @__PURE__ */ new Map();
1532
+ parent.set(start, null);
1533
+ const queue = [start];
1534
+ let found = false;
1535
+ while (queue.length > 0 && !found) {
1536
+ const current2 = queue.shift();
1537
+ for (const target of getTargets(current2)) {
1538
+ if (!parent.has(target)) {
1539
+ parent.set(target, current2);
1540
+ if (target === convergence) {
1541
+ found = true;
1542
+ break;
1543
+ }
1544
+ queue.push(target);
1545
+ }
1546
+ }
1547
+ }
1548
+ if (!found) {
1549
+ const pages2 = [];
1550
+ let current2 = start;
1551
+ while (true) {
1552
+ if (globalVisited2.has(current2)) break;
1553
+ globalVisited2.add(current2);
1554
+ pages2.push(current2);
1555
+ const targets = getTargets(current2);
1556
+ if (targets.length === 0) break;
1557
+ const next = targets.find((t) => !globalVisited2.has(t));
1558
+ if (!next) break;
1559
+ current2 = next;
1560
+ }
1561
+ return pages2;
1562
+ }
1563
+ const path = [];
1564
+ let node = convergence;
1565
+ while (node !== null && node !== start) {
1566
+ path.unshift(node);
1567
+ node = parent.get(node) ?? null;
1568
+ }
1569
+ path.unshift(start);
1570
+ if (path[path.length - 1] === convergence) {
1571
+ path.pop();
1572
+ }
1573
+ for (const p of path) {
1574
+ globalVisited2.add(p);
1575
+ }
1576
+ return path;
1577
+ }
1578
+ if (!allPageKeys.has(initialPageKey)) {
1579
+ return { initialPageKey, segments: [], terminalPageKeys, unreachablePageKeys };
1580
+ }
1581
+ const segments = [];
1582
+ const globalVisited = /* @__PURE__ */ new Set();
1583
+ let current = initialPageKey;
1584
+ let sharedPages = [];
1585
+ while (current !== null && !globalVisited.has(current)) {
1586
+ const targets = getTargets(current);
1587
+ if (targets.length === 0) {
1588
+ sharedPages.push(current);
1589
+ globalVisited.add(current);
1590
+ break;
1591
+ }
1592
+ const uniqueTargets = [...new Set(targets)];
1593
+ if (uniqueTargets.length === 1) {
1594
+ sharedPages.push(current);
1595
+ globalVisited.add(current);
1596
+ current = uniqueTargets[0];
1597
+ continue;
1598
+ }
1599
+ sharedPages.push(current);
1600
+ globalVisited.add(current);
1601
+ segments.push({ type: "shared", pageKeys: [...sharedPages] });
1602
+ sharedPages = [];
1603
+ const convergence = findConvergence(uniqueTargets);
1604
+ const arms = [];
1605
+ for (const armStart of uniqueTargets) {
1606
+ const armPages = walkArm(armStart, convergence, globalVisited);
1607
+ arms.push({ pageKeys: armPages });
1608
+ }
1609
+ segments.push({ type: "branch", sourcePageKey: current, arms });
1610
+ if (convergence !== null && !globalVisited.has(convergence)) {
1611
+ current = convergence;
1612
+ } else {
1613
+ current = null;
1614
+ }
1615
+ }
1616
+ if (sharedPages.length > 0) {
1617
+ segments.push({ type: "shared", pageKeys: sharedPages });
1618
+ }
1619
+ return { initialPageKey, segments, terminalPageKeys, unreachablePageKeys };
1620
+ }
1621
+ var init_computeFlow = __esm({
1622
+ "src/lib/computeFlow.ts"() {
1623
+ "use strict";
1624
+ }
1625
+ });
1626
+
1627
+ // src/commands/routes.ts
1628
+ var routes_exports = {};
1629
+ __export(routes_exports, {
1630
+ routesCommand: () => routesCommand
1631
+ });
1632
+ import pc7 from "picocolors";
1633
+ async function routesCommand() {
1634
+ const cwd = process.cwd();
1635
+ const s = spinner("Analyzing pages...");
1636
+ const config = await loadConfig(cwd);
1637
+ const pageKeys = scanPages(cwd);
1638
+ const pages = await extractPageDefinitions(cwd, pageKeys);
1639
+ s.stop();
1640
+ const flow = computeFunnelFlow(pages, config.initialPageKey);
1641
+ console.log();
1642
+ console.log(` ${pc7.bold(config.name)}`);
1643
+ console.log(` ${pc7.dim(`${pageKeys.length} pages`)}`);
1644
+ console.log();
1645
+ renderFlow(flow, pages);
1646
+ }
1647
+ function renderFlow(flow, pages) {
1648
+ const indent = " ";
1649
+ for (let i = 0; i < flow.segments.length; i++) {
1650
+ const segment = flow.segments[i];
1651
+ if (segment.type === "shared") {
1652
+ for (let j = 0; j < segment.pageKeys.length; j++) {
1653
+ const key = segment.pageKeys[j];
1654
+ const page = pages[key];
1655
+ const isStart = key === flow.initialPageKey;
1656
+ const isTerminal = flow.terminalPageKeys.includes(key);
1657
+ printPageBox(indent, key, page, isStart, isTerminal);
1658
+ const isLastInSegment = j === segment.pageKeys.length - 1;
1659
+ const hasNextSegment = i < flow.segments.length - 1;
1660
+ if ((!isLastInSegment || hasNextSegment) && !isTerminal) {
1661
+ printArrow(indent);
1662
+ }
1663
+ }
1664
+ } else {
1665
+ renderBranch(indent, segment, pages, flow);
1666
+ if (i < flow.segments.length - 1) {
1667
+ printArrow(indent);
1668
+ }
1669
+ }
1670
+ }
1671
+ if (flow.unreachablePageKeys.length > 0) {
1672
+ console.log();
1673
+ console.log(` ${pc7.yellow("\u26A0")} ${pc7.yellow("Unreachable pages:")}`);
1674
+ for (const key of flow.unreachablePageKeys) {
1675
+ const page = pages[key];
1676
+ console.log(` ${pc7.dim("\u2022")} ${key}${page?.name ? pc7.dim(` (${page.name})`) : ""}`);
1677
+ }
1678
+ }
1679
+ console.log();
1680
+ }
1681
+ function formatPageLabel(key, page, isStart, isTerminal) {
1682
+ const name = page?.name || key;
1683
+ const type = page?.type;
1684
+ const badges = [];
1685
+ if (isStart) badges.push(pc7.green("start"));
1686
+ if (isTerminal) badges.push(pc7.red("end"));
1687
+ if (type && type !== "default") badges.push(pc7.magenta(type));
1688
+ const label = key === name.toLowerCase().replace(/\s+/g, "-") ? name : `${name} ${pc7.dim(`(${key})`)}`;
1689
+ const badgeStr = badges.length > 0 ? " " + badges.map((b) => pc7.dim("[") + b + pc7.dim("]")).join(" ") : "";
1690
+ return label + badgeStr;
1691
+ }
1692
+ function printPageBox(indent, key, page, isStart, isTerminal) {
1693
+ const label = formatPageLabel(key, page, isStart, isTerminal);
1694
+ const rawLen = stripAnsi(label).length;
1695
+ const boxWidth = Math.max(rawLen + 2, 20);
1696
+ const padding = boxWidth - rawLen - 2;
1697
+ console.log(`${indent}\u250C${H.repeat(boxWidth)}\u2510`);
1698
+ console.log(`${indent}\u2502 ${label}${" ".repeat(padding)} \u2502`);
1699
+ console.log(`${indent}\u2514${H.repeat(boxWidth)}\u2518`);
1700
+ }
1701
+ function printArrow(indent) {
1702
+ console.log(`${indent} ${V}`);
1703
+ console.log(`${indent} \u25BC`);
1704
+ }
1705
+ function renderBranch(indent, segment, pages, flow) {
1706
+ const arms = segment.arms;
1707
+ const colWidths = arms.map((arm) => {
1708
+ const maxNameLen = Math.max(
1709
+ ...arm.pageKeys.map((key) => {
1710
+ const page = pages[key];
1711
+ const name = page?.name || key;
1712
+ const type = page?.type;
1713
+ const typeLen = type && type !== "default" ? ` [${type}]`.length : 0;
1714
+ return name.length + typeLen;
1715
+ }),
1716
+ 8
1717
+ );
1718
+ return maxNameLen + 4;
1719
+ });
1720
+ let splitLine = "";
1721
+ for (let a = 0; a < arms.length; a++) {
1722
+ const mid = Math.floor(colWidths[a] / 2);
1723
+ if (a === 0) {
1724
+ splitLine += " ".repeat(mid) + "\u251C" + H.repeat(colWidths[a] - mid - 1);
1725
+ } else if (a === arms.length - 1) {
1726
+ splitLine += H.repeat(mid) + "\u2524" + " ".repeat(colWidths[a] - mid - 1);
1727
+ } else {
1728
+ splitLine += H.repeat(mid) + "\u252C" + H.repeat(colWidths[a] - mid - 1);
1729
+ }
1730
+ if (a < arms.length - 1) splitLine += H.repeat(3);
1731
+ }
1732
+ console.log(`${indent}${splitLine}`);
1733
+ let pipeLine = "";
1734
+ for (let a = 0; a < arms.length; a++) {
1735
+ const mid = Math.floor(colWidths[a] / 2);
1736
+ pipeLine += " ".repeat(mid) + V + " ".repeat(colWidths[a] - mid - 1);
1737
+ if (a < arms.length - 1) pipeLine += " ";
1738
+ }
1739
+ console.log(`${indent}${pipeLine}`);
1740
+ const maxDepth = Math.max(...arms.map((a) => a.pageKeys.length));
1741
+ for (let row = 0; row < maxDepth; row++) {
1742
+ let line = "";
1743
+ for (let a = 0; a < arms.length; a++) {
1744
+ const key = arms[a].pageKeys[row];
1745
+ const mid = Math.floor(colWidths[a] / 2);
1746
+ if (key) {
1747
+ const page = pages[key];
1748
+ const name = page?.name || key;
1749
+ const type = page?.type;
1750
+ const typeBadge = type && type !== "default" ? " " + pc7.magenta(`[${type}]`) : "";
1751
+ const display = name + typeBadge;
1752
+ const displayLen = stripAnsi(display).length;
1753
+ const truncated = displayLen > colWidths[a] - 2 ? name.slice(0, colWidths[a] - 5) + "..." : display;
1754
+ const truncLen = stripAnsi(truncated).length;
1755
+ const leftPad = mid - Math.floor(truncLen / 2);
1756
+ const rightPad = colWidths[a] - leftPad - truncLen;
1757
+ line += " ".repeat(Math.max(0, leftPad)) + pc7.bold(truncated) + " ".repeat(Math.max(0, rightPad));
1758
+ } else {
1759
+ line += " ".repeat(colWidths[a]);
1760
+ }
1761
+ if (a < arms.length - 1) line += " ";
1762
+ }
1763
+ console.log(`${indent}${line}`);
1764
+ if (row < maxDepth - 1) {
1765
+ let connector = "";
1766
+ for (let a = 0; a < arms.length; a++) {
1767
+ const mid = Math.floor(colWidths[a] / 2);
1768
+ const hasMore = row + 1 < arms[a].pageKeys.length;
1769
+ if (hasMore) {
1770
+ connector += " ".repeat(mid) + "\u25BC" + " ".repeat(colWidths[a] - mid - 1);
1771
+ } else {
1772
+ connector += " ".repeat(colWidths[a]);
1773
+ }
1774
+ if (a < arms.length - 1) connector += " ";
1775
+ }
1776
+ console.log(`${indent}${connector}`);
1777
+ }
1778
+ }
1779
+ let mergeLine = "";
1780
+ for (let a = 0; a < arms.length; a++) {
1781
+ const mid = Math.floor(colWidths[a] / 2);
1782
+ if (a === 0) {
1783
+ mergeLine += " ".repeat(mid) + "\u251C" + H.repeat(colWidths[a] - mid - 1);
1784
+ } else if (a === arms.length - 1) {
1785
+ mergeLine += H.repeat(mid) + "\u2524" + " ".repeat(colWidths[a] - mid - 1);
1786
+ } else {
1787
+ mergeLine += H.repeat(mid) + "\u2534" + H.repeat(colWidths[a] - mid - 1);
1788
+ }
1789
+ if (a < arms.length - 1) mergeLine += H.repeat(3);
1790
+ }
1791
+ console.log(`${indent}${mergeLine}`);
1792
+ }
1793
+ function stripAnsi(str) {
1794
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
1795
+ }
1796
+ var H, V;
1797
+ var init_routes = __esm({
1798
+ "src/commands/routes.ts"() {
1799
+ "use strict";
1800
+ init_logger();
1801
+ init_config();
1802
+ init_pages();
1803
+ init_computeFlow();
1804
+ H = "\u2500";
1805
+ V = "\u2502";
1806
+ }
1807
+ });
1808
+
1451
1809
  // src/commands/build.ts
1452
1810
  var build_exports = {};
1453
1811
  __export(build_exports, {
@@ -1456,7 +1814,7 @@ __export(build_exports, {
1456
1814
  import { resolve as resolve3, join as join9 } from "path";
1457
1815
  import { randomUUID as randomUUID2 } from "crypto";
1458
1816
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, statSync, readdirSync as readdirSync4 } from "fs";
1459
- import pc7 from "picocolors";
1817
+ import pc8 from "picocolors";
1460
1818
  async function buildCommand() {
1461
1819
  const cwd = process.cwd();
1462
1820
  requireAuth();
@@ -1570,6 +1928,7 @@ async function buildCommand() {
1570
1928
  responses: config.responses || {},
1571
1929
  queryParams: { ...BUILTIN_QUERY_PARAMS, ...config.queryParams },
1572
1930
  data: config.data || {},
1931
+ user: BUILTIN_USER_VARS,
1573
1932
  products: config.products || {},
1574
1933
  defaultLocale: config.defaultLocale,
1575
1934
  assets,
@@ -1579,20 +1938,20 @@ async function buildCommand() {
1579
1938
  console.log();
1580
1939
  success("Build complete");
1581
1940
  console.log();
1582
- console.log(` ${pc7.dim("Output:")} dist/`);
1583
- console.log(` ${pc7.dim("Pages:")} ${pageKeys.length}`);
1584
- console.log(` ${pc7.dim("Size:")} ${formatSize(totalSize)}`);
1941
+ console.log(` ${pc8.dim("Output:")} dist/`);
1942
+ console.log(` ${pc8.dim("Pages:")} ${pageKeys.length}`);
1943
+ console.log(` ${pc8.dim("Size:")} ${formatSize(totalSize)}`);
1585
1944
  console.log();
1586
1945
  for (const asset of assets) {
1587
1946
  const sizeStr = formatSize(asset.size);
1588
1947
  const isOver = asset.size > MAX_PAGE_SIZE;
1589
- console.log(` ${isOver ? pc7.yellow("!") : pc7.dim("\xB7")} ${pc7.dim(asset.path)} ${isOver ? pc7.yellow(sizeStr) : pc7.dim(sizeStr)}`);
1948
+ console.log(` ${isOver ? pc8.yellow("!") : pc8.dim("\xB7")} ${pc8.dim(asset.path)} ${isOver ? pc8.yellow(sizeStr) : pc8.dim(sizeStr)}`);
1590
1949
  }
1591
1950
  console.log();
1592
1951
  if (totalSize > MAX_TOTAL_SIZE) {
1593
1952
  console.log(formatWarning(
1594
1953
  "BUNDLE_TOO_LARGE",
1595
- `Total bundle size (${formatSize(totalSize)}) exceeds the 2MB limit.`,
1954
+ `Total bundle size (${formatSize(totalSize)}) exceeds the 10MB limit.`,
1596
1955
  "This will be rejected on publish. Reduce dependencies or code-split."
1597
1956
  ));
1598
1957
  console.log();
@@ -1701,7 +2060,7 @@ function getSdkVersion2(cwd) {
1701
2060
  return "0.0.0";
1702
2061
  }
1703
2062
  }
1704
- var MAX_TOTAL_SIZE, MAX_PAGE_SIZE, BUILTIN_QUERY_PARAMS;
2063
+ var MAX_TOTAL_SIZE, MAX_PAGE_SIZE, BUILTIN_QUERY_PARAMS, BUILTIN_USER_VARS;
1705
2064
  var init_build = __esm({
1706
2065
  "src/commands/build.ts"() {
1707
2066
  "use strict";
@@ -1714,7 +2073,7 @@ var init_build = __esm({
1714
2073
  init_html();
1715
2074
  init_errors();
1716
2075
  init_errors();
1717
- MAX_TOTAL_SIZE = 2 * 1024 * 1024;
2076
+ MAX_TOTAL_SIZE = 10 * 1024 * 1024;
1718
2077
  MAX_PAGE_SIZE = 500 * 1024;
1719
2078
  BUILTIN_QUERY_PARAMS = {
1720
2079
  utm_source: { type: "string" },
@@ -1723,6 +2082,14 @@ var init_build = __esm({
1723
2082
  utm_content: { type: "string" },
1724
2083
  utm_term: { type: "string" }
1725
2084
  };
2085
+ BUILTIN_USER_VARS = {
2086
+ email: { type: "string" },
2087
+ name: { type: "string" },
2088
+ dateOfBirth: { type: "string" },
2089
+ gender: { type: "string" },
2090
+ stripeCustomerId: { type: "string" },
2091
+ paddleCustomerId: { type: "string" }
2092
+ };
1726
2093
  }
1727
2094
  });
1728
2095
 
@@ -1755,10 +2122,22 @@ __export(publish_exports, {
1755
2122
  });
1756
2123
  import { resolve as resolve4, join as join11 } from "path";
1757
2124
  import { readFileSync as readFileSync10, existsSync as existsSync6 } from "fs";
1758
- import pc8 from "picocolors";
1759
- function getMimeType(path) {
1760
- const ext = path.substring(path.lastIndexOf("."));
1761
- return MIME_TYPES[ext] || "application/octet-stream";
2125
+ import { execSync } from "child_process";
2126
+ import pc9 from "picocolors";
2127
+ import select3 from "@inquirer/select";
2128
+ function tryGit(command, cwd) {
2129
+ try {
2130
+ return execSync(command, { cwd, stdio: ["pipe", "pipe", "ignore"] }).toString().trim() || void 0;
2131
+ } catch {
2132
+ return void 0;
2133
+ }
2134
+ }
2135
+ function getGitMetadata(cwd) {
2136
+ const commitSha = tryGit("git rev-parse HEAD", cwd);
2137
+ if (!commitSha) return {};
2138
+ const branch = tryGit("git rev-parse --abbrev-ref HEAD", cwd);
2139
+ const message = tryGit("git log -1 --pretty=%s", cwd);
2140
+ return { commitSha, branch, message };
1762
2141
  }
1763
2142
  function formatSize2(bytes) {
1764
2143
  if (bytes < 1024) return `${bytes}B`;
@@ -1790,7 +2169,7 @@ async function publishCommand(options) {
1790
2169
  const manifest = JSON.parse(readFileSync10(manifestPath, "utf-8"));
1791
2170
  const assets = manifest.assets || [];
1792
2171
  const s = spinner("Preparing assets...");
1793
- const assetPayloads = [];
2172
+ const assetBuffers = [];
1794
2173
  let totalBytes = 0;
1795
2174
  for (let i = 0; i < assets.length; i++) {
1796
2175
  const asset = assets[i];
@@ -1803,45 +2182,133 @@ async function publishCommand(options) {
1803
2182
  "Run 'appfunnel build' to regenerate."
1804
2183
  );
1805
2184
  }
1806
- const content = readFileSync10(fullPath);
1807
- totalBytes += content.length;
1808
- assetPayloads.push({
1809
- path: asset.path,
1810
- content,
1811
- contentType: getMimeType(asset.path)
2185
+ const body = readFileSync10(fullPath);
2186
+ totalBytes += body.length;
2187
+ assetBuffers.push({ path: asset.path, body, size: body.length });
2188
+ s.text = `Preparing assets... ${i + 1}/${assets.length} ${pc9.dim(`(${formatSize2(totalBytes)})`)}`;
2189
+ }
2190
+ s.stop();
2191
+ const MAX_TOTAL_SIZE2 = 10 * 1024 * 1024;
2192
+ if (totalBytes > MAX_TOTAL_SIZE2) {
2193
+ throw new CLIError(
2194
+ "BUNDLE_TOO_LARGE",
2195
+ `Total bundle size (${formatSize2(totalBytes)}) exceeds the ${formatSize2(MAX_TOTAL_SIZE2)} limit.`,
2196
+ "Reduce page bundle sizes. Check for large dependencies."
2197
+ );
2198
+ }
2199
+ info(`${assets.length} assets prepared ${pc9.dim(`(${formatSize2(totalBytes)})`)}`);
2200
+ let promote = options?.promote ?? false;
2201
+ if (!promote) {
2202
+ promote = await select3({
2203
+ message: "Publish as the new active version?",
2204
+ choices: [
2205
+ { name: "Yes", value: true },
2206
+ { name: "No", value: false }
2207
+ ]
2208
+ });
2209
+ }
2210
+ const git = getGitMetadata(cwd);
2211
+ const initSpinner = spinner("Initializing publish...");
2212
+ let initResult;
2213
+ try {
2214
+ initResult = await publishInit(
2215
+ projectId,
2216
+ {
2217
+ manifest,
2218
+ funnelId: config.funnelId || void 0,
2219
+ promote,
2220
+ commitSha: git.commitSha,
2221
+ branch: git.branch,
2222
+ message: git.message,
2223
+ triggeredBy: creds.email || void 0
2224
+ },
2225
+ { token: creds.token }
2226
+ );
2227
+ } catch (err) {
2228
+ initSpinner.stop();
2229
+ if (err instanceof CLIError && err.code === "API_ERROR") {
2230
+ if (err.statusCode === 413) {
2231
+ throw new CLIError(
2232
+ "BUNDLE_TOO_LARGE",
2233
+ err.message,
2234
+ "Reduce page bundle sizes. Check for large dependencies."
2235
+ );
2236
+ }
2237
+ if (err.statusCode === 409) {
2238
+ throw new CLIError(
2239
+ "FUNNEL_NOT_HEADLESS",
2240
+ err.message,
2241
+ "Remove funnelId from config to create a new headless funnel."
2242
+ );
2243
+ }
2244
+ throw new CLIError("PUBLISH_FAILED", err.message);
2245
+ }
2246
+ throw err;
2247
+ }
2248
+ initSpinner.stop();
2249
+ info(`Build ${pc9.dim(initResult.buildId)} initialized`);
2250
+ const uploadSpinner = spinner(`Uploading 0/${assets.length} assets...`);
2251
+ const uploadInfoMap = new Map(initResult.assets.map((a) => [a.path, a]));
2252
+ let uploaded = 0;
2253
+ let uploadedBytes = 0;
2254
+ const uploadAsset = async (asset) => {
2255
+ const info2 = uploadInfoMap.get(asset.path);
2256
+ if (!info2) {
2257
+ throw new CLIError("PUBLISH_FAILED", `No upload URL for asset: ${asset.path}`);
2258
+ }
2259
+ const response = await fetch(info2.uploadUrl, {
2260
+ method: "PUT",
2261
+ body: new Uint8Array(asset.body),
2262
+ headers: {
2263
+ "Content-Type": info2.contentType
2264
+ }
1812
2265
  });
1813
- s.text = `Preparing assets... ${i + 1}/${assets.length} ${pc8.dim(`(${formatSize2(totalBytes)})`)}`;
2266
+ if (!response.ok) {
2267
+ throw new CLIError(
2268
+ "PUBLISH_FAILED",
2269
+ `Failed to upload ${asset.path}: ${response.status} ${response.statusText}`
2270
+ );
2271
+ }
2272
+ uploaded++;
2273
+ uploadedBytes += asset.size;
2274
+ uploadSpinner.text = `Uploading ${uploaded}/${assets.length} assets... ${pc9.dim(`(${formatSize2(uploadedBytes)}/${formatSize2(totalBytes)})`)}`;
2275
+ };
2276
+ const queue = [...assetBuffers];
2277
+ const workers = [];
2278
+ for (let i = 0; i < Math.min(UPLOAD_CONCURRENCY, queue.length); i++) {
2279
+ workers.push((async () => {
2280
+ while (queue.length > 0) {
2281
+ const asset = queue.shift();
2282
+ await uploadAsset(asset);
2283
+ }
2284
+ })());
1814
2285
  }
1815
- s.text = `Uploading ${assets.length} assets ${pc8.dim(`(${formatSize2(totalBytes)})`)}`;
1816
- const result = await publishBuild(
2286
+ await Promise.all(workers);
2287
+ uploadSpinner.stop();
2288
+ info(`${assets.length} assets uploaded ${pc9.dim(`(${formatSize2(totalBytes)})`)}`);
2289
+ const finalizeSpinner = spinner("Finalizing publish...");
2290
+ const result = await publishFinalize(
1817
2291
  projectId,
1818
- config.funnelId || "",
1819
- manifest,
1820
- assetPayloads,
1821
- { token: creds.token },
1822
- options?.promote
2292
+ { buildId: initResult.buildId, funnelId: initResult.funnelId },
2293
+ { token: creds.token }
1823
2294
  );
1824
- s.stop();
1825
- if (result.created && result.funnelId) {
1826
- patchConfigFunnelId(cwd, result.funnelId);
2295
+ finalizeSpinner.stop();
2296
+ if (initResult.created && initResult.funnelId) {
2297
+ patchConfigFunnelId(cwd, initResult.funnelId);
1827
2298
  info(`Funnel created \u2014 funnelId added to appfunnel.config.ts`);
1828
2299
  }
1829
2300
  console.log();
1830
2301
  success(result.activated ? "Published and activated" : "Published successfully");
1831
2302
  console.log();
1832
- console.log(` ${pc8.dim("Build ID:")} ${result.buildId}`);
1833
- if (result.funnelId && !config.funnelId) {
1834
- console.log(` ${pc8.dim("Funnel:")} ${result.funnelId}`);
1835
- }
1836
- console.log(` ${pc8.dim("Dashboard:")} ${pc8.cyan(result.dashboardUrl)}`);
1837
- console.log(` ${pc8.dim("Assets:")} ${assets.length} files ${pc8.dim(`(${formatSize2(totalBytes)})`)}`);
1838
- if (!result.activated) {
1839
- console.log();
1840
- console.log(` ${pc8.dim("Tip:")} Use ${pc8.cyan("--promote")} to activate immediately, or promote from the dashboard.`);
2303
+ console.log(` ${pc9.dim("Build ID:")} ${result.buildId}`);
2304
+ if (initResult.funnelId && !config.funnelId) {
2305
+ console.log(` ${pc9.dim("Funnel:")} ${initResult.funnelId}`);
1841
2306
  }
2307
+ console.log(` ${pc9.dim("Dashboard:")} ${pc9.cyan(result.dashboardUrl)}`);
2308
+ console.log(` ${pc9.dim("Assets:")} ${assets.length} files ${pc9.dim(`(${formatSize2(totalBytes)})`)}`);
1842
2309
  console.log();
1843
2310
  }
1844
- var MIME_TYPES;
2311
+ var UPLOAD_CONCURRENCY;
1845
2312
  var init_publish = __esm({
1846
2313
  "src/commands/publish.ts"() {
1847
2314
  "use strict";
@@ -1852,26 +2319,16 @@ var init_publish = __esm({
1852
2319
  init_api();
1853
2320
  init_errors();
1854
2321
  init_config_patch();
1855
- MIME_TYPES = {
1856
- ".js": "application/javascript",
1857
- ".css": "text/css",
1858
- ".html": "text/html",
1859
- ".json": "application/json",
1860
- ".svg": "image/svg+xml",
1861
- ".png": "image/png",
1862
- ".jpg": "image/jpeg",
1863
- ".woff2": "font/woff2",
1864
- ".woff": "font/woff"
1865
- };
2322
+ UPLOAD_CONCURRENCY = 10;
1866
2323
  }
1867
2324
  });
1868
2325
 
1869
2326
  // src/index.ts
1870
2327
  init_errors();
1871
2328
  import { Command } from "commander";
1872
- import pc9 from "picocolors";
2329
+ import pc10 from "picocolors";
1873
2330
  var program = new Command();
1874
- program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.11.0");
2331
+ program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.13.0");
1875
2332
  program.command("init").argument("[name]", "Project directory name").description("Create a new AppFunnel project").action(async (name) => {
1876
2333
  const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1877
2334
  await initCommand2(name);
@@ -1888,6 +2345,10 @@ program.command("dev").description("Start the development server").option("-p, -
1888
2345
  const { devCommand: devCommand2 } = await Promise.resolve().then(() => (init_dev(), dev_exports));
1889
2346
  await devCommand2({ port: parseInt(options.port, 10) });
1890
2347
  });
2348
+ program.command("routes").description("Visualize the funnel page flow").action(async () => {
2349
+ const { routesCommand: routesCommand2 } = await Promise.resolve().then(() => (init_routes(), routes_exports));
2350
+ await routesCommand2();
2351
+ });
1891
2352
  program.command("build").description("Build the funnel for production").action(async () => {
1892
2353
  const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_build(), build_exports));
1893
2354
  await buildCommand2();
@@ -1906,9 +2367,9 @@ async function main() {
1906
2367
  console.error(formatError(err));
1907
2368
  process.exit(1);
1908
2369
  }
1909
- console.error(`${pc9.red("ERROR")}: ${err instanceof Error ? err.message : String(err)}`);
2370
+ console.error(`${pc10.red("ERROR")}: ${err instanceof Error ? err.message : String(err)}`);
1910
2371
  if (err instanceof Error && err.stack) {
1911
- console.error(pc9.dim(err.stack));
2372
+ console.error(pc10.dim(err.stack));
1912
2373
  }
1913
2374
  process.exit(1);
1914
2375
  }