appfunnel 0.5.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 +426 -178
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -211,12 +211,12 @@ async function initCommand(name) {
|
|
|
211
211
|
publish: "appfunnel publish"
|
|
212
212
|
},
|
|
213
213
|
dependencies: {
|
|
214
|
-
"@appfunnel-dev/sdk": "^0.
|
|
214
|
+
"@appfunnel-dev/sdk": "^0.6.0",
|
|
215
215
|
react: "^18.3.0",
|
|
216
216
|
"react-dom": "^18.3.0"
|
|
217
217
|
},
|
|
218
218
|
devDependencies: {
|
|
219
|
-
appfunnel: "^0.
|
|
219
|
+
appfunnel: "^0.6.0",
|
|
220
220
|
typescript: "^5.4.0",
|
|
221
221
|
"@types/react": "^18.2.0",
|
|
222
222
|
"@types/react-dom": "^18.2.0",
|
|
@@ -364,7 +364,7 @@ import { randomUUID } from "crypto";
|
|
|
364
364
|
import open from "open";
|
|
365
365
|
async function loginCommand() {
|
|
366
366
|
const state = randomUUID();
|
|
367
|
-
return new Promise((
|
|
367
|
+
return new Promise((resolve5, reject) => {
|
|
368
368
|
const server = createServer((req, res) => {
|
|
369
369
|
const url = new URL(req.url || "/", `http://localhost`);
|
|
370
370
|
if (url.pathname !== "/callback") {
|
|
@@ -403,7 +403,7 @@ async function loginCommand() {
|
|
|
403
403
|
spinner2.stop();
|
|
404
404
|
success(`Logged in as ${email || userId}`);
|
|
405
405
|
server.close();
|
|
406
|
-
|
|
406
|
+
resolve5();
|
|
407
407
|
});
|
|
408
408
|
server.listen(0, "127.0.0.1", () => {
|
|
409
409
|
const addr = server.address();
|
|
@@ -539,7 +539,7 @@ var init_config = __esm({
|
|
|
539
539
|
import { readFileSync as readFileSync3 } from "fs";
|
|
540
540
|
import { join as join4 } from "path";
|
|
541
541
|
function checkVersionCompatibility(cwd) {
|
|
542
|
-
const cliVersion = "0.
|
|
542
|
+
const cliVersion = "0.6.0";
|
|
543
543
|
const sdkVersion = getSdkVersion(cwd);
|
|
544
544
|
const [cliMajor, cliMinor] = cliVersion.split(".").map(Number);
|
|
545
545
|
const [sdkMajor, sdkMinor] = sdkVersion.split(".").map(Number);
|
|
@@ -694,6 +694,7 @@ var init_pages = __esm({
|
|
|
694
694
|
|
|
695
695
|
// src/vite/entry.ts
|
|
696
696
|
import { join as join6 } from "path";
|
|
697
|
+
import { existsSync as existsSync4 } from "fs";
|
|
697
698
|
function generateEntrySource(options) {
|
|
698
699
|
const { config, pages, pagesDir, funnelTsxPath, isDev } = options;
|
|
699
700
|
const pageKeys = Object.keys(pages);
|
|
@@ -721,76 +722,197 @@ function generateEntrySource(options) {
|
|
|
721
722
|
for (const [key, def] of Object.entries(pages)) {
|
|
722
723
|
slugMap[def.slug || key] = key;
|
|
723
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
|
+
})();`;
|
|
724
736
|
const trackingCode = isDev ? `
|
|
725
737
|
// Dev mode: mock tracking \u2014 log events to console
|
|
726
|
-
const originalFetch = globalThis.fetch;
|
|
727
738
|
globalThis.__APPFUNNEL_DEV__ = true;
|
|
728
739
|
` : "";
|
|
740
|
+
const appCssPath = join6(pagesDir, "..", "app.css").replace(/\\/g, "/");
|
|
741
|
+
const hasAppCss = existsSync4(join6(pagesDir, "..", "app.css"));
|
|
729
742
|
return `
|
|
730
|
-
import { StrictMode, lazy, Suspense, useState, useEffect,
|
|
743
|
+
import { StrictMode, Component, lazy, Suspense, useState, useCallback, useEffect, useTransition, useDeferredValue, useSyncExternalStore } from 'react'
|
|
731
744
|
import { createRoot } from 'react-dom/client'
|
|
732
|
-
import { FunnelProvider } from '@appfunnel-dev/sdk
|
|
745
|
+
import { FunnelProvider, useNavigation } from '@appfunnel-dev/sdk'
|
|
746
|
+
${hasAppCss ? `import '${appCssPath}'` : ""}
|
|
733
747
|
import FunnelWrapper from '${funnelTsxPath.replace(/\\/g, "/")}'
|
|
734
748
|
|
|
735
749
|
${trackingCode}
|
|
736
750
|
|
|
737
|
-
const
|
|
751
|
+
const pageComponents = {
|
|
738
752
|
${pageImports}
|
|
739
753
|
}
|
|
740
754
|
|
|
755
|
+
${priceDataCode}
|
|
756
|
+
|
|
741
757
|
const config = ${JSON.stringify(fullConfig, null, 2)}
|
|
742
758
|
|
|
743
|
-
const slugToKey = ${JSON.stringify(slugMap)}
|
|
744
759
|
const keyToSlug = ${JSON.stringify(
|
|
745
760
|
Object.fromEntries(Object.entries(slugMap).map(([s, k]) => [k, s]))
|
|
746
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
|
+
}
|
|
747
788
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
+
}`}
|
|
751
802
|
}
|
|
752
803
|
|
|
753
|
-
|
|
754
|
-
|
|
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
|
+
` : ""}
|
|
755
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
|
|
756
865
|
useEffect(() => {
|
|
757
866
|
const handlePopState = () => {
|
|
758
867
|
const path = window.location.pathname.split('/').filter(Boolean).pop() || ''
|
|
759
|
-
const
|
|
760
|
-
if (
|
|
761
|
-
|
|
868
|
+
const key = slugToKey[path]
|
|
869
|
+
if (key && key !== routerPageKey) {
|
|
870
|
+
goToPage(key)
|
|
762
871
|
}
|
|
763
872
|
}
|
|
764
873
|
window.addEventListener('popstate', handlePopState)
|
|
765
874
|
return () => window.removeEventListener('popstate', handlePopState)
|
|
766
|
-
}, [
|
|
767
|
-
|
|
768
|
-
// Expose navigation to FunnelProvider's router
|
|
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
|
-
}, [])
|
|
875
|
+
}, [routerPageKey, goToPage])
|
|
777
876
|
|
|
778
|
-
const PageComponent =
|
|
877
|
+
const PageComponent = pageComponents[displayedKey]
|
|
779
878
|
|
|
780
879
|
if (!PageComponent) {
|
|
781
|
-
return <div style={{ padding: '2rem', color: 'red' }}>Page not found: {
|
|
880
|
+
return <div style={{ padding: '2rem', color: 'red' }}>Page not found: {displayedKey}</div>
|
|
782
881
|
}
|
|
783
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
|
+
|
|
784
905
|
return (
|
|
785
906
|
<FunnelProvider
|
|
786
|
-
config={
|
|
787
|
-
initialPage={
|
|
788
|
-
|
|
907
|
+
config={runtimeConfig}
|
|
908
|
+
initialPage={initialPage}
|
|
909
|
+
basePath={basePath}
|
|
910
|
+
campaignSlug={campaignSlug}
|
|
911
|
+
priceData={priceData}
|
|
912
|
+
sessionData={sessionData}
|
|
789
913
|
>
|
|
790
914
|
<FunnelWrapper>
|
|
791
|
-
<
|
|
792
|
-
<PageComponent />
|
|
793
|
-
</Suspense>
|
|
915
|
+
<PageRenderer />
|
|
794
916
|
</FunnelWrapper>
|
|
795
917
|
</FunnelProvider>
|
|
796
918
|
)
|
|
@@ -798,9 +920,14 @@ function App() {
|
|
|
798
920
|
|
|
799
921
|
createRoot(document.getElementById('root')).render(
|
|
800
922
|
<StrictMode>
|
|
801
|
-
<
|
|
923
|
+
${isDev ? "<ErrorBoundary>" : ""}
|
|
924
|
+
<App />
|
|
925
|
+
${isDev ? "</ErrorBoundary>" : ""}
|
|
802
926
|
</StrictMode>
|
|
803
927
|
)
|
|
928
|
+
|
|
929
|
+
// Reveal body (the host page may set opacity:0 for a loading transition)
|
|
930
|
+
document.body.style.opacity = '1'
|
|
804
931
|
`;
|
|
805
932
|
}
|
|
806
933
|
var init_entry = __esm({
|
|
@@ -832,54 +959,66 @@ var init_html = __esm({
|
|
|
832
959
|
|
|
833
960
|
// src/vite/plugin.ts
|
|
834
961
|
import { resolve as resolve2, join as join7 } from "path";
|
|
835
|
-
import { existsSync as
|
|
962
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
836
963
|
function appfunnelPlugin(options) {
|
|
837
964
|
const { cwd, config, isDev } = options;
|
|
838
965
|
let pages = options.pages;
|
|
839
966
|
const pagesDir = resolve2(cwd, "src", "pages");
|
|
840
967
|
const funnelTsxPath = resolve2(cwd, "src", "funnel.tsx");
|
|
841
|
-
|
|
968
|
+
const appfunnelDir = join7(cwd, APPFUNNEL_DIR);
|
|
969
|
+
const htmlPath = join7(appfunnelDir, "index.html");
|
|
842
970
|
function getEntrySource() {
|
|
843
971
|
return generateEntrySource({
|
|
844
972
|
config,
|
|
845
973
|
pages,
|
|
846
974
|
pagesDir,
|
|
847
975
|
funnelTsxPath,
|
|
848
|
-
isDev
|
|
976
|
+
isDev,
|
|
977
|
+
priceData: options.priceData
|
|
849
978
|
});
|
|
850
979
|
}
|
|
851
980
|
return {
|
|
852
981
|
name: "appfunnel",
|
|
853
982
|
config() {
|
|
983
|
+
mkdirSync3(appfunnelDir, { recursive: true });
|
|
984
|
+
writeFileSync3(htmlPath, generateHtml(config.name || "AppFunnel"));
|
|
854
985
|
return {
|
|
986
|
+
// Don't let Vite auto-serve index.html — we handle it ourselves
|
|
987
|
+
appType: "custom",
|
|
855
988
|
resolve: {
|
|
856
989
|
alias: {
|
|
857
990
|
"@": resolve2(cwd, "src")
|
|
858
991
|
}
|
|
859
992
|
},
|
|
860
|
-
// Ensure we can import .tsx files
|
|
861
993
|
esbuild: {
|
|
862
994
|
jsx: "automatic"
|
|
863
995
|
},
|
|
864
996
|
optimizeDeps: {
|
|
865
|
-
include: ["react", "react-dom", "react/jsx-runtime"]
|
|
997
|
+
include: ["react", "react-dom", "react/jsx-runtime"],
|
|
998
|
+
force: true
|
|
866
999
|
}
|
|
867
1000
|
};
|
|
868
1001
|
},
|
|
869
1002
|
resolveId(id) {
|
|
870
|
-
if (id === VIRTUAL_ENTRY_ID) {
|
|
1003
|
+
if (id === VIRTUAL_ENTRY_ID || id === "/" + VIRTUAL_ENTRY_ID) {
|
|
871
1004
|
return RESOLVED_VIRTUAL_ENTRY_ID;
|
|
872
1005
|
}
|
|
873
1006
|
return null;
|
|
874
1007
|
},
|
|
875
|
-
load(id) {
|
|
1008
|
+
async load(id) {
|
|
876
1009
|
if (id === RESOLVED_VIRTUAL_ENTRY_ID) {
|
|
877
|
-
|
|
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;
|
|
878
1018
|
}
|
|
879
1019
|
return null;
|
|
880
1020
|
},
|
|
881
1021
|
configureServer(devServer) {
|
|
882
|
-
server = devServer;
|
|
883
1022
|
const watcher = devServer.watcher;
|
|
884
1023
|
watcher.add(pagesDir);
|
|
885
1024
|
const handlePagesChange = async () => {
|
|
@@ -903,7 +1042,7 @@ function appfunnelPlugin(options) {
|
|
|
903
1042
|
}
|
|
904
1043
|
});
|
|
905
1044
|
const configPath = join7(cwd, "appfunnel.config.ts");
|
|
906
|
-
if (
|
|
1045
|
+
if (existsSync5(configPath)) {
|
|
907
1046
|
watcher.add(configPath);
|
|
908
1047
|
watcher.on("change", (file) => {
|
|
909
1048
|
if (file === configPath) {
|
|
@@ -912,33 +1051,129 @@ function appfunnelPlugin(options) {
|
|
|
912
1051
|
});
|
|
913
1052
|
}
|
|
914
1053
|
return () => {
|
|
915
|
-
devServer.middlewares.use((req, res, next) => {
|
|
916
|
-
|
|
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")) {
|
|
917
1057
|
return next();
|
|
918
1058
|
}
|
|
919
|
-
|
|
920
|
-
|
|
1059
|
+
try {
|
|
1060
|
+
const rawHtml = readFileSync5(htmlPath, "utf-8");
|
|
1061
|
+
const html = await devServer.transformIndexHtml(req.url || "/", rawHtml);
|
|
921
1062
|
res.statusCode = 200;
|
|
922
1063
|
res.setHeader("Content-Type", "text/html");
|
|
923
|
-
res.end(
|
|
924
|
-
}
|
|
1064
|
+
res.end(html);
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
next(err);
|
|
1067
|
+
}
|
|
925
1068
|
});
|
|
926
1069
|
};
|
|
927
1070
|
},
|
|
928
|
-
// For production build: inject the HTML as the input
|
|
929
1071
|
transformIndexHtml(html) {
|
|
930
1072
|
return html;
|
|
931
1073
|
}
|
|
932
1074
|
};
|
|
933
1075
|
}
|
|
934
|
-
var VIRTUAL_ENTRY_ID, RESOLVED_VIRTUAL_ENTRY_ID;
|
|
1076
|
+
var VIRTUAL_ENTRY_ID, RESOLVED_VIRTUAL_ENTRY_ID, APPFUNNEL_DIR;
|
|
935
1077
|
var init_plugin = __esm({
|
|
936
1078
|
"src/vite/plugin.ts"() {
|
|
937
1079
|
"use strict";
|
|
938
1080
|
init_entry();
|
|
939
1081
|
init_html();
|
|
940
1082
|
VIRTUAL_ENTRY_ID = "@appfunnel/entry";
|
|
941
|
-
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";
|
|
942
1177
|
}
|
|
943
1178
|
});
|
|
944
1179
|
|
|
@@ -947,7 +1182,7 @@ var dev_exports = {};
|
|
|
947
1182
|
__export(dev_exports, {
|
|
948
1183
|
devCommand: () => devCommand
|
|
949
1184
|
});
|
|
950
|
-
import { readFileSync as
|
|
1185
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
951
1186
|
import { join as join8 } from "path";
|
|
952
1187
|
import pc6 from "picocolors";
|
|
953
1188
|
async function devCommand(options) {
|
|
@@ -963,13 +1198,22 @@ async function devCommand(options) {
|
|
|
963
1198
|
const projectId = await promptForProject(creds.token);
|
|
964
1199
|
config.projectId = projectId;
|
|
965
1200
|
const configPath = join8(cwd, "appfunnel.config.ts");
|
|
966
|
-
const configSource =
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1201
|
+
const configSource = readFileSync6(configPath, "utf-8");
|
|
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
|
+
}
|
|
971
1215
|
if (updated !== configSource) {
|
|
972
|
-
|
|
1216
|
+
writeFileSync4(configPath, updated);
|
|
973
1217
|
success(`Updated projectId in appfunnel.config.ts`);
|
|
974
1218
|
} else {
|
|
975
1219
|
warn(`Could not auto-update appfunnel.config.ts \u2014 add projectId: '${projectId}' manually.`);
|
|
@@ -980,8 +1224,43 @@ async function devCommand(options) {
|
|
|
980
1224
|
let pages = await extractPageDefinitions(cwd, pageKeys);
|
|
981
1225
|
s2.stop();
|
|
982
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
|
+
}
|
|
983
1253
|
const { createServer: createServer2 } = await import("vite");
|
|
984
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
|
+
}
|
|
985
1264
|
const server = await createServer2({
|
|
986
1265
|
root: cwd,
|
|
987
1266
|
server: {
|
|
@@ -990,11 +1269,13 @@ async function devCommand(options) {
|
|
|
990
1269
|
},
|
|
991
1270
|
plugins: [
|
|
992
1271
|
react.default(),
|
|
1272
|
+
...tailwindPlugin ? [tailwindPlugin()] : [],
|
|
993
1273
|
appfunnelPlugin({
|
|
994
1274
|
cwd,
|
|
995
1275
|
config,
|
|
996
1276
|
pages,
|
|
997
1277
|
isDev: true,
|
|
1278
|
+
priceData,
|
|
998
1279
|
async onPagesChange() {
|
|
999
1280
|
pageKeys = scanPages(cwd);
|
|
1000
1281
|
pages = await extractPageDefinitions(cwd, pageKeys);
|
|
@@ -1028,6 +1309,7 @@ var init_dev = __esm({
|
|
|
1028
1309
|
init_pages();
|
|
1029
1310
|
init_plugin();
|
|
1030
1311
|
init_projects();
|
|
1312
|
+
init_api();
|
|
1031
1313
|
}
|
|
1032
1314
|
});
|
|
1033
1315
|
|
|
@@ -1036,8 +1318,9 @@ var build_exports = {};
|
|
|
1036
1318
|
__export(build_exports, {
|
|
1037
1319
|
buildCommand: () => buildCommand
|
|
1038
1320
|
});
|
|
1039
|
-
import { resolve as
|
|
1040
|
-
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";
|
|
1041
1324
|
import pc7 from "picocolors";
|
|
1042
1325
|
async function buildCommand() {
|
|
1043
1326
|
const cwd = process.cwd();
|
|
@@ -1058,15 +1341,25 @@ async function buildCommand() {
|
|
|
1058
1341
|
s.stop();
|
|
1059
1342
|
validateRoutes(config, pages, pageKeys);
|
|
1060
1343
|
info(`Building ${pageKeys.length} pages...`);
|
|
1061
|
-
const outDir =
|
|
1344
|
+
const outDir = resolve3(cwd, "dist");
|
|
1062
1345
|
const { build } = await import("vite");
|
|
1063
1346
|
const react = await import("@vitejs/plugin-react");
|
|
1064
|
-
|
|
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");
|
|
1065
1357
|
const htmlContent = generateHtml(config.name || "AppFunnel");
|
|
1066
|
-
|
|
1358
|
+
writeFileSync5(htmlPath, htmlContent);
|
|
1067
1359
|
try {
|
|
1068
1360
|
await build({
|
|
1069
1361
|
root: cwd,
|
|
1362
|
+
base: "./",
|
|
1070
1363
|
build: {
|
|
1071
1364
|
outDir,
|
|
1072
1365
|
emptyOutDir: true,
|
|
@@ -1087,6 +1380,7 @@ async function buildCommand() {
|
|
|
1087
1380
|
},
|
|
1088
1381
|
plugins: [
|
|
1089
1382
|
react.default(),
|
|
1383
|
+
...tailwindPlugin ? [tailwindPlugin()] : [],
|
|
1090
1384
|
appfunnelPlugin({
|
|
1091
1385
|
cwd,
|
|
1092
1386
|
config,
|
|
@@ -1122,9 +1416,12 @@ async function buildCommand() {
|
|
|
1122
1416
|
const totalSize = assets.reduce((sum, a) => sum + a.size, 0);
|
|
1123
1417
|
const manifest = {
|
|
1124
1418
|
version: 1,
|
|
1419
|
+
buildHash: randomUUID2(),
|
|
1125
1420
|
sdkVersion: getSdkVersion2(cwd),
|
|
1126
1421
|
projectId: config.projectId,
|
|
1127
1422
|
funnelId: config.funnelId,
|
|
1423
|
+
name: config.name,
|
|
1424
|
+
initialPageKey: config.initialPageKey,
|
|
1128
1425
|
pages: { ...config.pages, ...mergedPages },
|
|
1129
1426
|
routes: { ...config.routes, ...mergedRoutes },
|
|
1130
1427
|
responses: config.responses || {},
|
|
@@ -1134,11 +1431,11 @@ async function buildCommand() {
|
|
|
1134
1431
|
assets,
|
|
1135
1432
|
totalSize
|
|
1136
1433
|
};
|
|
1137
|
-
|
|
1434
|
+
writeFileSync5(join9(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
1138
1435
|
console.log();
|
|
1139
1436
|
success("Build complete");
|
|
1140
1437
|
console.log();
|
|
1141
|
-
console.log(` ${pc7.dim("Output:")}
|
|
1438
|
+
console.log(` ${pc7.dim("Output:")} dist/`);
|
|
1142
1439
|
console.log(` ${pc7.dim("Pages:")} ${pageKeys.length}`);
|
|
1143
1440
|
console.log(` ${pc7.dim("Size:")} ${formatSize(totalSize)}`);
|
|
1144
1441
|
console.log();
|
|
@@ -1254,7 +1551,7 @@ function formatSize(bytes) {
|
|
|
1254
1551
|
}
|
|
1255
1552
|
function getSdkVersion2(cwd) {
|
|
1256
1553
|
try {
|
|
1257
|
-
const pkg = JSON.parse(
|
|
1554
|
+
const pkg = JSON.parse(readFileSync7(join9(cwd, "node_modules", "@appfunnel", "sdk", "package.json"), "utf-8"));
|
|
1258
1555
|
return pkg.version;
|
|
1259
1556
|
} catch {
|
|
1260
1557
|
return "0.0.0";
|
|
@@ -1285,83 +1582,25 @@ var init_build = __esm({
|
|
|
1285
1582
|
}
|
|
1286
1583
|
});
|
|
1287
1584
|
|
|
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;
|
|
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");
|
|
1357
1599
|
}
|
|
1358
1600
|
}
|
|
1359
|
-
var
|
|
1360
|
-
|
|
1361
|
-
"src/lib/api.ts"() {
|
|
1601
|
+
var init_config_patch = __esm({
|
|
1602
|
+
"src/lib/config-patch.ts"() {
|
|
1362
1603
|
"use strict";
|
|
1363
|
-
init_errors();
|
|
1364
|
-
DEFAULT_API_BASE3 = "https://api.appfunnel.net";
|
|
1365
1604
|
}
|
|
1366
1605
|
});
|
|
1367
1606
|
|
|
@@ -1370,34 +1609,49 @@ var publish_exports = {};
|
|
|
1370
1609
|
__export(publish_exports, {
|
|
1371
1610
|
publishCommand: () => publishCommand
|
|
1372
1611
|
});
|
|
1373
|
-
import { resolve as
|
|
1374
|
-
import { readFileSync as
|
|
1612
|
+
import { resolve as resolve4, join as join11 } from "path";
|
|
1613
|
+
import { readFileSync as readFileSync9, existsSync as existsSync6 } from "fs";
|
|
1375
1614
|
import pc8 from "picocolors";
|
|
1376
1615
|
function getMimeType(path) {
|
|
1377
1616
|
const ext = path.substring(path.lastIndexOf("."));
|
|
1378
1617
|
return MIME_TYPES[ext] || "application/octet-stream";
|
|
1379
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
|
+
}
|
|
1380
1624
|
async function publishCommand() {
|
|
1381
1625
|
const cwd = process.cwd();
|
|
1382
1626
|
const creds = requireAuth();
|
|
1383
1627
|
checkVersionCompatibility(cwd);
|
|
1384
1628
|
const config = await loadConfig(cwd);
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
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)) {
|
|
1388
1640
|
throw new CLIError(
|
|
1389
1641
|
"BUILD_NOT_FOUND",
|
|
1390
1642
|
"No build output found.",
|
|
1391
1643
|
"Run 'appfunnel build' first."
|
|
1392
1644
|
);
|
|
1393
1645
|
}
|
|
1394
|
-
const manifest = JSON.parse(
|
|
1646
|
+
const manifest = JSON.parse(readFileSync9(manifestPath, "utf-8"));
|
|
1395
1647
|
const assets = manifest.assets || [];
|
|
1396
|
-
const s = spinner("
|
|
1648
|
+
const s = spinner("Preparing assets...");
|
|
1397
1649
|
const assetPayloads = [];
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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)) {
|
|
1401
1655
|
s.stop();
|
|
1402
1656
|
throw new CLIError(
|
|
1403
1657
|
"BUILD_NOT_FOUND",
|
|
@@ -1405,44 +1659,37 @@ async function publishCommand() {
|
|
|
1405
1659
|
"Run 'appfunnel build' to regenerate."
|
|
1406
1660
|
);
|
|
1407
1661
|
}
|
|
1662
|
+
const content = readFileSync9(fullPath);
|
|
1663
|
+
totalBytes += content.length;
|
|
1408
1664
|
assetPayloads.push({
|
|
1409
1665
|
path: asset.path,
|
|
1410
|
-
content
|
|
1666
|
+
content,
|
|
1411
1667
|
contentType: getMimeType(asset.path)
|
|
1412
1668
|
});
|
|
1669
|
+
s.text = `Preparing assets... ${i + 1}/${assets.length} ${pc8.dim(`(${formatSize2(totalBytes)})`)}`;
|
|
1413
1670
|
}
|
|
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
|
-
}
|
|
1671
|
+
s.text = `Uploading ${assets.length} assets ${pc8.dim(`(${formatSize2(totalBytes)})`)}`;
|
|
1432
1672
|
const result = await publishBuild(
|
|
1433
1673
|
projectId,
|
|
1434
|
-
funnelId,
|
|
1674
|
+
config.funnelId || "",
|
|
1435
1675
|
manifest,
|
|
1436
1676
|
assetPayloads,
|
|
1437
1677
|
{ token: creds.token }
|
|
1438
1678
|
);
|
|
1439
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
|
+
}
|
|
1440
1684
|
console.log();
|
|
1441
1685
|
success("Published successfully");
|
|
1442
1686
|
console.log();
|
|
1443
1687
|
console.log(` ${pc8.dim("Build ID:")} ${result.buildId}`);
|
|
1688
|
+
if (result.funnelId && !config.funnelId) {
|
|
1689
|
+
console.log(` ${pc8.dim("Funnel:")} ${result.funnelId}`);
|
|
1690
|
+
}
|
|
1444
1691
|
console.log(` ${pc8.dim("URL:")} ${pc8.cyan(result.url)}`);
|
|
1445
|
-
console.log(` ${pc8.dim("Assets:")} ${assets.length} files`);
|
|
1692
|
+
console.log(` ${pc8.dim("Assets:")} ${assets.length} files ${pc8.dim(`(${formatSize2(totalBytes)})`)}`);
|
|
1446
1693
|
console.log();
|
|
1447
1694
|
}
|
|
1448
1695
|
var MIME_TYPES;
|
|
@@ -1455,6 +1702,7 @@ var init_publish = __esm({
|
|
|
1455
1702
|
init_version();
|
|
1456
1703
|
init_api();
|
|
1457
1704
|
init_errors();
|
|
1705
|
+
init_config_patch();
|
|
1458
1706
|
MIME_TYPES = {
|
|
1459
1707
|
".js": "application/javascript",
|
|
1460
1708
|
".css": "text/css",
|
|
@@ -1474,7 +1722,7 @@ init_errors();
|
|
|
1474
1722
|
import { Command } from "commander";
|
|
1475
1723
|
import pc9 from "picocolors";
|
|
1476
1724
|
var program = new Command();
|
|
1477
|
-
program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.
|
|
1725
|
+
program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.6.0");
|
|
1478
1726
|
program.command("init <name>").description("Create a new AppFunnel project").action(async (name) => {
|
|
1479
1727
|
const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1480
1728
|
await initCommand2(name);
|