appfunnel 0.1.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 +1461 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1461 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/lib/errors.ts
|
|
14
|
+
import pc from "picocolors";
|
|
15
|
+
function formatError(err) {
|
|
16
|
+
const lines = [
|
|
17
|
+
`${pc.red("ERROR")} ${pc.dim(`[${err.code}]`)}: ${err.message}`
|
|
18
|
+
];
|
|
19
|
+
if (err.hint) {
|
|
20
|
+
lines.push(` ${pc.dim("Hint:")} ${err.hint}`);
|
|
21
|
+
}
|
|
22
|
+
return lines.join("\n");
|
|
23
|
+
}
|
|
24
|
+
function formatWarning(code, message, hint) {
|
|
25
|
+
const lines = [
|
|
26
|
+
`${pc.yellow("WARNING")} ${pc.dim(`[${code}]`)}: ${message}`
|
|
27
|
+
];
|
|
28
|
+
if (hint) {
|
|
29
|
+
lines.push(` ${pc.dim("Hint:")} ${hint}`);
|
|
30
|
+
}
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
var CLIError;
|
|
34
|
+
var init_errors = __esm({
|
|
35
|
+
"src/lib/errors.ts"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
CLIError = class extends Error {
|
|
38
|
+
code;
|
|
39
|
+
hint;
|
|
40
|
+
statusCode;
|
|
41
|
+
constructor(code, message, hint) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "CLIError";
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.hint = hint;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// src/lib/logger.ts
|
|
52
|
+
import { readFileSync } from "fs";
|
|
53
|
+
import pc2 from "picocolors";
|
|
54
|
+
import ora from "ora";
|
|
55
|
+
function success(msg) {
|
|
56
|
+
console.log(`${pc2.green("\u2713")} ${msg}`);
|
|
57
|
+
}
|
|
58
|
+
function error(msg) {
|
|
59
|
+
console.error(`${pc2.red("\u2717")} ${msg}`);
|
|
60
|
+
}
|
|
61
|
+
function warn(msg) {
|
|
62
|
+
console.warn(`${pc2.yellow("!")} ${msg}`);
|
|
63
|
+
}
|
|
64
|
+
function info(msg) {
|
|
65
|
+
console.log(`${pc2.blue("\u2139")} ${msg}`);
|
|
66
|
+
}
|
|
67
|
+
function spinner(msg) {
|
|
68
|
+
return ora({ text: msg, color: "cyan" }).start();
|
|
69
|
+
}
|
|
70
|
+
var init_logger = __esm({
|
|
71
|
+
"src/lib/logger.ts"() {
|
|
72
|
+
"use strict";
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// src/lib/auth.ts
|
|
77
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
|
|
78
|
+
import { join } from "path";
|
|
79
|
+
import { homedir } from "os";
|
|
80
|
+
function readCredentials() {
|
|
81
|
+
try {
|
|
82
|
+
const raw = readFileSync2(CREDENTIALS_PATH, "utf-8");
|
|
83
|
+
const data = JSON.parse(raw);
|
|
84
|
+
if (!data.token) return null;
|
|
85
|
+
return data;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function writeCredentials(creds) {
|
|
91
|
+
const dir = homedir();
|
|
92
|
+
mkdirSync(dir, { recursive: true });
|
|
93
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2) + "\n", "utf-8");
|
|
94
|
+
}
|
|
95
|
+
function requireAuth() {
|
|
96
|
+
const creds = readCredentials();
|
|
97
|
+
if (!creds) {
|
|
98
|
+
throw new CLIError(
|
|
99
|
+
"AUTH_REQUIRED",
|
|
100
|
+
"Not logged in.",
|
|
101
|
+
"Run 'appfunnel login' to authenticate."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (creds.expiresAt) {
|
|
105
|
+
const expiresAt = new Date(creds.expiresAt);
|
|
106
|
+
if (expiresAt < /* @__PURE__ */ new Date()) {
|
|
107
|
+
throw new CLIError(
|
|
108
|
+
"AUTH_EXPIRED",
|
|
109
|
+
"Token expired.",
|
|
110
|
+
"Run 'appfunnel login' to re-authenticate."
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return creds;
|
|
115
|
+
}
|
|
116
|
+
var CREDENTIALS_PATH;
|
|
117
|
+
var init_auth = __esm({
|
|
118
|
+
"src/lib/auth.ts"() {
|
|
119
|
+
"use strict";
|
|
120
|
+
init_errors();
|
|
121
|
+
CREDENTIALS_PATH = join(homedir(), ".appfunnelrc");
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// src/commands/init.ts
|
|
126
|
+
var init_exports = {};
|
|
127
|
+
__export(init_exports, {
|
|
128
|
+
initCommand: () => initCommand
|
|
129
|
+
});
|
|
130
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync } from "fs";
|
|
131
|
+
import { join as join2 } from "path";
|
|
132
|
+
import pc3 from "picocolors";
|
|
133
|
+
import select from "@inquirer/select";
|
|
134
|
+
async function fetchProjects(token) {
|
|
135
|
+
const response = await fetch(`${DEFAULT_API_BASE}/user/projects`, {
|
|
136
|
+
headers: {
|
|
137
|
+
Authorization: token,
|
|
138
|
+
"Content-Type": "application/json"
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new CLIError("API_ERROR", "Failed to fetch projects.");
|
|
143
|
+
}
|
|
144
|
+
const body = await response.json();
|
|
145
|
+
return body.data;
|
|
146
|
+
}
|
|
147
|
+
async function initCommand(name) {
|
|
148
|
+
const creds = requireAuth();
|
|
149
|
+
const dir = join2(process.cwd(), name);
|
|
150
|
+
if (existsSync(dir)) {
|
|
151
|
+
error(`Directory '${name}' already exists.`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const spin = spinner("Fetching projects\u2026");
|
|
155
|
+
let projects;
|
|
156
|
+
try {
|
|
157
|
+
projects = await fetchProjects(creds.token);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
spin.stop();
|
|
160
|
+
if (err instanceof CLIError) throw err;
|
|
161
|
+
throw new CLIError(
|
|
162
|
+
"API_ERROR",
|
|
163
|
+
"Failed to reach the API. Check your internet connection."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
spin.stop();
|
|
167
|
+
if (projects.length === 0) {
|
|
168
|
+
throw new CLIError(
|
|
169
|
+
"NO_PROJECTS",
|
|
170
|
+
"No projects found.",
|
|
171
|
+
"Create a project at https://appfunnel.net first."
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const projectId = await select({
|
|
175
|
+
message: "Select a project",
|
|
176
|
+
choices: projects.map((p) => ({
|
|
177
|
+
name: `${p.name} ${pc3.dim(`(${p.id})`)}`,
|
|
178
|
+
value: p.id
|
|
179
|
+
}))
|
|
180
|
+
});
|
|
181
|
+
const project = projects.find((p) => p.id === projectId);
|
|
182
|
+
const s = spinner(`Creating ${name}...`);
|
|
183
|
+
mkdirSync2(join2(dir, "src", "pages"), { recursive: true });
|
|
184
|
+
mkdirSync2(join2(dir, "src", "components"), { recursive: true });
|
|
185
|
+
mkdirSync2(join2(dir, "locales"), { recursive: true });
|
|
186
|
+
writeFileSync2(
|
|
187
|
+
join2(dir, "package.json"),
|
|
188
|
+
JSON.stringify(
|
|
189
|
+
{
|
|
190
|
+
name,
|
|
191
|
+
version: "0.1.0",
|
|
192
|
+
private: true,
|
|
193
|
+
type: "module",
|
|
194
|
+
scripts: {
|
|
195
|
+
dev: "appfunnel dev",
|
|
196
|
+
build: "appfunnel build",
|
|
197
|
+
publish: "appfunnel publish"
|
|
198
|
+
},
|
|
199
|
+
dependencies: {
|
|
200
|
+
"@appfunnel/sdk": "^0.1.0",
|
|
201
|
+
react: "^18.3.0",
|
|
202
|
+
"react-dom": "^18.3.0"
|
|
203
|
+
},
|
|
204
|
+
devDependencies: {
|
|
205
|
+
appfunnel: "^0.1.0",
|
|
206
|
+
typescript: "^5.4.0",
|
|
207
|
+
"@types/react": "^18.2.0",
|
|
208
|
+
"@types/react-dom": "^18.2.0",
|
|
209
|
+
vite: "^6.0.0",
|
|
210
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
211
|
+
tailwindcss: "^4.0.0",
|
|
212
|
+
"@tailwindcss/vite": "^4.0.0"
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
null,
|
|
216
|
+
2
|
|
217
|
+
) + "\n"
|
|
218
|
+
);
|
|
219
|
+
writeFileSync2(
|
|
220
|
+
join2(dir, "tsconfig.json"),
|
|
221
|
+
JSON.stringify(
|
|
222
|
+
{
|
|
223
|
+
compilerOptions: {
|
|
224
|
+
target: "ES2020",
|
|
225
|
+
module: "ESNext",
|
|
226
|
+
moduleResolution: "bundler",
|
|
227
|
+
jsx: "react-jsx",
|
|
228
|
+
strict: true,
|
|
229
|
+
esModuleInterop: true,
|
|
230
|
+
skipLibCheck: true,
|
|
231
|
+
paths: {
|
|
232
|
+
"@/*": ["./src/*"]
|
|
233
|
+
},
|
|
234
|
+
baseUrl: "."
|
|
235
|
+
},
|
|
236
|
+
include: ["src"]
|
|
237
|
+
},
|
|
238
|
+
null,
|
|
239
|
+
2
|
|
240
|
+
) + "\n"
|
|
241
|
+
);
|
|
242
|
+
writeFileSync2(join2(dir, "src", "app.css"), `@import "tailwindcss";
|
|
243
|
+
`);
|
|
244
|
+
writeFileSync2(
|
|
245
|
+
join2(dir, "appfunnel.config.ts"),
|
|
246
|
+
`import { defineConfig } from '@appfunnel/sdk'
|
|
247
|
+
|
|
248
|
+
export default defineConfig({
|
|
249
|
+
projectId: '${projectId}',
|
|
250
|
+
name: '${name}',
|
|
251
|
+
defaultLocale: 'en',
|
|
252
|
+
|
|
253
|
+
variables: {
|
|
254
|
+
'answers.goal': { type: 'string' },
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
queryParams: ['utm_source', 'utm_medium', 'utm_campaign'],
|
|
258
|
+
|
|
259
|
+
products: {
|
|
260
|
+
items: [],
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
`
|
|
264
|
+
);
|
|
265
|
+
writeFileSync2(
|
|
266
|
+
join2(dir, "src", "funnel.tsx"),
|
|
267
|
+
`import './app.css'
|
|
268
|
+
|
|
269
|
+
export default function Funnel({ children }: { children: React.ReactNode }) {
|
|
270
|
+
return (
|
|
271
|
+
<div className="min-h-screen">
|
|
272
|
+
{children}
|
|
273
|
+
</div>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
`
|
|
277
|
+
);
|
|
278
|
+
writeFileSync2(
|
|
279
|
+
join2(dir, "src", "pages", "index.tsx"),
|
|
280
|
+
`import { definePage, useVariable, useNavigation } from '@appfunnel/sdk'
|
|
281
|
+
|
|
282
|
+
export const page = definePage({
|
|
283
|
+
name: 'Landing',
|
|
284
|
+
type: 'default',
|
|
285
|
+
routes: [],
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
export default function Landing() {
|
|
289
|
+
const [goal, setGoal] = useVariable<string>('answers.goal')
|
|
290
|
+
const { goToNextPage } = useNavigation()
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
294
|
+
<div className="w-full max-w-md space-y-6">
|
|
295
|
+
<h1 className="text-3xl font-bold text-center">Welcome</h1>
|
|
296
|
+
<input
|
|
297
|
+
type="text"
|
|
298
|
+
value={goal}
|
|
299
|
+
onChange={(e) => setGoal(e.target.value)}
|
|
300
|
+
placeholder="What's your goal?"
|
|
301
|
+
className="w-full rounded-xl border p-4 text-lg"
|
|
302
|
+
/>
|
|
303
|
+
<button
|
|
304
|
+
onClick={goToNextPage}
|
|
305
|
+
disabled={!goal.trim()}
|
|
306
|
+
className="w-full rounded-xl bg-blue-600 py-4 text-lg font-bold text-white disabled:opacity-50"
|
|
307
|
+
>
|
|
308
|
+
Continue
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
`
|
|
315
|
+
);
|
|
316
|
+
writeFileSync2(
|
|
317
|
+
join2(dir, "locales", "en.json"),
|
|
318
|
+
JSON.stringify({ welcome: "Welcome" }, null, 2) + "\n"
|
|
319
|
+
);
|
|
320
|
+
writeFileSync2(
|
|
321
|
+
join2(dir, ".gitignore"),
|
|
322
|
+
`node_modules
|
|
323
|
+
dist
|
|
324
|
+
.appfunnel
|
|
325
|
+
`
|
|
326
|
+
);
|
|
327
|
+
s.stop();
|
|
328
|
+
console.log();
|
|
329
|
+
success(`Created ${pc3.bold(name)} for project ${pc3.bold(project.name)}`);
|
|
330
|
+
console.log();
|
|
331
|
+
console.log(` ${pc3.dim("cd")} ${name}`);
|
|
332
|
+
console.log(` ${pc3.dim("npm install")}`);
|
|
333
|
+
console.log(` ${pc3.dim("appfunnel dev")}`);
|
|
334
|
+
console.log();
|
|
335
|
+
}
|
|
336
|
+
var DEFAULT_API_BASE;
|
|
337
|
+
var init_init = __esm({
|
|
338
|
+
"src/commands/init.ts"() {
|
|
339
|
+
"use strict";
|
|
340
|
+
init_logger();
|
|
341
|
+
init_auth();
|
|
342
|
+
init_errors();
|
|
343
|
+
DEFAULT_API_BASE = "https://api.appfunnel.net";
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// src/commands/login.ts
|
|
348
|
+
var login_exports = {};
|
|
349
|
+
__export(login_exports, {
|
|
350
|
+
loginCommand: () => loginCommand
|
|
351
|
+
});
|
|
352
|
+
import { createServer } from "http";
|
|
353
|
+
import { randomUUID } from "crypto";
|
|
354
|
+
import open from "open";
|
|
355
|
+
async function loginCommand() {
|
|
356
|
+
const state = randomUUID();
|
|
357
|
+
return new Promise((resolve5, reject) => {
|
|
358
|
+
const server = createServer((req, res) => {
|
|
359
|
+
const url = new URL(req.url || "/", `http://localhost`);
|
|
360
|
+
if (url.pathname !== "/callback") {
|
|
361
|
+
res.writeHead(404);
|
|
362
|
+
res.end("Not found");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const token = url.searchParams.get("token");
|
|
366
|
+
const returnedState = url.searchParams.get("state");
|
|
367
|
+
const userId = url.searchParams.get("userId") || "";
|
|
368
|
+
const email = url.searchParams.get("email") || "";
|
|
369
|
+
const expiresAt = url.searchParams.get("expiresAt") || "";
|
|
370
|
+
if (returnedState !== state) {
|
|
371
|
+
res.writeHead(400);
|
|
372
|
+
res.end("Invalid state parameter. Please try again.");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (!token) {
|
|
376
|
+
res.writeHead(400);
|
|
377
|
+
res.end("No token received. Please try again.");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
writeCredentials({ token, userId, email, expiresAt });
|
|
381
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
382
|
+
res.end(`
|
|
383
|
+
<!DOCTYPE html>
|
|
384
|
+
<html>
|
|
385
|
+
<body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;">
|
|
386
|
+
<div style="text-align: center;">
|
|
387
|
+
<h1>Logged in!</h1>
|
|
388
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
389
|
+
</div>
|
|
390
|
+
</body>
|
|
391
|
+
</html>
|
|
392
|
+
`);
|
|
393
|
+
spinner2.stop();
|
|
394
|
+
success(`Logged in as ${email || userId}`);
|
|
395
|
+
server.close();
|
|
396
|
+
resolve5();
|
|
397
|
+
});
|
|
398
|
+
server.listen(0, "127.0.0.1", () => {
|
|
399
|
+
const addr = server.address();
|
|
400
|
+
if (!addr || typeof addr === "string") {
|
|
401
|
+
reject(new Error("Failed to start local server"));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const port = addr.port;
|
|
405
|
+
const authUrl = `${AUTH_BASE_URL}/cli/authorize?port=${port}&state=${state}`;
|
|
406
|
+
info("Opening browser for authentication...");
|
|
407
|
+
open(authUrl).catch(() => {
|
|
408
|
+
warn(`Could not open browser. Please visit:
|
|
409
|
+
${authUrl}`);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
const spinner2 = spinner("Waiting for authentication...");
|
|
413
|
+
const timeout = setTimeout(() => {
|
|
414
|
+
spinner2.stop();
|
|
415
|
+
server.close();
|
|
416
|
+
reject(new Error("Authentication timed out. Please try again."));
|
|
417
|
+
}, TIMEOUT_MS);
|
|
418
|
+
server.on("close", () => clearTimeout(timeout));
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
var AUTH_BASE_URL, TIMEOUT_MS;
|
|
422
|
+
var init_login = __esm({
|
|
423
|
+
"src/commands/login.ts"() {
|
|
424
|
+
"use strict";
|
|
425
|
+
init_logger();
|
|
426
|
+
init_auth();
|
|
427
|
+
AUTH_BASE_URL = "https://appfunnel.net";
|
|
428
|
+
TIMEOUT_MS = 12e4;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// src/commands/whoami.ts
|
|
433
|
+
var whoami_exports = {};
|
|
434
|
+
__export(whoami_exports, {
|
|
435
|
+
whoamiCommand: () => whoamiCommand
|
|
436
|
+
});
|
|
437
|
+
import pc4 from "picocolors";
|
|
438
|
+
async function whoamiCommand() {
|
|
439
|
+
const creds = requireAuth();
|
|
440
|
+
const spin = spinner("Verifying credentials\u2026");
|
|
441
|
+
try {
|
|
442
|
+
const response = await fetch(`${DEFAULT_API_BASE2}/user`, {
|
|
443
|
+
headers: {
|
|
444
|
+
Authorization: creds.token,
|
|
445
|
+
"Content-Type": "application/json"
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
if (!response.ok) {
|
|
449
|
+
spin.stop();
|
|
450
|
+
throw new CLIError(
|
|
451
|
+
"AUTH_EXPIRED",
|
|
452
|
+
"Token is no longer valid.",
|
|
453
|
+
"Run 'appfunnel login' to re-authenticate."
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
const user = await response.json();
|
|
457
|
+
spin.stop();
|
|
458
|
+
success(`Logged in as ${pc4.bold(user.email)}`);
|
|
459
|
+
info(`User ID: ${pc4.dim(user.id)}`);
|
|
460
|
+
if (creds.expiresAt) {
|
|
461
|
+
const expiresAt = new Date(creds.expiresAt);
|
|
462
|
+
info(`Token expires: ${pc4.dim(expiresAt.toLocaleString())}`);
|
|
463
|
+
}
|
|
464
|
+
} catch (err) {
|
|
465
|
+
spin.stop();
|
|
466
|
+
if (err instanceof CLIError) throw err;
|
|
467
|
+
throw new CLIError(
|
|
468
|
+
"API_ERROR",
|
|
469
|
+
"Failed to reach the API. Check your internet connection."
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
var DEFAULT_API_BASE2;
|
|
474
|
+
var init_whoami = __esm({
|
|
475
|
+
"src/commands/whoami.ts"() {
|
|
476
|
+
"use strict";
|
|
477
|
+
init_auth();
|
|
478
|
+
init_errors();
|
|
479
|
+
init_logger();
|
|
480
|
+
DEFAULT_API_BASE2 = "https://api.appfunnel.net";
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// src/lib/config.ts
|
|
485
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
486
|
+
import { join as join3, resolve } from "path";
|
|
487
|
+
async function loadConfig(cwd) {
|
|
488
|
+
const configPath = join3(cwd, CONFIG_FILE);
|
|
489
|
+
if (!existsSync2(configPath)) {
|
|
490
|
+
throw new CLIError(
|
|
491
|
+
"CONFIG_NOT_FOUND",
|
|
492
|
+
`No ${CONFIG_FILE} found in ${cwd}.`,
|
|
493
|
+
"Run 'appfunnel init' to create a new project, or cd into your project directory."
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
const { transform } = await import("esbuild");
|
|
497
|
+
const raw = readFileSync3(configPath, "utf-8");
|
|
498
|
+
const result = await transform(raw, {
|
|
499
|
+
loader: "ts",
|
|
500
|
+
format: "esm",
|
|
501
|
+
target: "es2022"
|
|
502
|
+
});
|
|
503
|
+
const code = result.code.replace(/import\s*\{[^}]*\}\s*from\s*['"]@appfunnel\/sdk['"]\s*;?/g, "").replace(/\bdefineConfig\s*\(/g, "(");
|
|
504
|
+
const dataUri = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
|
|
505
|
+
const mod = await import(dataUri);
|
|
506
|
+
const config = mod.default;
|
|
507
|
+
if (!config || !config.projectId) {
|
|
508
|
+
throw new CLIError(
|
|
509
|
+
"CONFIG_NOT_FOUND",
|
|
510
|
+
`Invalid config in ${CONFIG_FILE}: missing projectId.`,
|
|
511
|
+
"Make sure your config exports a valid object with defineConfig()."
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
return config;
|
|
515
|
+
}
|
|
516
|
+
function resolvePagesDir(cwd) {
|
|
517
|
+
return resolve(cwd, "src", "pages");
|
|
518
|
+
}
|
|
519
|
+
var CONFIG_FILE;
|
|
520
|
+
var init_config = __esm({
|
|
521
|
+
"src/lib/config.ts"() {
|
|
522
|
+
"use strict";
|
|
523
|
+
init_errors();
|
|
524
|
+
CONFIG_FILE = "appfunnel.config.ts";
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// src/lib/version.ts
|
|
529
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
530
|
+
import { join as join4 } from "path";
|
|
531
|
+
function checkVersionCompatibility(cwd) {
|
|
532
|
+
const cliVersion = getCliVersion();
|
|
533
|
+
const sdkVersion = getSdkVersion(cwd);
|
|
534
|
+
const [cliMajor, cliMinor] = cliVersion.split(".").map(Number);
|
|
535
|
+
const [sdkMajor, sdkMinor] = sdkVersion.split(".").map(Number);
|
|
536
|
+
if (cliMajor !== sdkMajor || cliMinor !== sdkMinor) {
|
|
537
|
+
throw new CLIError(
|
|
538
|
+
"VERSION_MISMATCH",
|
|
539
|
+
`CLI version ${cliVersion} requires @appfunnel/sdk ^${cliMajor}.${cliMinor}.0, but found ${sdkVersion}.`,
|
|
540
|
+
"Run 'npm install @appfunnel/sdk@latest' to update."
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function getCliVersion() {
|
|
545
|
+
try {
|
|
546
|
+
const pkg = JSON.parse(
|
|
547
|
+
readFileSync4(new URL("../../package.json", import.meta.url), "utf-8")
|
|
548
|
+
);
|
|
549
|
+
return pkg.version;
|
|
550
|
+
} catch {
|
|
551
|
+
return "0.0.0";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function getSdkVersion(cwd) {
|
|
555
|
+
try {
|
|
556
|
+
const pkgPath = join4(cwd, "node_modules", "@appfunnel", "sdk", "package.json");
|
|
557
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
558
|
+
return pkg.version;
|
|
559
|
+
} catch {
|
|
560
|
+
throw new CLIError(
|
|
561
|
+
"VERSION_MISMATCH",
|
|
562
|
+
"@appfunnel/sdk is not installed.",
|
|
563
|
+
"Run 'npm install @appfunnel/sdk' to install it."
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
var init_version = __esm({
|
|
568
|
+
"src/lib/version.ts"() {
|
|
569
|
+
"use strict";
|
|
570
|
+
init_errors();
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// src/extract/pages.ts
|
|
575
|
+
import { readdirSync, readFileSync as readFileSync5, existsSync as existsSync3 } from "fs";
|
|
576
|
+
import { join as join5, basename } from "path";
|
|
577
|
+
function scanPages(cwd) {
|
|
578
|
+
const pagesDir = resolvePagesDir(cwd);
|
|
579
|
+
if (!existsSync3(pagesDir)) {
|
|
580
|
+
throw new CLIError(
|
|
581
|
+
"NO_PAGES",
|
|
582
|
+
"No src/pages/ directory found.",
|
|
583
|
+
"Create src/pages/ and add at least one .tsx page file."
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
const files = readdirSync(pagesDir).filter((f) => f.endsWith(".tsx") && !f.startsWith("_")).map((f) => basename(f, ".tsx")).sort();
|
|
587
|
+
if (files.length === 0) {
|
|
588
|
+
throw new CLIError(
|
|
589
|
+
"NO_PAGES",
|
|
590
|
+
"No page files found in src/pages/.",
|
|
591
|
+
"Add .tsx files to src/pages/. Each file is a funnel page."
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
return files;
|
|
595
|
+
}
|
|
596
|
+
async function extractPageDefinitions(cwd, pageKeys) {
|
|
597
|
+
const ts = await import("typescript");
|
|
598
|
+
const pagesDir = resolvePagesDir(cwd);
|
|
599
|
+
const result = {};
|
|
600
|
+
for (const key of pageKeys) {
|
|
601
|
+
const filePath = join5(pagesDir, `${key}.tsx`);
|
|
602
|
+
const source = readFileSync5(filePath, "utf-8");
|
|
603
|
+
const definition = extractDefinePage(ts, source, filePath);
|
|
604
|
+
if (definition) {
|
|
605
|
+
result[key] = definition;
|
|
606
|
+
} else {
|
|
607
|
+
result[key] = {
|
|
608
|
+
name: key,
|
|
609
|
+
type: "default"
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
615
|
+
function extractDefinePage(ts, source, fileName) {
|
|
616
|
+
const sourceFile = ts.createSourceFile(
|
|
617
|
+
fileName,
|
|
618
|
+
source,
|
|
619
|
+
ts.ScriptTarget.Latest,
|
|
620
|
+
true,
|
|
621
|
+
ts.ScriptKind.TSX
|
|
622
|
+
);
|
|
623
|
+
let definition = null;
|
|
624
|
+
function visit(node) {
|
|
625
|
+
if (ts.isVariableStatement(node)) {
|
|
626
|
+
const isExported = node.modifiers?.some(
|
|
627
|
+
(m) => m.kind === ts.SyntaxKind.ExportKeyword
|
|
628
|
+
);
|
|
629
|
+
if (!isExported) return;
|
|
630
|
+
for (const decl of node.declarationList.declarations) {
|
|
631
|
+
if (ts.isIdentifier(decl.name) && decl.name.text === "page" && decl.initializer && ts.isCallExpression(decl.initializer)) {
|
|
632
|
+
const call = decl.initializer;
|
|
633
|
+
const callee = call.expression;
|
|
634
|
+
if (ts.isIdentifier(callee) && callee.text === "definePage") {
|
|
635
|
+
const arg = call.arguments[0];
|
|
636
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
637
|
+
definition = extractObjectLiteral(ts, arg);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
ts.forEachChild(node, visit);
|
|
644
|
+
}
|
|
645
|
+
ts.forEachChild(sourceFile, visit);
|
|
646
|
+
return definition;
|
|
647
|
+
}
|
|
648
|
+
function extractObjectLiteral(ts, node) {
|
|
649
|
+
const result = {};
|
|
650
|
+
for (const prop of node.properties) {
|
|
651
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
652
|
+
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) continue;
|
|
653
|
+
const key = ts.isIdentifier(prop.name) ? prop.name.text : prop.name.text;
|
|
654
|
+
result[key] = extractValue(ts, prop.initializer);
|
|
655
|
+
}
|
|
656
|
+
return result;
|
|
657
|
+
}
|
|
658
|
+
function extractValue(ts, node) {
|
|
659
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
660
|
+
return node.text;
|
|
661
|
+
}
|
|
662
|
+
if (ts.isNumericLiteral(node)) {
|
|
663
|
+
return Number(node.text);
|
|
664
|
+
}
|
|
665
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
666
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
667
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
668
|
+
if (node.kind === ts.SyntaxKind.UndefinedKeyword) return void 0;
|
|
669
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
670
|
+
return node.elements.map((el) => extractValue(ts, el));
|
|
671
|
+
}
|
|
672
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
673
|
+
return extractObjectLiteral(ts, node);
|
|
674
|
+
}
|
|
675
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken) {
|
|
676
|
+
const operand = extractValue(ts, node.operand);
|
|
677
|
+
if (typeof operand === "number") return -operand;
|
|
678
|
+
}
|
|
679
|
+
return void 0;
|
|
680
|
+
}
|
|
681
|
+
var init_pages = __esm({
|
|
682
|
+
"src/extract/pages.ts"() {
|
|
683
|
+
"use strict";
|
|
684
|
+
init_config();
|
|
685
|
+
init_errors();
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// src/vite/entry.ts
|
|
690
|
+
import { join as join6 } from "path";
|
|
691
|
+
function generateEntrySource(options) {
|
|
692
|
+
const { config, pages, pagesDir, funnelTsxPath, isDev } = options;
|
|
693
|
+
const pageKeys = Object.keys(pages);
|
|
694
|
+
const mergedPages = {};
|
|
695
|
+
const mergedRoutes = {};
|
|
696
|
+
for (const [key, def] of Object.entries(pages)) {
|
|
697
|
+
mergedPages[key] = {
|
|
698
|
+
name: def.name || key,
|
|
699
|
+
type: def.type || "default",
|
|
700
|
+
slug: def.slug || key
|
|
701
|
+
};
|
|
702
|
+
if (def.routes) {
|
|
703
|
+
mergedRoutes[key] = def.routes;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const fullConfig = {
|
|
707
|
+
...config,
|
|
708
|
+
pages: { ...config.pages, ...mergedPages },
|
|
709
|
+
routes: { ...config.routes, ...mergedRoutes }
|
|
710
|
+
};
|
|
711
|
+
const pageImports = pageKeys.map(
|
|
712
|
+
(key) => ` '${key}': lazy(() => import('${join6(pagesDir, key + ".tsx").replace(/\\/g, "/")}')),`
|
|
713
|
+
).join("\n");
|
|
714
|
+
const slugMap = {};
|
|
715
|
+
for (const [key, def] of Object.entries(pages)) {
|
|
716
|
+
slugMap[def.slug || key] = key;
|
|
717
|
+
}
|
|
718
|
+
const trackingCode = isDev ? `
|
|
719
|
+
// Dev mode: mock tracking \u2014 log events to console
|
|
720
|
+
const originalFetch = globalThis.fetch;
|
|
721
|
+
globalThis.__APPFUNNEL_DEV__ = true;
|
|
722
|
+
` : "";
|
|
723
|
+
return `
|
|
724
|
+
import { StrictMode, lazy, Suspense, useState, useEffect, useCallback } from 'react'
|
|
725
|
+
import { createRoot } from 'react-dom/client'
|
|
726
|
+
import { FunnelProvider } from '@appfunnel/sdk/internal'
|
|
727
|
+
import FunnelWrapper from '${funnelTsxPath.replace(/\\/g, "/")}'
|
|
728
|
+
|
|
729
|
+
${trackingCode}
|
|
730
|
+
|
|
731
|
+
const pages = {
|
|
732
|
+
${pageImports}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const config = ${JSON.stringify(fullConfig, null, 2)}
|
|
736
|
+
|
|
737
|
+
const slugToKey = ${JSON.stringify(slugMap)}
|
|
738
|
+
const keyToSlug = ${JSON.stringify(
|
|
739
|
+
Object.fromEntries(Object.entries(slugMap).map(([s, k]) => [k, s]))
|
|
740
|
+
)}
|
|
741
|
+
|
|
742
|
+
function getInitialPage() {
|
|
743
|
+
const path = window.location.pathname.split('/').filter(Boolean).pop() || ''
|
|
744
|
+
return slugToKey[path] || '${config.initialPage || pageKeys[0] || "index"}'
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function App() {
|
|
748
|
+
const [currentPage, setCurrentPage] = useState(getInitialPage)
|
|
749
|
+
|
|
750
|
+
useEffect(() => {
|
|
751
|
+
const handlePopState = () => {
|
|
752
|
+
const path = window.location.pathname.split('/').filter(Boolean).pop() || ''
|
|
753
|
+
const pageKey = slugToKey[path]
|
|
754
|
+
if (pageKey && pageKey !== currentPage) {
|
|
755
|
+
setCurrentPage(pageKey)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
window.addEventListener('popstate', handlePopState)
|
|
759
|
+
return () => window.removeEventListener('popstate', handlePopState)
|
|
760
|
+
}, [currentPage])
|
|
761
|
+
|
|
762
|
+
// Expose navigation to FunnelProvider's router
|
|
763
|
+
useEffect(() => {
|
|
764
|
+
window.__APPFUNNEL_NAVIGATE__ = (pageKey) => {
|
|
765
|
+
setCurrentPage(pageKey)
|
|
766
|
+
const slug = keyToSlug[pageKey] || pageKey
|
|
767
|
+
window.history.pushState(null, '', '/' + slug)
|
|
768
|
+
}
|
|
769
|
+
return () => { delete window.__APPFUNNEL_NAVIGATE__ }
|
|
770
|
+
}, [])
|
|
771
|
+
|
|
772
|
+
const PageComponent = pages[currentPage]
|
|
773
|
+
|
|
774
|
+
if (!PageComponent) {
|
|
775
|
+
return <div style={{ padding: '2rem', color: 'red' }}>Page not found: {currentPage}</div>
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return (
|
|
779
|
+
<FunnelProvider
|
|
780
|
+
config={config}
|
|
781
|
+
initialPage={currentPage}
|
|
782
|
+
apiBaseUrl={${isDev ? "''" : "config.settings?.apiBaseUrl || ''"}}
|
|
783
|
+
>
|
|
784
|
+
<FunnelWrapper>
|
|
785
|
+
<Suspense fallback={null}>
|
|
786
|
+
<PageComponent />
|
|
787
|
+
</Suspense>
|
|
788
|
+
</FunnelWrapper>
|
|
789
|
+
</FunnelProvider>
|
|
790
|
+
)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
createRoot(document.getElementById('root')).render(
|
|
794
|
+
<StrictMode>
|
|
795
|
+
<App />
|
|
796
|
+
</StrictMode>
|
|
797
|
+
)
|
|
798
|
+
`;
|
|
799
|
+
}
|
|
800
|
+
var init_entry = __esm({
|
|
801
|
+
"src/vite/entry.ts"() {
|
|
802
|
+
"use strict";
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// src/vite/html.ts
|
|
807
|
+
function generateHtml(title = "AppFunnel") {
|
|
808
|
+
return `<!DOCTYPE html>
|
|
809
|
+
<html lang="en">
|
|
810
|
+
<head>
|
|
811
|
+
<meta charset="UTF-8" />
|
|
812
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
813
|
+
<title>${title}</title>
|
|
814
|
+
</head>
|
|
815
|
+
<body>
|
|
816
|
+
<div id="root"></div>
|
|
817
|
+
<script type="module" src="/@appfunnel/entry"></script>
|
|
818
|
+
</body>
|
|
819
|
+
</html>`;
|
|
820
|
+
}
|
|
821
|
+
var init_html = __esm({
|
|
822
|
+
"src/vite/html.ts"() {
|
|
823
|
+
"use strict";
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
// src/vite/plugin.ts
|
|
828
|
+
import { resolve as resolve2, join as join7 } from "path";
|
|
829
|
+
import { existsSync as existsSync4 } from "fs";
|
|
830
|
+
function appfunnelPlugin(options) {
|
|
831
|
+
const { cwd, config, isDev } = options;
|
|
832
|
+
let pages = options.pages;
|
|
833
|
+
const pagesDir = resolve2(cwd, "src", "pages");
|
|
834
|
+
const funnelTsxPath = resolve2(cwd, "src", "funnel.tsx");
|
|
835
|
+
let server;
|
|
836
|
+
function getEntrySource() {
|
|
837
|
+
return generateEntrySource({
|
|
838
|
+
config,
|
|
839
|
+
pages,
|
|
840
|
+
pagesDir,
|
|
841
|
+
funnelTsxPath,
|
|
842
|
+
isDev
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
name: "appfunnel",
|
|
847
|
+
config() {
|
|
848
|
+
return {
|
|
849
|
+
resolve: {
|
|
850
|
+
alias: {
|
|
851
|
+
"@": resolve2(cwd, "src")
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
// Ensure we can import .tsx files
|
|
855
|
+
esbuild: {
|
|
856
|
+
jsx: "automatic"
|
|
857
|
+
},
|
|
858
|
+
optimizeDeps: {
|
|
859
|
+
include: ["react", "react-dom", "react/jsx-runtime"]
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
},
|
|
863
|
+
resolveId(id) {
|
|
864
|
+
if (id === VIRTUAL_ENTRY_ID) {
|
|
865
|
+
return RESOLVED_VIRTUAL_ENTRY_ID;
|
|
866
|
+
}
|
|
867
|
+
return null;
|
|
868
|
+
},
|
|
869
|
+
load(id) {
|
|
870
|
+
if (id === RESOLVED_VIRTUAL_ENTRY_ID) {
|
|
871
|
+
return getEntrySource();
|
|
872
|
+
}
|
|
873
|
+
return null;
|
|
874
|
+
},
|
|
875
|
+
configureServer(devServer) {
|
|
876
|
+
server = devServer;
|
|
877
|
+
const watcher = devServer.watcher;
|
|
878
|
+
watcher.add(pagesDir);
|
|
879
|
+
const handlePagesChange = async () => {
|
|
880
|
+
if (options.onPagesChange) {
|
|
881
|
+
await options.onPagesChange();
|
|
882
|
+
}
|
|
883
|
+
const mod = devServer.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ENTRY_ID);
|
|
884
|
+
if (mod) {
|
|
885
|
+
devServer.moduleGraph.invalidateModule(mod);
|
|
886
|
+
}
|
|
887
|
+
devServer.ws.send({ type: "full-reload" });
|
|
888
|
+
};
|
|
889
|
+
watcher.on("add", (file) => {
|
|
890
|
+
if (file.startsWith(pagesDir) && file.endsWith(".tsx")) {
|
|
891
|
+
handlePagesChange();
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
watcher.on("unlink", (file) => {
|
|
895
|
+
if (file.startsWith(pagesDir) && file.endsWith(".tsx")) {
|
|
896
|
+
handlePagesChange();
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
const configPath = join7(cwd, "appfunnel.config.ts");
|
|
900
|
+
if (existsSync4(configPath)) {
|
|
901
|
+
watcher.add(configPath);
|
|
902
|
+
watcher.on("change", (file) => {
|
|
903
|
+
if (file === configPath) {
|
|
904
|
+
devServer.ws.send({ type: "full-reload" });
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return () => {
|
|
909
|
+
devServer.middlewares.use((req, res, next) => {
|
|
910
|
+
if (req.url?.startsWith("/@") || req.url?.startsWith("/node_modules") || req.url?.startsWith("/src") || req.url?.includes(".")) {
|
|
911
|
+
return next();
|
|
912
|
+
}
|
|
913
|
+
const html = generateHtml(config.name || "AppFunnel");
|
|
914
|
+
devServer.transformIndexHtml(req.url || "/", html).then((transformed) => {
|
|
915
|
+
res.statusCode = 200;
|
|
916
|
+
res.setHeader("Content-Type", "text/html");
|
|
917
|
+
res.end(transformed);
|
|
918
|
+
}).catch(next);
|
|
919
|
+
});
|
|
920
|
+
};
|
|
921
|
+
},
|
|
922
|
+
// For production build: inject the HTML as the input
|
|
923
|
+
transformIndexHtml(html) {
|
|
924
|
+
return html;
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
var VIRTUAL_ENTRY_ID, RESOLVED_VIRTUAL_ENTRY_ID;
|
|
929
|
+
var init_plugin = __esm({
|
|
930
|
+
"src/vite/plugin.ts"() {
|
|
931
|
+
"use strict";
|
|
932
|
+
init_entry();
|
|
933
|
+
init_html();
|
|
934
|
+
VIRTUAL_ENTRY_ID = "@appfunnel/entry";
|
|
935
|
+
RESOLVED_VIRTUAL_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID;
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// src/commands/dev.ts
|
|
940
|
+
var dev_exports = {};
|
|
941
|
+
__export(dev_exports, {
|
|
942
|
+
devCommand: () => devCommand
|
|
943
|
+
});
|
|
944
|
+
import pc5 from "picocolors";
|
|
945
|
+
async function devCommand(options) {
|
|
946
|
+
const cwd = process.cwd();
|
|
947
|
+
const port = options.port || 5173;
|
|
948
|
+
requireAuth();
|
|
949
|
+
checkVersionCompatibility(cwd);
|
|
950
|
+
const s = spinner("Loading config...");
|
|
951
|
+
const config = await loadConfig(cwd);
|
|
952
|
+
let pageKeys = scanPages(cwd);
|
|
953
|
+
let pages = await extractPageDefinitions(cwd, pageKeys);
|
|
954
|
+
s.stop();
|
|
955
|
+
info(`Found ${pageKeys.length} pages: ${pageKeys.join(", ")}`);
|
|
956
|
+
const { createServer: createServer2 } = await import("vite");
|
|
957
|
+
const react = await import("@vitejs/plugin-react");
|
|
958
|
+
const server = await createServer2({
|
|
959
|
+
root: cwd,
|
|
960
|
+
server: {
|
|
961
|
+
port,
|
|
962
|
+
strictPort: false
|
|
963
|
+
},
|
|
964
|
+
plugins: [
|
|
965
|
+
react.default(),
|
|
966
|
+
appfunnelPlugin({
|
|
967
|
+
cwd,
|
|
968
|
+
config,
|
|
969
|
+
pages,
|
|
970
|
+
isDev: true,
|
|
971
|
+
async onPagesChange() {
|
|
972
|
+
pageKeys = scanPages(cwd);
|
|
973
|
+
pages = await extractPageDefinitions(cwd, pageKeys);
|
|
974
|
+
info(`Pages updated: ${pageKeys.join(", ")}`);
|
|
975
|
+
}
|
|
976
|
+
})
|
|
977
|
+
],
|
|
978
|
+
css: {
|
|
979
|
+
postcss: cwd
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
await server.listen();
|
|
983
|
+
const address = server.resolvedUrls?.local?.[0] || `http://localhost:${port}`;
|
|
984
|
+
console.log();
|
|
985
|
+
console.log(` ${pc5.bold(config.name || "AppFunnel")} dev server`);
|
|
986
|
+
console.log();
|
|
987
|
+
console.log(` ${pc5.dim("Local:")} ${pc5.cyan(address)}`);
|
|
988
|
+
console.log(` ${pc5.dim("Pages:")} ${pageKeys.length}`);
|
|
989
|
+
console.log(` ${pc5.dim("Tracking:")} ${pc5.yellow("mocked (console)")}`);
|
|
990
|
+
console.log();
|
|
991
|
+
console.log(` ${pc5.dim("Press")} ${pc5.bold("Ctrl+C")} ${pc5.dim("to stop")}`);
|
|
992
|
+
console.log();
|
|
993
|
+
}
|
|
994
|
+
var init_dev = __esm({
|
|
995
|
+
"src/commands/dev.ts"() {
|
|
996
|
+
"use strict";
|
|
997
|
+
init_logger();
|
|
998
|
+
init_auth();
|
|
999
|
+
init_config();
|
|
1000
|
+
init_version();
|
|
1001
|
+
init_pages();
|
|
1002
|
+
init_plugin();
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
// src/commands/build.ts
|
|
1007
|
+
var build_exports = {};
|
|
1008
|
+
__export(build_exports, {
|
|
1009
|
+
buildCommand: () => buildCommand
|
|
1010
|
+
});
|
|
1011
|
+
import { resolve as resolve3, join as join8 } from "path";
|
|
1012
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, statSync, readdirSync as readdirSync2 } from "fs";
|
|
1013
|
+
import pc6 from "picocolors";
|
|
1014
|
+
async function buildCommand() {
|
|
1015
|
+
const cwd = process.cwd();
|
|
1016
|
+
requireAuth();
|
|
1017
|
+
checkVersionCompatibility(cwd);
|
|
1018
|
+
const s = spinner("Analyzing pages...");
|
|
1019
|
+
const config = await loadConfig(cwd);
|
|
1020
|
+
const pageKeys = scanPages(cwd);
|
|
1021
|
+
const pages = await extractPageDefinitions(cwd, pageKeys);
|
|
1022
|
+
s.stop();
|
|
1023
|
+
validateRoutes(config, pages, pageKeys);
|
|
1024
|
+
info(`Building ${pageKeys.length} pages...`);
|
|
1025
|
+
const outDir = resolve3(cwd, ".appfunnel");
|
|
1026
|
+
const { build } = await import("vite");
|
|
1027
|
+
const react = await import("@vitejs/plugin-react");
|
|
1028
|
+
const htmlPath = resolve3(cwd, "index.html");
|
|
1029
|
+
const htmlContent = generateHtml(config.name || "AppFunnel");
|
|
1030
|
+
writeFileSync3(htmlPath, htmlContent);
|
|
1031
|
+
try {
|
|
1032
|
+
await build({
|
|
1033
|
+
root: cwd,
|
|
1034
|
+
build: {
|
|
1035
|
+
outDir,
|
|
1036
|
+
emptyOutDir: true,
|
|
1037
|
+
sourcemap: false,
|
|
1038
|
+
minify: "esbuild",
|
|
1039
|
+
rollupOptions: {
|
|
1040
|
+
output: {
|
|
1041
|
+
manualChunks(id) {
|
|
1042
|
+
if (id.includes("node_modules/react")) {
|
|
1043
|
+
return "vendor-react";
|
|
1044
|
+
}
|
|
1045
|
+
if (id.includes("@appfunnel/sdk")) {
|
|
1046
|
+
return "vendor-sdk";
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
plugins: [
|
|
1053
|
+
react.default(),
|
|
1054
|
+
appfunnelPlugin({
|
|
1055
|
+
cwd,
|
|
1056
|
+
config,
|
|
1057
|
+
pages,
|
|
1058
|
+
isDev: false
|
|
1059
|
+
})
|
|
1060
|
+
],
|
|
1061
|
+
css: {
|
|
1062
|
+
postcss: cwd
|
|
1063
|
+
},
|
|
1064
|
+
logLevel: "warn"
|
|
1065
|
+
});
|
|
1066
|
+
} finally {
|
|
1067
|
+
try {
|
|
1068
|
+
const { unlinkSync } = await import("fs");
|
|
1069
|
+
unlinkSync(htmlPath);
|
|
1070
|
+
} catch {
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const mergedPages = {};
|
|
1074
|
+
const mergedRoutes = {};
|
|
1075
|
+
for (const [key, def] of Object.entries(pages)) {
|
|
1076
|
+
mergedPages[key] = {
|
|
1077
|
+
name: def.name || key,
|
|
1078
|
+
type: def.type || "default",
|
|
1079
|
+
slug: def.slug || key
|
|
1080
|
+
};
|
|
1081
|
+
if (def.routes) {
|
|
1082
|
+
mergedRoutes[key] = def.routes;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const assets = collectAssets(outDir);
|
|
1086
|
+
const totalSize = assets.reduce((sum, a) => sum + a.size, 0);
|
|
1087
|
+
const manifest = {
|
|
1088
|
+
version: 1,
|
|
1089
|
+
sdkVersion: getSdkVersion2(cwd),
|
|
1090
|
+
projectId: config.projectId,
|
|
1091
|
+
funnelId: config.funnelId,
|
|
1092
|
+
pages: { ...config.pages, ...mergedPages },
|
|
1093
|
+
routes: { ...config.routes, ...mergedRoutes },
|
|
1094
|
+
variables: config.variables,
|
|
1095
|
+
queryParams: config.queryParams || [],
|
|
1096
|
+
defaultLocale: config.defaultLocale,
|
|
1097
|
+
assets,
|
|
1098
|
+
totalSize
|
|
1099
|
+
};
|
|
1100
|
+
writeFileSync3(join8(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
1101
|
+
console.log();
|
|
1102
|
+
success("Build complete");
|
|
1103
|
+
console.log();
|
|
1104
|
+
console.log(` ${pc6.dim("Output:")} .appfunnel/`);
|
|
1105
|
+
console.log(` ${pc6.dim("Pages:")} ${pageKeys.length}`);
|
|
1106
|
+
console.log(` ${pc6.dim("Size:")} ${formatSize(totalSize)}`);
|
|
1107
|
+
console.log();
|
|
1108
|
+
for (const asset of assets.filter((a) => a.path.endsWith(".js"))) {
|
|
1109
|
+
const sizeStr = formatSize(asset.size);
|
|
1110
|
+
const isOver = asset.size > MAX_PAGE_SIZE;
|
|
1111
|
+
console.log(` ${isOver ? pc6.yellow("!") : pc6.dim("\xB7")} ${pc6.dim(asset.path)} ${isOver ? pc6.yellow(sizeStr) : pc6.dim(sizeStr)}`);
|
|
1112
|
+
}
|
|
1113
|
+
console.log();
|
|
1114
|
+
if (totalSize > MAX_TOTAL_SIZE) {
|
|
1115
|
+
console.log(formatWarning(
|
|
1116
|
+
"BUNDLE_TOO_LARGE",
|
|
1117
|
+
`Total bundle size (${formatSize(totalSize)}) exceeds the 2MB limit.`,
|
|
1118
|
+
"This will be rejected on publish. Reduce dependencies or code-split."
|
|
1119
|
+
));
|
|
1120
|
+
console.log();
|
|
1121
|
+
}
|
|
1122
|
+
for (const asset of assets) {
|
|
1123
|
+
if (asset.size > MAX_PAGE_SIZE && asset.path.endsWith(".js")) {
|
|
1124
|
+
console.log(formatWarning(
|
|
1125
|
+
"PAGE_SIZE",
|
|
1126
|
+
`${asset.path} is ${formatSize(asset.size)} (limit: 500KB).`,
|
|
1127
|
+
"Consider reducing dependencies in this page."
|
|
1128
|
+
));
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function validateRoutes(config, pages, pageKeys) {
|
|
1133
|
+
const allPageKeys = new Set(pageKeys);
|
|
1134
|
+
const allVariables = new Set(Object.keys(config.variables));
|
|
1135
|
+
for (const [pageKey, def] of Object.entries(pages)) {
|
|
1136
|
+
if (!def.routes) continue;
|
|
1137
|
+
for (const route of def.routes) {
|
|
1138
|
+
if (!allPageKeys.has(route.to)) {
|
|
1139
|
+
throw new CLIError(
|
|
1140
|
+
"INVALID_ROUTE",
|
|
1141
|
+
`Page "${pageKey}" routes to "${route.to}" which does not exist.`,
|
|
1142
|
+
`Available pages: ${pageKeys.join(", ")}. Check src/pages/${pageKey}.tsx.`
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
if (route.when) {
|
|
1146
|
+
validateConditionVariables(route.when, pageKey, allVariables);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function validateConditionVariables(condition, pageKey, allVariables) {
|
|
1152
|
+
if (!condition || typeof condition !== "object") return;
|
|
1153
|
+
const cond = condition;
|
|
1154
|
+
if (cond.variable && typeof cond.variable === "string") {
|
|
1155
|
+
const prefix = cond.variable.split(".")[0];
|
|
1156
|
+
const systemPrefixes = ["page", "device", "browser", "os", "session", "system", "metadata", "user", "query", "products", "card", "payment", "stripe", "subscription"];
|
|
1157
|
+
if (!systemPrefixes.includes(prefix) && !allVariables.has(cond.variable)) {
|
|
1158
|
+
throw new CLIError(
|
|
1159
|
+
"UNDEFINED_VARIABLE",
|
|
1160
|
+
`Route condition in "${pageKey}" references variable "${cond.variable}" which is not defined.`,
|
|
1161
|
+
`Add it to variables in appfunnel.config.ts: '${cond.variable}': { type: 'string' }`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
if (Array.isArray(cond.rules)) {
|
|
1166
|
+
for (const rule of cond.rules) {
|
|
1167
|
+
validateConditionVariables(rule, pageKey, allVariables);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function collectAssets(outDir) {
|
|
1172
|
+
const assets = [];
|
|
1173
|
+
function walk(dir, prefix = "") {
|
|
1174
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
1175
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1176
|
+
const fullPath = join8(dir, entry.name);
|
|
1177
|
+
if (entry.isDirectory()) {
|
|
1178
|
+
walk(fullPath, relPath);
|
|
1179
|
+
} else if (entry.name !== "manifest.json" && !entry.name.startsWith(".")) {
|
|
1180
|
+
assets.push({ path: relPath, size: statSync(fullPath).size });
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
walk(outDir);
|
|
1185
|
+
return assets;
|
|
1186
|
+
}
|
|
1187
|
+
function formatSize(bytes) {
|
|
1188
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1189
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1190
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
1191
|
+
}
|
|
1192
|
+
function getSdkVersion2(cwd) {
|
|
1193
|
+
try {
|
|
1194
|
+
const pkg = JSON.parse(readFileSync6(join8(cwd, "node_modules", "@appfunnel", "sdk", "package.json"), "utf-8"));
|
|
1195
|
+
return pkg.version;
|
|
1196
|
+
} catch {
|
|
1197
|
+
return "0.0.0";
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
var MAX_TOTAL_SIZE, MAX_PAGE_SIZE;
|
|
1201
|
+
var init_build = __esm({
|
|
1202
|
+
"src/commands/build.ts"() {
|
|
1203
|
+
"use strict";
|
|
1204
|
+
init_logger();
|
|
1205
|
+
init_auth();
|
|
1206
|
+
init_config();
|
|
1207
|
+
init_version();
|
|
1208
|
+
init_pages();
|
|
1209
|
+
init_plugin();
|
|
1210
|
+
init_html();
|
|
1211
|
+
init_errors();
|
|
1212
|
+
init_errors();
|
|
1213
|
+
MAX_TOTAL_SIZE = 2 * 1024 * 1024;
|
|
1214
|
+
MAX_PAGE_SIZE = 500 * 1024;
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
// src/lib/api.ts
|
|
1219
|
+
async function apiFetch(path, options) {
|
|
1220
|
+
const { token, apiBaseUrl, ...fetchOpts } = options;
|
|
1221
|
+
const base = apiBaseUrl || DEFAULT_API_BASE3;
|
|
1222
|
+
const url = `${base}${path}`;
|
|
1223
|
+
const isFormData = fetchOpts.body instanceof FormData;
|
|
1224
|
+
const headers = {
|
|
1225
|
+
Authorization: token,
|
|
1226
|
+
...fetchOpts.headers || {}
|
|
1227
|
+
};
|
|
1228
|
+
if (!isFormData) {
|
|
1229
|
+
headers["Content-Type"] = "application/json";
|
|
1230
|
+
}
|
|
1231
|
+
const response = await fetch(url, {
|
|
1232
|
+
...fetchOpts,
|
|
1233
|
+
headers
|
|
1234
|
+
});
|
|
1235
|
+
if (!response.ok) {
|
|
1236
|
+
const body = await response.text().catch(() => "");
|
|
1237
|
+
let message = `API request failed: ${response.status} ${response.statusText}`;
|
|
1238
|
+
try {
|
|
1239
|
+
const parsed = JSON.parse(body);
|
|
1240
|
+
if (parsed.error) message = parsed.error;
|
|
1241
|
+
if (parsed.message) message = parsed.message;
|
|
1242
|
+
} catch {
|
|
1243
|
+
}
|
|
1244
|
+
const error2 = new CLIError("API_ERROR", message);
|
|
1245
|
+
error2.statusCode = response.status;
|
|
1246
|
+
throw error2;
|
|
1247
|
+
}
|
|
1248
|
+
return response;
|
|
1249
|
+
}
|
|
1250
|
+
async function publishBuild(projectId, funnelId, manifest, assets, options) {
|
|
1251
|
+
const formData = new FormData();
|
|
1252
|
+
formData.set("manifest", JSON.stringify(manifest));
|
|
1253
|
+
formData.set("funnelId", funnelId);
|
|
1254
|
+
for (const asset of assets) {
|
|
1255
|
+
formData.append(
|
|
1256
|
+
"assets",
|
|
1257
|
+
new Blob([new Uint8Array(asset.content)], { type: asset.contentType }),
|
|
1258
|
+
asset.path
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
try {
|
|
1262
|
+
const response = await apiFetch(`/project/${projectId}/headless/publish`, {
|
|
1263
|
+
...options,
|
|
1264
|
+
method: "POST",
|
|
1265
|
+
body: formData
|
|
1266
|
+
});
|
|
1267
|
+
return await response.json();
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
if (err instanceof CLIError && err.code === "API_ERROR") {
|
|
1270
|
+
if (err.statusCode === 413) {
|
|
1271
|
+
throw new CLIError(
|
|
1272
|
+
"BUNDLE_TOO_LARGE",
|
|
1273
|
+
err.message,
|
|
1274
|
+
"Reduce page bundle sizes. Check for large dependencies."
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
if (err.statusCode === 409) {
|
|
1278
|
+
throw new CLIError(
|
|
1279
|
+
"FUNNEL_NOT_HEADLESS",
|
|
1280
|
+
err.message,
|
|
1281
|
+
"Create a new headless funnel from the dashboard, or remove funnelId from config."
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
throw new CLIError("PUBLISH_FAILED", err.message);
|
|
1285
|
+
}
|
|
1286
|
+
throw err;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
var DEFAULT_API_BASE3;
|
|
1290
|
+
var init_api = __esm({
|
|
1291
|
+
"src/lib/api.ts"() {
|
|
1292
|
+
"use strict";
|
|
1293
|
+
init_errors();
|
|
1294
|
+
DEFAULT_API_BASE3 = "https://api.appfunnel.net";
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
// src/commands/publish.ts
|
|
1299
|
+
var publish_exports = {};
|
|
1300
|
+
__export(publish_exports, {
|
|
1301
|
+
publishCommand: () => publishCommand
|
|
1302
|
+
});
|
|
1303
|
+
import { resolve as resolve4, join as join9 } from "path";
|
|
1304
|
+
import { readFileSync as readFileSync7, existsSync as existsSync5 } from "fs";
|
|
1305
|
+
import pc7 from "picocolors";
|
|
1306
|
+
function getMimeType(path) {
|
|
1307
|
+
const ext = path.substring(path.lastIndexOf("."));
|
|
1308
|
+
return MIME_TYPES[ext] || "application/octet-stream";
|
|
1309
|
+
}
|
|
1310
|
+
async function publishCommand() {
|
|
1311
|
+
const cwd = process.cwd();
|
|
1312
|
+
const creds = requireAuth();
|
|
1313
|
+
checkVersionCompatibility(cwd);
|
|
1314
|
+
const config = await loadConfig(cwd);
|
|
1315
|
+
const outDir = resolve4(cwd, ".appfunnel");
|
|
1316
|
+
const manifestPath = join9(outDir, "manifest.json");
|
|
1317
|
+
if (!existsSync5(manifestPath)) {
|
|
1318
|
+
throw new CLIError(
|
|
1319
|
+
"BUILD_NOT_FOUND",
|
|
1320
|
+
"No build output found.",
|
|
1321
|
+
"Run 'appfunnel build' first."
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
const manifest = JSON.parse(readFileSync7(manifestPath, "utf-8"));
|
|
1325
|
+
const assets = manifest.assets || [];
|
|
1326
|
+
const s = spinner("Uploading build...");
|
|
1327
|
+
const assetPayloads = [];
|
|
1328
|
+
for (const asset of assets) {
|
|
1329
|
+
const fullPath = join9(outDir, asset.path);
|
|
1330
|
+
if (!existsSync5(fullPath)) {
|
|
1331
|
+
s.stop();
|
|
1332
|
+
throw new CLIError(
|
|
1333
|
+
"BUILD_NOT_FOUND",
|
|
1334
|
+
`Build asset missing: ${asset.path}`,
|
|
1335
|
+
"Run 'appfunnel build' to regenerate."
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
assetPayloads.push({
|
|
1339
|
+
path: asset.path,
|
|
1340
|
+
content: readFileSync7(fullPath),
|
|
1341
|
+
contentType: getMimeType(asset.path)
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
const projectId = config.projectId;
|
|
1345
|
+
const funnelId = config.funnelId;
|
|
1346
|
+
if (!projectId) {
|
|
1347
|
+
s.stop();
|
|
1348
|
+
throw new CLIError(
|
|
1349
|
+
"CONFIG_NOT_FOUND",
|
|
1350
|
+
"No projectId in appfunnel.config.ts.",
|
|
1351
|
+
"Add projectId to your config. You can find it in the dashboard."
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
if (!funnelId) {
|
|
1355
|
+
s.stop();
|
|
1356
|
+
throw new CLIError(
|
|
1357
|
+
"CONFIG_NOT_FOUND",
|
|
1358
|
+
"No funnelId in appfunnel.config.ts.",
|
|
1359
|
+
"Add funnelId to your config, or create a new funnel from the dashboard."
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const result = await publishBuild(
|
|
1363
|
+
projectId,
|
|
1364
|
+
funnelId,
|
|
1365
|
+
manifest,
|
|
1366
|
+
assetPayloads,
|
|
1367
|
+
{ token: creds.token }
|
|
1368
|
+
);
|
|
1369
|
+
s.stop();
|
|
1370
|
+
console.log();
|
|
1371
|
+
success("Published successfully");
|
|
1372
|
+
console.log();
|
|
1373
|
+
console.log(` ${pc7.dim("Build ID:")} ${result.buildId}`);
|
|
1374
|
+
console.log(` ${pc7.dim("URL:")} ${pc7.cyan(result.url)}`);
|
|
1375
|
+
console.log(` ${pc7.dim("Assets:")} ${assets.length} files`);
|
|
1376
|
+
console.log();
|
|
1377
|
+
}
|
|
1378
|
+
var MIME_TYPES;
|
|
1379
|
+
var init_publish = __esm({
|
|
1380
|
+
"src/commands/publish.ts"() {
|
|
1381
|
+
"use strict";
|
|
1382
|
+
init_logger();
|
|
1383
|
+
init_auth();
|
|
1384
|
+
init_config();
|
|
1385
|
+
init_version();
|
|
1386
|
+
init_api();
|
|
1387
|
+
init_errors();
|
|
1388
|
+
MIME_TYPES = {
|
|
1389
|
+
".js": "application/javascript",
|
|
1390
|
+
".css": "text/css",
|
|
1391
|
+
".html": "text/html",
|
|
1392
|
+
".json": "application/json",
|
|
1393
|
+
".svg": "image/svg+xml",
|
|
1394
|
+
".png": "image/png",
|
|
1395
|
+
".jpg": "image/jpeg",
|
|
1396
|
+
".woff2": "font/woff2",
|
|
1397
|
+
".woff": "font/woff"
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
// src/index.ts
|
|
1403
|
+
init_errors();
|
|
1404
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
1405
|
+
import { Command } from "commander";
|
|
1406
|
+
import pc8 from "picocolors";
|
|
1407
|
+
function getCliVersion2() {
|
|
1408
|
+
try {
|
|
1409
|
+
const pkg = JSON.parse(
|
|
1410
|
+
readFileSync8(new URL("../package.json", import.meta.url), "utf-8")
|
|
1411
|
+
);
|
|
1412
|
+
return pkg.version;
|
|
1413
|
+
} catch {
|
|
1414
|
+
return "0.0.0";
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
var program = new Command();
|
|
1418
|
+
program.name("appfunnel").description("Build and publish headless AppFunnel projects").version(getCliVersion2());
|
|
1419
|
+
program.command("init <name>").description("Create a new AppFunnel project").action(async (name) => {
|
|
1420
|
+
const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1421
|
+
await initCommand2(name);
|
|
1422
|
+
});
|
|
1423
|
+
program.command("login").description("Authenticate with AppFunnel").action(async () => {
|
|
1424
|
+
const { loginCommand: loginCommand2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
1425
|
+
await loginCommand2();
|
|
1426
|
+
});
|
|
1427
|
+
program.command("whoami").description("Show the currently authenticated user").action(async () => {
|
|
1428
|
+
const { whoamiCommand: whoamiCommand2 } = await Promise.resolve().then(() => (init_whoami(), whoami_exports));
|
|
1429
|
+
await whoamiCommand2();
|
|
1430
|
+
});
|
|
1431
|
+
program.command("dev").description("Start the development server").option("-p, --port <port>", "Port number", "5173").action(async (options) => {
|
|
1432
|
+
const { devCommand: devCommand2 } = await Promise.resolve().then(() => (init_dev(), dev_exports));
|
|
1433
|
+
await devCommand2({ port: parseInt(options.port, 10) });
|
|
1434
|
+
});
|
|
1435
|
+
program.command("build").description("Build the funnel for production").action(async () => {
|
|
1436
|
+
const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_build(), build_exports));
|
|
1437
|
+
await buildCommand2();
|
|
1438
|
+
});
|
|
1439
|
+
program.command("publish").description("Publish the build to AppFunnel").action(async () => {
|
|
1440
|
+
const { publishCommand: publishCommand2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
|
|
1441
|
+
await publishCommand2();
|
|
1442
|
+
});
|
|
1443
|
+
program.hook("postAction", () => {
|
|
1444
|
+
});
|
|
1445
|
+
async function main() {
|
|
1446
|
+
try {
|
|
1447
|
+
await program.parseAsync(process.argv);
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
if (err instanceof CLIError) {
|
|
1450
|
+
console.error(formatError(err));
|
|
1451
|
+
process.exit(1);
|
|
1452
|
+
}
|
|
1453
|
+
console.error(`${pc8.red("ERROR")}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1454
|
+
if (err instanceof Error && err.stack) {
|
|
1455
|
+
console.error(pc8.dim(err.stack));
|
|
1456
|
+
}
|
|
1457
|
+
process.exit(1);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
main();
|
|
1461
|
+
//# sourceMappingURL=index.js.map
|