appfunnel 0.4.0 → 0.6.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 +432 -209
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -48,7 +48,6 @@ var init_errors = __esm({
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
// src/lib/logger.ts
|
|
51
|
-
import { readFileSync } from "fs";
|
|
52
51
|
import pc2 from "picocolors";
|
|
53
52
|
import ora from "ora";
|
|
54
53
|
function success(msg) {
|
|
@@ -73,12 +72,12 @@ var init_logger = __esm({
|
|
|
73
72
|
});
|
|
74
73
|
|
|
75
74
|
// src/lib/auth.ts
|
|
76
|
-
import { readFileSync
|
|
75
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
77
76
|
import { join } from "path";
|
|
78
77
|
import { homedir } from "os";
|
|
79
78
|
function readCredentials() {
|
|
80
79
|
try {
|
|
81
|
-
const raw =
|
|
80
|
+
const raw = readFileSync(CREDENTIALS_PATH, "utf-8");
|
|
82
81
|
const data = JSON.parse(raw);
|
|
83
82
|
if (!data.token) return null;
|
|
84
83
|
return data;
|
|
@@ -212,12 +211,12 @@ async function initCommand(name) {
|
|
|
212
211
|
publish: "appfunnel publish"
|
|
213
212
|
},
|
|
214
213
|
dependencies: {
|
|
215
|
-
"@appfunnel-dev/sdk": "^0.
|
|
214
|
+
"@appfunnel-dev/sdk": "^0.6.0",
|
|
216
215
|
react: "^18.3.0",
|
|
217
216
|
"react-dom": "^18.3.0"
|
|
218
217
|
},
|
|
219
218
|
devDependencies: {
|
|
220
|
-
appfunnel: "^0.
|
|
219
|
+
appfunnel: "^0.6.0",
|
|
221
220
|
typescript: "^5.4.0",
|
|
222
221
|
"@types/react": "^18.2.0",
|
|
223
222
|
"@types/react-dom": "^18.2.0",
|
|
@@ -365,7 +364,7 @@ import { randomUUID } from "crypto";
|
|
|
365
364
|
import open from "open";
|
|
366
365
|
async function loginCommand() {
|
|
367
366
|
const state = randomUUID();
|
|
368
|
-
return new Promise((
|
|
367
|
+
return new Promise((resolve5, reject) => {
|
|
369
368
|
const server = createServer((req, res) => {
|
|
370
369
|
const url = new URL(req.url || "/", `http://localhost`);
|
|
371
370
|
if (url.pathname !== "/callback") {
|
|
@@ -404,7 +403,7 @@ async function loginCommand() {
|
|
|
404
403
|
spinner2.stop();
|
|
405
404
|
success(`Logged in as ${email || userId}`);
|
|
406
405
|
server.close();
|
|
407
|
-
|
|
406
|
+
resolve5();
|
|
408
407
|
});
|
|
409
408
|
server.listen(0, "127.0.0.1", () => {
|
|
410
409
|
const addr = server.address();
|
|
@@ -493,7 +492,7 @@ var init_whoami = __esm({
|
|
|
493
492
|
});
|
|
494
493
|
|
|
495
494
|
// src/lib/config.ts
|
|
496
|
-
import { existsSync as existsSync2, readFileSync as
|
|
495
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
497
496
|
import { join as join3, resolve } from "path";
|
|
498
497
|
async function loadConfig(cwd) {
|
|
499
498
|
const configPath = join3(cwd, CONFIG_FILE);
|
|
@@ -505,7 +504,7 @@ async function loadConfig(cwd) {
|
|
|
505
504
|
);
|
|
506
505
|
}
|
|
507
506
|
const { transform } = await import("esbuild");
|
|
508
|
-
const raw =
|
|
507
|
+
const raw = readFileSync2(configPath, "utf-8");
|
|
509
508
|
const result = await transform(raw, {
|
|
510
509
|
loader: "ts",
|
|
511
510
|
format: "esm",
|
|
@@ -537,10 +536,10 @@ var init_config = __esm({
|
|
|
537
536
|
});
|
|
538
537
|
|
|
539
538
|
// src/lib/version.ts
|
|
540
|
-
import { readFileSync as
|
|
539
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
541
540
|
import { join as join4 } from "path";
|
|
542
541
|
function checkVersionCompatibility(cwd) {
|
|
543
|
-
const cliVersion =
|
|
542
|
+
const cliVersion = "0.6.0";
|
|
544
543
|
const sdkVersion = getSdkVersion(cwd);
|
|
545
544
|
const [cliMajor, cliMinor] = cliVersion.split(".").map(Number);
|
|
546
545
|
const [sdkMajor, sdkMinor] = sdkVersion.split(".").map(Number);
|
|
@@ -552,19 +551,6 @@ function checkVersionCompatibility(cwd) {
|
|
|
552
551
|
);
|
|
553
552
|
}
|
|
554
553
|
}
|
|
555
|
-
function getCliVersion() {
|
|
556
|
-
try {
|
|
557
|
-
const pkg = JSON.parse(
|
|
558
|
-
readFileSync4(
|
|
559
|
-
new URL("../../package.json", import.meta.url),
|
|
560
|
-
"utf-8"
|
|
561
|
-
)
|
|
562
|
-
);
|
|
563
|
-
return pkg.version;
|
|
564
|
-
} catch {
|
|
565
|
-
return "0.0.0";
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
554
|
function getSdkVersion(cwd) {
|
|
569
555
|
try {
|
|
570
556
|
const pkgPath = join4(
|
|
@@ -574,7 +560,7 @@ function getSdkVersion(cwd) {
|
|
|
574
560
|
"sdk",
|
|
575
561
|
"package.json"
|
|
576
562
|
);
|
|
577
|
-
const pkg = JSON.parse(
|
|
563
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
578
564
|
return pkg.version;
|
|
579
565
|
} catch {
|
|
580
566
|
throw new CLIError(
|
|
@@ -592,7 +578,7 @@ var init_version = __esm({
|
|
|
592
578
|
});
|
|
593
579
|
|
|
594
580
|
// src/extract/pages.ts
|
|
595
|
-
import { readdirSync, readFileSync as
|
|
581
|
+
import { readdirSync, readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
|
|
596
582
|
import { join as join5, basename } from "path";
|
|
597
583
|
function scanPages(cwd) {
|
|
598
584
|
const pagesDir = resolvePagesDir(cwd);
|
|
@@ -619,7 +605,7 @@ async function extractPageDefinitions(cwd, pageKeys) {
|
|
|
619
605
|
const result = {};
|
|
620
606
|
for (const key of pageKeys) {
|
|
621
607
|
const filePath = join5(pagesDir, `${key}.tsx`);
|
|
622
|
-
const source =
|
|
608
|
+
const source = readFileSync4(filePath, "utf-8");
|
|
623
609
|
const definition = extractDefinePage(ts, source, filePath);
|
|
624
610
|
if (definition) {
|
|
625
611
|
result[key] = definition;
|
|
@@ -708,6 +694,7 @@ var init_pages = __esm({
|
|
|
708
694
|
|
|
709
695
|
// src/vite/entry.ts
|
|
710
696
|
import { join as join6 } from "path";
|
|
697
|
+
import { existsSync as existsSync4 } from "fs";
|
|
711
698
|
function generateEntrySource(options) {
|
|
712
699
|
const { config, pages, pagesDir, funnelTsxPath, isDev } = options;
|
|
713
700
|
const pageKeys = Object.keys(pages);
|
|
@@ -735,76 +722,197 @@ function generateEntrySource(options) {
|
|
|
735
722
|
for (const [key, def] of Object.entries(pages)) {
|
|
736
723
|
slugMap[def.slug || key] = key;
|
|
737
724
|
}
|
|
725
|
+
const priceDataCode = isDev ? options.priceData && options.priceData.size > 0 ? `const priceData = new Map(${JSON.stringify([...options.priceData.entries()])});` : `const priceData = undefined;` : `const priceData = (() => {
|
|
726
|
+
const rd = typeof window !== 'undefined' && window.__APPFUNNEL_DATA__;
|
|
727
|
+
if (!rd || !rd.products?.items) return undefined;
|
|
728
|
+
// Build price map from server-injected product data
|
|
729
|
+
const map = new Map();
|
|
730
|
+
for (const item of rd.products.items) {
|
|
731
|
+
if (item.priceData && item.storePriceId) map.set(item.storePriceId, item.priceData);
|
|
732
|
+
if (item.trialPriceData && item.trialStorePriceId) map.set(item.trialStorePriceId, item.trialPriceData);
|
|
733
|
+
}
|
|
734
|
+
return map.size > 0 ? map : undefined;
|
|
735
|
+
})();`;
|
|
738
736
|
const trackingCode = isDev ? `
|
|
739
737
|
// Dev mode: mock tracking \u2014 log events to console
|
|
740
|
-
const originalFetch = globalThis.fetch;
|
|
741
738
|
globalThis.__APPFUNNEL_DEV__ = true;
|
|
742
739
|
` : "";
|
|
740
|
+
const appCssPath = join6(pagesDir, "..", "app.css").replace(/\\/g, "/");
|
|
741
|
+
const hasAppCss = existsSync4(join6(pagesDir, "..", "app.css"));
|
|
743
742
|
return `
|
|
744
|
-
import { StrictMode, lazy, Suspense, useState, useEffect,
|
|
743
|
+
import { StrictMode, Component, lazy, Suspense, useState, useCallback, useEffect, useTransition, useDeferredValue, useSyncExternalStore } from 'react'
|
|
745
744
|
import { createRoot } from 'react-dom/client'
|
|
746
|
-
import { FunnelProvider } from '@appfunnel-dev/sdk
|
|
745
|
+
import { FunnelProvider, useNavigation } from '@appfunnel-dev/sdk'
|
|
746
|
+
${hasAppCss ? `import '${appCssPath}'` : ""}
|
|
747
747
|
import FunnelWrapper from '${funnelTsxPath.replace(/\\/g, "/")}'
|
|
748
748
|
|
|
749
749
|
${trackingCode}
|
|
750
750
|
|
|
751
|
-
const
|
|
751
|
+
const pageComponents = {
|
|
752
752
|
${pageImports}
|
|
753
753
|
}
|
|
754
754
|
|
|
755
|
+
${priceDataCode}
|
|
756
|
+
|
|
755
757
|
const config = ${JSON.stringify(fullConfig, null, 2)}
|
|
756
758
|
|
|
757
|
-
const slugToKey = ${JSON.stringify(slugMap)}
|
|
758
759
|
const keyToSlug = ${JSON.stringify(
|
|
759
760
|
Object.fromEntries(Object.entries(slugMap).map(([s, k]) => [k, s]))
|
|
760
761
|
)}
|
|
762
|
+
const slugToKey = ${JSON.stringify(slugMap)}
|
|
763
|
+
|
|
764
|
+
const DEV_CAMPAIGN_SLUG = 'campaign'
|
|
765
|
+
const DEFAULT_INITIAL = '${config.initialPageKey || pageKeys[0] || "index"}'
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Parse the URL to extract basePath, campaignSlug, and initial page.
|
|
769
|
+
*
|
|
770
|
+
* URL pattern: /f/<campaignSlug>[/<pageSlug>]
|
|
771
|
+
*
|
|
772
|
+
* In dev, redirects bare / to /f/<projectId> so the URL matches production.
|
|
773
|
+
*/
|
|
774
|
+
function parseUrl() {
|
|
775
|
+
const parts = window.location.pathname.split('/').filter(Boolean)
|
|
776
|
+
|
|
777
|
+
// /f/<campaignSlug>[/<pageSlug>]
|
|
778
|
+
if (parts[0] === 'f' && parts.length >= 2) {
|
|
779
|
+
const campaignSlug = parts[1]
|
|
780
|
+
const pageSlug = parts[2] || ''
|
|
781
|
+
const pageKey = pageSlug ? (slugToKey[pageSlug] || '') : ''
|
|
782
|
+
return {
|
|
783
|
+
basePath: '/f/' + campaignSlug,
|
|
784
|
+
campaignSlug,
|
|
785
|
+
initialPage: pageKey || DEFAULT_INITIAL,
|
|
786
|
+
}
|
|
787
|
+
}
|
|
761
788
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
789
|
+
// Bare URL \u2192 redirect to /f/<slug> in dev
|
|
790
|
+
${isDev ? `
|
|
791
|
+
window.history.replaceState(null, '', '/f/' + DEV_CAMPAIGN_SLUG)
|
|
792
|
+
return {
|
|
793
|
+
basePath: '/f/' + DEV_CAMPAIGN_SLUG,
|
|
794
|
+
campaignSlug: DEV_CAMPAIGN_SLUG,
|
|
795
|
+
initialPage: DEFAULT_INITIAL,
|
|
796
|
+
}` : `
|
|
797
|
+
return {
|
|
798
|
+
basePath: '',
|
|
799
|
+
campaignSlug: '',
|
|
800
|
+
initialPage: DEFAULT_INITIAL,
|
|
801
|
+
}`}
|
|
765
802
|
}
|
|
766
803
|
|
|
767
|
-
|
|
768
|
-
|
|
804
|
+
const { basePath, campaignSlug, initialPage } = parseUrl()
|
|
805
|
+
|
|
806
|
+
${isDev ? `
|
|
807
|
+
class ErrorBoundary extends Component {
|
|
808
|
+
constructor(props) {
|
|
809
|
+
super(props)
|
|
810
|
+
this.state = { error: null }
|
|
811
|
+
}
|
|
812
|
+
static getDerivedStateFromError(error) {
|
|
813
|
+
return { error }
|
|
814
|
+
}
|
|
815
|
+
componentDidCatch(error, info) {
|
|
816
|
+
console.error('[AppFunnel] Render error:', error, info)
|
|
817
|
+
}
|
|
818
|
+
render() {
|
|
819
|
+
if (this.state.error) {
|
|
820
|
+
return (
|
|
821
|
+
<div style={{ padding: '2rem', fontFamily: 'monospace' }}>
|
|
822
|
+
<h2 style={{ color: 'red' }}>AppFunnel Error</h2>
|
|
823
|
+
<pre style={{ whiteSpace: 'pre-wrap', color: '#333' }}>{this.state.error.message}</pre>
|
|
824
|
+
<pre style={{ whiteSpace: 'pre-wrap', color: '#666', fontSize: '12px' }}>{this.state.error.stack}</pre>
|
|
825
|
+
</div>
|
|
826
|
+
)
|
|
827
|
+
}
|
|
828
|
+
return this.props.children
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
` : ""}
|
|
769
832
|
|
|
833
|
+
/**
|
|
834
|
+
* PageRenderer lives inside FunnelProvider so it can use SDK hooks.
|
|
835
|
+
* Subscribes to the router \u2014 re-renders when the page changes.
|
|
836
|
+
* Uses useTransition to keep showing the current page while the next one loads.
|
|
837
|
+
*/
|
|
838
|
+
function PageRenderer() {
|
|
839
|
+
const { currentPage, goToPage } = useNavigation()
|
|
840
|
+
const routerPageKey = currentPage?.key || ''
|
|
841
|
+
|
|
842
|
+
// Track the displayed page separately so we can transition smoothly
|
|
843
|
+
const [displayedKey, setDisplayedKey] = useState(routerPageKey)
|
|
844
|
+
const [isPending, startTransition] = useTransition()
|
|
845
|
+
|
|
846
|
+
// When the router's page changes, transition to the new page
|
|
847
|
+
useEffect(() => {
|
|
848
|
+
if (routerPageKey && routerPageKey !== displayedKey) {
|
|
849
|
+
startTransition(() => {
|
|
850
|
+
setDisplayedKey(routerPageKey)
|
|
851
|
+
})
|
|
852
|
+
}
|
|
853
|
+
}, [routerPageKey, displayedKey])
|
|
854
|
+
|
|
855
|
+
// Sync URL with current page
|
|
856
|
+
const slug = currentPage?.slug || routerPageKey
|
|
857
|
+
useEffect(() => {
|
|
858
|
+
const expectedPath = basePath ? basePath + '/' + slug : '/' + slug
|
|
859
|
+
if (slug && window.location.pathname !== expectedPath) {
|
|
860
|
+
window.history.pushState(null, '', expectedPath)
|
|
861
|
+
}
|
|
862
|
+
}, [slug])
|
|
863
|
+
|
|
864
|
+
// Handle browser back/forward
|
|
770
865
|
useEffect(() => {
|
|
771
866
|
const handlePopState = () => {
|
|
772
867
|
const path = window.location.pathname.split('/').filter(Boolean).pop() || ''
|
|
773
|
-
const
|
|
774
|
-
if (
|
|
775
|
-
|
|
868
|
+
const key = slugToKey[path]
|
|
869
|
+
if (key && key !== routerPageKey) {
|
|
870
|
+
goToPage(key)
|
|
776
871
|
}
|
|
777
872
|
}
|
|
778
873
|
window.addEventListener('popstate', handlePopState)
|
|
779
874
|
return () => window.removeEventListener('popstate', handlePopState)
|
|
780
|
-
}, [
|
|
781
|
-
|
|
782
|
-
// Expose navigation to FunnelProvider's router
|
|
783
|
-
useEffect(() => {
|
|
784
|
-
window.__APPFUNNEL_NAVIGATE__ = (pageKey) => {
|
|
785
|
-
setCurrentPage(pageKey)
|
|
786
|
-
const slug = keyToSlug[pageKey] || pageKey
|
|
787
|
-
window.history.pushState(null, '', '/' + slug)
|
|
788
|
-
}
|
|
789
|
-
return () => { delete window.__APPFUNNEL_NAVIGATE__ }
|
|
790
|
-
}, [])
|
|
875
|
+
}, [routerPageKey, goToPage])
|
|
791
876
|
|
|
792
|
-
const PageComponent =
|
|
877
|
+
const PageComponent = pageComponents[displayedKey]
|
|
793
878
|
|
|
794
879
|
if (!PageComponent) {
|
|
795
|
-
return <div style={{ padding: '2rem', color: 'red' }}>Page not found: {
|
|
880
|
+
return <div style={{ padding: '2rem', color: 'red' }}>Page not found: {displayedKey}</div>
|
|
796
881
|
}
|
|
797
882
|
|
|
883
|
+
return (
|
|
884
|
+
<Suspense fallback={null}>
|
|
885
|
+
<PageComponent />
|
|
886
|
+
</Suspense>
|
|
887
|
+
)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Runtime data injected by the server (production only)
|
|
891
|
+
const __rd = typeof window !== 'undefined' && window.__APPFUNNEL_DATA__
|
|
892
|
+
|
|
893
|
+
function App() {
|
|
894
|
+
// In production, merge server-injected integrations into config
|
|
895
|
+
const runtimeConfig = __rd && __rd.integrations
|
|
896
|
+
? { ...config, integrations: { ...config.integrations, ...__rd.integrations } }
|
|
897
|
+
: config
|
|
898
|
+
|
|
899
|
+
const sessionData = __rd ? {
|
|
900
|
+
campaignId: __rd.campaignId || '',
|
|
901
|
+
funnelId: __rd.funnelId || config.funnelId || '',
|
|
902
|
+
experimentId: __rd.experimentId || null,
|
|
903
|
+
} : undefined
|
|
904
|
+
|
|
798
905
|
return (
|
|
799
906
|
<FunnelProvider
|
|
800
|
-
config={
|
|
801
|
-
initialPage={
|
|
802
|
-
|
|
907
|
+
config={runtimeConfig}
|
|
908
|
+
initialPage={initialPage}
|
|
909
|
+
basePath={basePath}
|
|
910
|
+
campaignSlug={campaignSlug}
|
|
911
|
+
priceData={priceData}
|
|
912
|
+
sessionData={sessionData}
|
|
803
913
|
>
|
|
804
914
|
<FunnelWrapper>
|
|
805
|
-
<
|
|
806
|
-
<PageComponent />
|
|
807
|
-
</Suspense>
|
|
915
|
+
<PageRenderer />
|
|
808
916
|
</FunnelWrapper>
|
|
809
917
|
</FunnelProvider>
|
|
810
918
|
)
|
|
@@ -812,9 +920,14 @@ function App() {
|
|
|
812
920
|
|
|
813
921
|
createRoot(document.getElementById('root')).render(
|
|
814
922
|
<StrictMode>
|
|
815
|
-
<
|
|
923
|
+
${isDev ? "<ErrorBoundary>" : ""}
|
|
924
|
+
<App />
|
|
925
|
+
${isDev ? "</ErrorBoundary>" : ""}
|
|
816
926
|
</StrictMode>
|
|
817
927
|
)
|
|
928
|
+
|
|
929
|
+
// Reveal body (the host page may set opacity:0 for a loading transition)
|
|
930
|
+
document.body.style.opacity = '1'
|
|
818
931
|
`;
|
|
819
932
|
}
|
|
820
933
|
var init_entry = __esm({
|
|
@@ -846,54 +959,66 @@ var init_html = __esm({
|
|
|
846
959
|
|
|
847
960
|
// src/vite/plugin.ts
|
|
848
961
|
import { resolve as resolve2, join as join7 } from "path";
|
|
849
|
-
import { existsSync as
|
|
962
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
850
963
|
function appfunnelPlugin(options) {
|
|
851
964
|
const { cwd, config, isDev } = options;
|
|
852
965
|
let pages = options.pages;
|
|
853
966
|
const pagesDir = resolve2(cwd, "src", "pages");
|
|
854
967
|
const funnelTsxPath = resolve2(cwd, "src", "funnel.tsx");
|
|
855
|
-
|
|
968
|
+
const appfunnelDir = join7(cwd, APPFUNNEL_DIR);
|
|
969
|
+
const htmlPath = join7(appfunnelDir, "index.html");
|
|
856
970
|
function getEntrySource() {
|
|
857
971
|
return generateEntrySource({
|
|
858
972
|
config,
|
|
859
973
|
pages,
|
|
860
974
|
pagesDir,
|
|
861
975
|
funnelTsxPath,
|
|
862
|
-
isDev
|
|
976
|
+
isDev,
|
|
977
|
+
priceData: options.priceData
|
|
863
978
|
});
|
|
864
979
|
}
|
|
865
980
|
return {
|
|
866
981
|
name: "appfunnel",
|
|
867
982
|
config() {
|
|
983
|
+
mkdirSync3(appfunnelDir, { recursive: true });
|
|
984
|
+
writeFileSync3(htmlPath, generateHtml(config.name || "AppFunnel"));
|
|
868
985
|
return {
|
|
986
|
+
// Don't let Vite auto-serve index.html — we handle it ourselves
|
|
987
|
+
appType: "custom",
|
|
869
988
|
resolve: {
|
|
870
989
|
alias: {
|
|
871
990
|
"@": resolve2(cwd, "src")
|
|
872
991
|
}
|
|
873
992
|
},
|
|
874
|
-
// Ensure we can import .tsx files
|
|
875
993
|
esbuild: {
|
|
876
994
|
jsx: "automatic"
|
|
877
995
|
},
|
|
878
996
|
optimizeDeps: {
|
|
879
|
-
include: ["react", "react-dom", "react/jsx-runtime"]
|
|
997
|
+
include: ["react", "react-dom", "react/jsx-runtime"],
|
|
998
|
+
force: true
|
|
880
999
|
}
|
|
881
1000
|
};
|
|
882
1001
|
},
|
|
883
1002
|
resolveId(id) {
|
|
884
|
-
if (id === VIRTUAL_ENTRY_ID) {
|
|
1003
|
+
if (id === VIRTUAL_ENTRY_ID || id === "/" + VIRTUAL_ENTRY_ID) {
|
|
885
1004
|
return RESOLVED_VIRTUAL_ENTRY_ID;
|
|
886
1005
|
}
|
|
887
1006
|
return null;
|
|
888
1007
|
},
|
|
889
|
-
load(id) {
|
|
1008
|
+
async load(id) {
|
|
890
1009
|
if (id === RESOLVED_VIRTUAL_ENTRY_ID) {
|
|
891
|
-
|
|
1010
|
+
const { transform } = await import("esbuild");
|
|
1011
|
+
const source = getEntrySource();
|
|
1012
|
+
const result = await transform(source, {
|
|
1013
|
+
loader: "tsx",
|
|
1014
|
+
jsx: "automatic",
|
|
1015
|
+
sourcefile: "appfunnel-entry.tsx"
|
|
1016
|
+
});
|
|
1017
|
+
return result.code;
|
|
892
1018
|
}
|
|
893
1019
|
return null;
|
|
894
1020
|
},
|
|
895
1021
|
configureServer(devServer) {
|
|
896
|
-
server = devServer;
|
|
897
1022
|
const watcher = devServer.watcher;
|
|
898
1023
|
watcher.add(pagesDir);
|
|
899
1024
|
const handlePagesChange = async () => {
|
|
@@ -917,7 +1042,7 @@ function appfunnelPlugin(options) {
|
|
|
917
1042
|
}
|
|
918
1043
|
});
|
|
919
1044
|
const configPath = join7(cwd, "appfunnel.config.ts");
|
|
920
|
-
if (
|
|
1045
|
+
if (existsSync5(configPath)) {
|
|
921
1046
|
watcher.add(configPath);
|
|
922
1047
|
watcher.on("change", (file) => {
|
|
923
1048
|
if (file === configPath) {
|
|
@@ -926,33 +1051,129 @@ function appfunnelPlugin(options) {
|
|
|
926
1051
|
});
|
|
927
1052
|
}
|
|
928
1053
|
return () => {
|
|
929
|
-
devServer.middlewares.use((req, res, next) => {
|
|
930
|
-
|
|
1054
|
+
devServer.middlewares.use(async (req, res, next) => {
|
|
1055
|
+
const url = req.url?.split("?")[0] || "";
|
|
1056
|
+
if (url.includes(".") || url.startsWith("/@") || url.startsWith("/node_modules")) {
|
|
931
1057
|
return next();
|
|
932
1058
|
}
|
|
933
|
-
|
|
934
|
-
|
|
1059
|
+
try {
|
|
1060
|
+
const rawHtml = readFileSync5(htmlPath, "utf-8");
|
|
1061
|
+
const html = await devServer.transformIndexHtml(req.url || "/", rawHtml);
|
|
935
1062
|
res.statusCode = 200;
|
|
936
1063
|
res.setHeader("Content-Type", "text/html");
|
|
937
|
-
res.end(
|
|
938
|
-
}
|
|
1064
|
+
res.end(html);
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
next(err);
|
|
1067
|
+
}
|
|
939
1068
|
});
|
|
940
1069
|
};
|
|
941
1070
|
},
|
|
942
|
-
// For production build: inject the HTML as the input
|
|
943
1071
|
transformIndexHtml(html) {
|
|
944
1072
|
return html;
|
|
945
1073
|
}
|
|
946
1074
|
};
|
|
947
1075
|
}
|
|
948
|
-
var VIRTUAL_ENTRY_ID, RESOLVED_VIRTUAL_ENTRY_ID;
|
|
1076
|
+
var VIRTUAL_ENTRY_ID, RESOLVED_VIRTUAL_ENTRY_ID, APPFUNNEL_DIR;
|
|
949
1077
|
var init_plugin = __esm({
|
|
950
1078
|
"src/vite/plugin.ts"() {
|
|
951
1079
|
"use strict";
|
|
952
1080
|
init_entry();
|
|
953
1081
|
init_html();
|
|
954
1082
|
VIRTUAL_ENTRY_ID = "@appfunnel/entry";
|
|
955
|
-
RESOLVED_VIRTUAL_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID;
|
|
1083
|
+
RESOLVED_VIRTUAL_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID + ".tsx";
|
|
1084
|
+
APPFUNNEL_DIR = ".appfunnel";
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
|
|
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";
|
|
956
1177
|
}
|
|
957
1178
|
});
|
|
958
1179
|
|
|
@@ -961,7 +1182,7 @@ var dev_exports = {};
|
|
|
961
1182
|
__export(dev_exports, {
|
|
962
1183
|
devCommand: () => devCommand
|
|
963
1184
|
});
|
|
964
|
-
import { readFileSync as readFileSync6, writeFileSync as
|
|
1185
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
965
1186
|
import { join as join8 } from "path";
|
|
966
1187
|
import pc6 from "picocolors";
|
|
967
1188
|
async function devCommand(options) {
|
|
@@ -978,12 +1199,21 @@ async function devCommand(options) {
|
|
|
978
1199
|
config.projectId = projectId;
|
|
979
1200
|
const configPath = join8(cwd, "appfunnel.config.ts");
|
|
980
1201
|
const configSource = readFileSync6(configPath, "utf-8");
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1202
|
+
let updated;
|
|
1203
|
+
if (/projectId:\s*['"]/.test(configSource)) {
|
|
1204
|
+
updated = configSource.replace(
|
|
1205
|
+
/projectId:\s*['"].*?['"]/,
|
|
1206
|
+
`projectId: '${projectId}'`
|
|
1207
|
+
);
|
|
1208
|
+
} else {
|
|
1209
|
+
updated = configSource.replace(
|
|
1210
|
+
/(defineConfig\(\{[\t ]*\n)/,
|
|
1211
|
+
`$1 projectId: '${projectId}',
|
|
1212
|
+
`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
985
1215
|
if (updated !== configSource) {
|
|
986
|
-
|
|
1216
|
+
writeFileSync4(configPath, updated);
|
|
987
1217
|
success(`Updated projectId in appfunnel.config.ts`);
|
|
988
1218
|
} else {
|
|
989
1219
|
warn(`Could not auto-update appfunnel.config.ts \u2014 add projectId: '${projectId}' manually.`);
|
|
@@ -994,8 +1224,43 @@ async function devCommand(options) {
|
|
|
994
1224
|
let pages = await extractPageDefinitions(cwd, pageKeys);
|
|
995
1225
|
s2.stop();
|
|
996
1226
|
info(`Found ${pageKeys.length} pages: ${pageKeys.join(", ")}`);
|
|
1227
|
+
let priceData = /* @__PURE__ */ new Map();
|
|
1228
|
+
if (config.projectId && config.products?.items?.length) {
|
|
1229
|
+
try {
|
|
1230
|
+
const storePriceIds = [
|
|
1231
|
+
...new Set(
|
|
1232
|
+
config.products.items.flatMap(
|
|
1233
|
+
(item) => [item.storePriceId, item.trialStorePriceId].filter(Boolean)
|
|
1234
|
+
)
|
|
1235
|
+
)
|
|
1236
|
+
];
|
|
1237
|
+
info(`Fetching ${storePriceIds.length} store prices: ${storePriceIds.join(", ")}`);
|
|
1238
|
+
const s3 = spinner("Fetching store prices...");
|
|
1239
|
+
priceData = await fetchPrices(config.projectId, storePriceIds, { token: creds.token });
|
|
1240
|
+
s3.stop();
|
|
1241
|
+
const missingIds = storePriceIds.filter((id) => !priceData.has(id));
|
|
1242
|
+
if (missingIds.length > 0) {
|
|
1243
|
+
error(`Missing store prices: ${missingIds.join(", ")}`);
|
|
1244
|
+
error("Make sure these storePriceId values in your config match prices in your project.");
|
|
1245
|
+
process.exit(1);
|
|
1246
|
+
}
|
|
1247
|
+
success(`Fetched ${priceData.size}/${storePriceIds.length} store prices`);
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
error(`Failed to fetch store prices: ${err.message}`);
|
|
1250
|
+
process.exit(1);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
997
1253
|
const { createServer: createServer2 } = await import("vite");
|
|
998
1254
|
const react = await import("@vitejs/plugin-react");
|
|
1255
|
+
let tailwindPlugin = null;
|
|
1256
|
+
try {
|
|
1257
|
+
const { createRequire } = await import("module");
|
|
1258
|
+
const require2 = createRequire(join8(cwd, "package.json"));
|
|
1259
|
+
const tailwindPath = require2.resolve("@tailwindcss/vite");
|
|
1260
|
+
const tailwindVite = await import(tailwindPath);
|
|
1261
|
+
tailwindPlugin = tailwindVite.default;
|
|
1262
|
+
} catch {
|
|
1263
|
+
}
|
|
999
1264
|
const server = await createServer2({
|
|
1000
1265
|
root: cwd,
|
|
1001
1266
|
server: {
|
|
@@ -1004,11 +1269,13 @@ async function devCommand(options) {
|
|
|
1004
1269
|
},
|
|
1005
1270
|
plugins: [
|
|
1006
1271
|
react.default(),
|
|
1272
|
+
...tailwindPlugin ? [tailwindPlugin()] : [],
|
|
1007
1273
|
appfunnelPlugin({
|
|
1008
1274
|
cwd,
|
|
1009
1275
|
config,
|
|
1010
1276
|
pages,
|
|
1011
1277
|
isDev: true,
|
|
1278
|
+
priceData,
|
|
1012
1279
|
async onPagesChange() {
|
|
1013
1280
|
pageKeys = scanPages(cwd);
|
|
1014
1281
|
pages = await extractPageDefinitions(cwd, pageKeys);
|
|
@@ -1042,6 +1309,7 @@ var init_dev = __esm({
|
|
|
1042
1309
|
init_pages();
|
|
1043
1310
|
init_plugin();
|
|
1044
1311
|
init_projects();
|
|
1312
|
+
init_api();
|
|
1045
1313
|
}
|
|
1046
1314
|
});
|
|
1047
1315
|
|
|
@@ -1050,8 +1318,9 @@ var build_exports = {};
|
|
|
1050
1318
|
__export(build_exports, {
|
|
1051
1319
|
buildCommand: () => buildCommand
|
|
1052
1320
|
});
|
|
1053
|
-
import { resolve as
|
|
1054
|
-
import {
|
|
1321
|
+
import { resolve as resolve3, join as join9 } from "path";
|
|
1322
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1323
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, statSync, readdirSync as readdirSync2 } from "fs";
|
|
1055
1324
|
import pc7 from "picocolors";
|
|
1056
1325
|
async function buildCommand() {
|
|
1057
1326
|
const cwd = process.cwd();
|
|
@@ -1072,15 +1341,25 @@ async function buildCommand() {
|
|
|
1072
1341
|
s.stop();
|
|
1073
1342
|
validateRoutes(config, pages, pageKeys);
|
|
1074
1343
|
info(`Building ${pageKeys.length} pages...`);
|
|
1075
|
-
const outDir =
|
|
1344
|
+
const outDir = resolve3(cwd, "dist");
|
|
1076
1345
|
const { build } = await import("vite");
|
|
1077
1346
|
const react = await import("@vitejs/plugin-react");
|
|
1078
|
-
|
|
1347
|
+
let tailwindPlugin = null;
|
|
1348
|
+
try {
|
|
1349
|
+
const { createRequire } = await import("module");
|
|
1350
|
+
const require2 = createRequire(join9(cwd, "package.json"));
|
|
1351
|
+
const tailwindPath = require2.resolve("@tailwindcss/vite");
|
|
1352
|
+
const tailwindVite = await import(tailwindPath);
|
|
1353
|
+
tailwindPlugin = tailwindVite.default;
|
|
1354
|
+
} catch {
|
|
1355
|
+
}
|
|
1356
|
+
const htmlPath = resolve3(cwd, "index.html");
|
|
1079
1357
|
const htmlContent = generateHtml(config.name || "AppFunnel");
|
|
1080
|
-
|
|
1358
|
+
writeFileSync5(htmlPath, htmlContent);
|
|
1081
1359
|
try {
|
|
1082
1360
|
await build({
|
|
1083
1361
|
root: cwd,
|
|
1362
|
+
base: "./",
|
|
1084
1363
|
build: {
|
|
1085
1364
|
outDir,
|
|
1086
1365
|
emptyOutDir: true,
|
|
@@ -1101,6 +1380,7 @@ async function buildCommand() {
|
|
|
1101
1380
|
},
|
|
1102
1381
|
plugins: [
|
|
1103
1382
|
react.default(),
|
|
1383
|
+
...tailwindPlugin ? [tailwindPlugin()] : [],
|
|
1104
1384
|
appfunnelPlugin({
|
|
1105
1385
|
cwd,
|
|
1106
1386
|
config,
|
|
@@ -1136,9 +1416,12 @@ async function buildCommand() {
|
|
|
1136
1416
|
const totalSize = assets.reduce((sum, a) => sum + a.size, 0);
|
|
1137
1417
|
const manifest = {
|
|
1138
1418
|
version: 1,
|
|
1419
|
+
buildHash: randomUUID2(),
|
|
1139
1420
|
sdkVersion: getSdkVersion2(cwd),
|
|
1140
1421
|
projectId: config.projectId,
|
|
1141
1422
|
funnelId: config.funnelId,
|
|
1423
|
+
name: config.name,
|
|
1424
|
+
initialPageKey: config.initialPageKey,
|
|
1142
1425
|
pages: { ...config.pages, ...mergedPages },
|
|
1143
1426
|
routes: { ...config.routes, ...mergedRoutes },
|
|
1144
1427
|
responses: config.responses || {},
|
|
@@ -1148,11 +1431,11 @@ async function buildCommand() {
|
|
|
1148
1431
|
assets,
|
|
1149
1432
|
totalSize
|
|
1150
1433
|
};
|
|
1151
|
-
|
|
1434
|
+
writeFileSync5(join9(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
1152
1435
|
console.log();
|
|
1153
1436
|
success("Build complete");
|
|
1154
1437
|
console.log();
|
|
1155
|
-
console.log(` ${pc7.dim("Output:")}
|
|
1438
|
+
console.log(` ${pc7.dim("Output:")} dist/`);
|
|
1156
1439
|
console.log(` ${pc7.dim("Pages:")} ${pageKeys.length}`);
|
|
1157
1440
|
console.log(` ${pc7.dim("Size:")} ${formatSize(totalSize)}`);
|
|
1158
1441
|
console.log();
|
|
@@ -1299,83 +1582,25 @@ var init_build = __esm({
|
|
|
1299
1582
|
}
|
|
1300
1583
|
});
|
|
1301
1584
|
|
|
1302
|
-
// src/lib/
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
...fetchOpts,
|
|
1317
|
-
headers
|
|
1318
|
-
});
|
|
1319
|
-
if (!response.ok) {
|
|
1320
|
-
const body = await response.text().catch(() => "");
|
|
1321
|
-
let message = `API request failed: ${response.status} ${response.statusText}`;
|
|
1322
|
-
try {
|
|
1323
|
-
const parsed = JSON.parse(body);
|
|
1324
|
-
if (parsed.error) message = parsed.error;
|
|
1325
|
-
if (parsed.message) message = parsed.message;
|
|
1326
|
-
} catch {
|
|
1327
|
-
}
|
|
1328
|
-
const error2 = new CLIError("API_ERROR", message);
|
|
1329
|
-
error2.statusCode = response.status;
|
|
1330
|
-
throw error2;
|
|
1331
|
-
}
|
|
1332
|
-
return response;
|
|
1333
|
-
}
|
|
1334
|
-
async function publishBuild(projectId, funnelId, manifest, assets, options) {
|
|
1335
|
-
const formData = new FormData();
|
|
1336
|
-
formData.set("manifest", JSON.stringify(manifest));
|
|
1337
|
-
formData.set("funnelId", funnelId);
|
|
1338
|
-
for (const asset of assets) {
|
|
1339
|
-
formData.append(
|
|
1340
|
-
"assets",
|
|
1341
|
-
new Blob([new Uint8Array(asset.content)], { type: asset.contentType }),
|
|
1342
|
-
asset.path
|
|
1343
|
-
);
|
|
1344
|
-
}
|
|
1345
|
-
try {
|
|
1346
|
-
const response = await apiFetch(`/project/${projectId}/headless/publish`, {
|
|
1347
|
-
...options,
|
|
1348
|
-
method: "POST",
|
|
1349
|
-
body: formData
|
|
1350
|
-
});
|
|
1351
|
-
return await response.json();
|
|
1352
|
-
} catch (err) {
|
|
1353
|
-
if (err instanceof CLIError && err.code === "API_ERROR") {
|
|
1354
|
-
if (err.statusCode === 413) {
|
|
1355
|
-
throw new CLIError(
|
|
1356
|
-
"BUNDLE_TOO_LARGE",
|
|
1357
|
-
err.message,
|
|
1358
|
-
"Reduce page bundle sizes. Check for large dependencies."
|
|
1359
|
-
);
|
|
1360
|
-
}
|
|
1361
|
-
if (err.statusCode === 409) {
|
|
1362
|
-
throw new CLIError(
|
|
1363
|
-
"FUNNEL_NOT_HEADLESS",
|
|
1364
|
-
err.message,
|
|
1365
|
-
"Create a new headless funnel from the dashboard, or remove funnelId from config."
|
|
1366
|
-
);
|
|
1367
|
-
}
|
|
1368
|
-
throw new CLIError("PUBLISH_FAILED", err.message);
|
|
1369
|
-
}
|
|
1370
|
-
throw err;
|
|
1585
|
+
// src/lib/config-patch.ts
|
|
1586
|
+
import { join as join10 } from "path";
|
|
1587
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
1588
|
+
function patchConfigFunnelId(cwd, funnelId) {
|
|
1589
|
+
const configPath = join10(cwd, "appfunnel.config.ts");
|
|
1590
|
+
let content = readFileSync8(configPath, "utf-8");
|
|
1591
|
+
if (content.includes("funnelId")) return;
|
|
1592
|
+
const patched = content.replace(
|
|
1593
|
+
/(projectId:\s*['"][^'"]+['"],?\s*\n)/,
|
|
1594
|
+
`$1 funnelId: '${funnelId}',
|
|
1595
|
+
`
|
|
1596
|
+
);
|
|
1597
|
+
if (patched !== content) {
|
|
1598
|
+
writeFileSync6(configPath, patched, "utf-8");
|
|
1371
1599
|
}
|
|
1372
1600
|
}
|
|
1373
|
-
var
|
|
1374
|
-
|
|
1375
|
-
"src/lib/api.ts"() {
|
|
1601
|
+
var init_config_patch = __esm({
|
|
1602
|
+
"src/lib/config-patch.ts"() {
|
|
1376
1603
|
"use strict";
|
|
1377
|
-
init_errors();
|
|
1378
|
-
DEFAULT_API_BASE3 = "https://api.appfunnel.net";
|
|
1379
1604
|
}
|
|
1380
1605
|
});
|
|
1381
1606
|
|
|
@@ -1384,34 +1609,49 @@ var publish_exports = {};
|
|
|
1384
1609
|
__export(publish_exports, {
|
|
1385
1610
|
publishCommand: () => publishCommand
|
|
1386
1611
|
});
|
|
1387
|
-
import { resolve as
|
|
1388
|
-
import { readFileSync as
|
|
1612
|
+
import { resolve as resolve4, join as join11 } from "path";
|
|
1613
|
+
import { readFileSync as readFileSync9, existsSync as existsSync6 } from "fs";
|
|
1389
1614
|
import pc8 from "picocolors";
|
|
1390
1615
|
function getMimeType(path) {
|
|
1391
1616
|
const ext = path.substring(path.lastIndexOf("."));
|
|
1392
1617
|
return MIME_TYPES[ext] || "application/octet-stream";
|
|
1393
1618
|
}
|
|
1619
|
+
function formatSize2(bytes) {
|
|
1620
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1621
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1622
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
1623
|
+
}
|
|
1394
1624
|
async function publishCommand() {
|
|
1395
1625
|
const cwd = process.cwd();
|
|
1396
1626
|
const creds = requireAuth();
|
|
1397
1627
|
checkVersionCompatibility(cwd);
|
|
1398
1628
|
const config = await loadConfig(cwd);
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1629
|
+
const projectId = config.projectId;
|
|
1630
|
+
if (!projectId) {
|
|
1631
|
+
throw new CLIError(
|
|
1632
|
+
"CONFIG_NOT_FOUND",
|
|
1633
|
+
"No projectId in appfunnel.config.ts.",
|
|
1634
|
+
"Add projectId to your config. You can find it in the dashboard."
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
const outDir = resolve4(cwd, "dist");
|
|
1638
|
+
const manifestPath = join11(outDir, "manifest.json");
|
|
1639
|
+
if (!existsSync6(manifestPath)) {
|
|
1402
1640
|
throw new CLIError(
|
|
1403
1641
|
"BUILD_NOT_FOUND",
|
|
1404
1642
|
"No build output found.",
|
|
1405
1643
|
"Run 'appfunnel build' first."
|
|
1406
1644
|
);
|
|
1407
1645
|
}
|
|
1408
|
-
const manifest = JSON.parse(
|
|
1646
|
+
const manifest = JSON.parse(readFileSync9(manifestPath, "utf-8"));
|
|
1409
1647
|
const assets = manifest.assets || [];
|
|
1410
|
-
const s = spinner("
|
|
1648
|
+
const s = spinner("Preparing assets...");
|
|
1411
1649
|
const assetPayloads = [];
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1650
|
+
let totalBytes = 0;
|
|
1651
|
+
for (let i = 0; i < assets.length; i++) {
|
|
1652
|
+
const asset = assets[i];
|
|
1653
|
+
const fullPath = join11(outDir, asset.path);
|
|
1654
|
+
if (!existsSync6(fullPath)) {
|
|
1415
1655
|
s.stop();
|
|
1416
1656
|
throw new CLIError(
|
|
1417
1657
|
"BUILD_NOT_FOUND",
|
|
@@ -1419,44 +1659,37 @@ async function publishCommand() {
|
|
|
1419
1659
|
"Run 'appfunnel build' to regenerate."
|
|
1420
1660
|
);
|
|
1421
1661
|
}
|
|
1662
|
+
const content = readFileSync9(fullPath);
|
|
1663
|
+
totalBytes += content.length;
|
|
1422
1664
|
assetPayloads.push({
|
|
1423
1665
|
path: asset.path,
|
|
1424
|
-
content
|
|
1666
|
+
content,
|
|
1425
1667
|
contentType: getMimeType(asset.path)
|
|
1426
1668
|
});
|
|
1669
|
+
s.text = `Preparing assets... ${i + 1}/${assets.length} ${pc8.dim(`(${formatSize2(totalBytes)})`)}`;
|
|
1427
1670
|
}
|
|
1428
|
-
|
|
1429
|
-
const funnelId = config.funnelId;
|
|
1430
|
-
if (!projectId) {
|
|
1431
|
-
s.stop();
|
|
1432
|
-
throw new CLIError(
|
|
1433
|
-
"CONFIG_NOT_FOUND",
|
|
1434
|
-
"No projectId in appfunnel.config.ts.",
|
|
1435
|
-
"Add projectId to your config. You can find it in the dashboard."
|
|
1436
|
-
);
|
|
1437
|
-
}
|
|
1438
|
-
if (!funnelId) {
|
|
1439
|
-
s.stop();
|
|
1440
|
-
throw new CLIError(
|
|
1441
|
-
"CONFIG_NOT_FOUND",
|
|
1442
|
-
"No funnelId in appfunnel.config.ts.",
|
|
1443
|
-
"Add funnelId to your config, or create a new funnel from the dashboard."
|
|
1444
|
-
);
|
|
1445
|
-
}
|
|
1671
|
+
s.text = `Uploading ${assets.length} assets ${pc8.dim(`(${formatSize2(totalBytes)})`)}`;
|
|
1446
1672
|
const result = await publishBuild(
|
|
1447
1673
|
projectId,
|
|
1448
|
-
funnelId,
|
|
1674
|
+
config.funnelId || "",
|
|
1449
1675
|
manifest,
|
|
1450
1676
|
assetPayloads,
|
|
1451
1677
|
{ token: creds.token }
|
|
1452
1678
|
);
|
|
1453
1679
|
s.stop();
|
|
1680
|
+
if (result.created && result.funnelId) {
|
|
1681
|
+
patchConfigFunnelId(cwd, result.funnelId);
|
|
1682
|
+
info(`Funnel created \u2014 funnelId added to appfunnel.config.ts`);
|
|
1683
|
+
}
|
|
1454
1684
|
console.log();
|
|
1455
1685
|
success("Published successfully");
|
|
1456
1686
|
console.log();
|
|
1457
1687
|
console.log(` ${pc8.dim("Build ID:")} ${result.buildId}`);
|
|
1688
|
+
if (result.funnelId && !config.funnelId) {
|
|
1689
|
+
console.log(` ${pc8.dim("Funnel:")} ${result.funnelId}`);
|
|
1690
|
+
}
|
|
1458
1691
|
console.log(` ${pc8.dim("URL:")} ${pc8.cyan(result.url)}`);
|
|
1459
|
-
console.log(` ${pc8.dim("Assets:")} ${assets.length} files`);
|
|
1692
|
+
console.log(` ${pc8.dim("Assets:")} ${assets.length} files ${pc8.dim(`(${formatSize2(totalBytes)})`)}`);
|
|
1460
1693
|
console.log();
|
|
1461
1694
|
}
|
|
1462
1695
|
var MIME_TYPES;
|
|
@@ -1469,6 +1702,7 @@ var init_publish = __esm({
|
|
|
1469
1702
|
init_version();
|
|
1470
1703
|
init_api();
|
|
1471
1704
|
init_errors();
|
|
1705
|
+
init_config_patch();
|
|
1472
1706
|
MIME_TYPES = {
|
|
1473
1707
|
".js": "application/javascript",
|
|
1474
1708
|
".css": "text/css",
|
|
@@ -1485,21 +1719,10 @@ var init_publish = __esm({
|
|
|
1485
1719
|
|
|
1486
1720
|
// src/index.ts
|
|
1487
1721
|
init_errors();
|
|
1488
|
-
import { readFileSync as readFileSync9 } from "fs";
|
|
1489
1722
|
import { Command } from "commander";
|
|
1490
1723
|
import pc9 from "picocolors";
|
|
1491
|
-
function getCliVersion2() {
|
|
1492
|
-
try {
|
|
1493
|
-
const pkg = JSON.parse(
|
|
1494
|
-
readFileSync9(new URL("../package.json", import.meta.url), "utf-8")
|
|
1495
|
-
);
|
|
1496
|
-
return pkg.version;
|
|
1497
|
-
} catch {
|
|
1498
|
-
return "0.0.0";
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
1724
|
var program = new Command();
|
|
1502
|
-
program.name("appfunnel").description("Build and publish headless AppFunnel projects").version(
|
|
1725
|
+
program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.6.0");
|
|
1503
1726
|
program.command("init <name>").description("Create a new AppFunnel project").action(async (name) => {
|
|
1504
1727
|
const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1505
1728
|
await initCommand2(name);
|