@geekmidas/cli 1.10.35 → 1.10.37
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/CHANGELOG.md +20 -0
- package/dist/{config-B62g483e.mjs → config-Bbu1JKvf.mjs} +2 -2
- package/dist/{config-B62g483e.mjs.map → config-Bbu1JKvf.mjs.map} +1 -1
- package/dist/{config-Cuo8vFsp.cjs → config-DujV8DbR.cjs} +2 -2
- package/dist/{config-Cuo8vFsp.cjs.map → config-DujV8DbR.cjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +2 -2
- package/dist/deploy/sniffer-loader.cjs +1 -1
- package/dist/{fullstack-secrets-Bchl2MDd.cjs → fullstack-secrets-BYzi6Hwl.cjs} +3 -2
- package/dist/{fullstack-secrets-Bchl2MDd.cjs.map → fullstack-secrets-BYzi6Hwl.cjs.map} +1 -1
- package/dist/{fullstack-secrets-P84v8zWn.mjs → fullstack-secrets-l8s3II_H.mjs} +3 -2
- package/dist/{fullstack-secrets-P84v8zWn.mjs.map → fullstack-secrets-l8s3II_H.mjs.map} +1 -1
- package/dist/{index-Dt_dZ7K4.d.mts → index-BBvGMG_A.d.mts} +162 -68
- package/dist/index-BBvGMG_A.d.mts.map +1 -0
- package/dist/{index-C3t5VL4R.d.cts → index-Dz2a7xQU.d.cts} +162 -68
- package/dist/index-Dz2a7xQU.d.cts.map +1 -0
- package/dist/index.cjs +1366 -149
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1366 -149
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-B_rJjImN.cjs → openapi-0JUzU5Tv.cjs} +72 -15
- package/dist/openapi-0JUzU5Tv.cjs.map +1 -0
- package/dist/{openapi-CflxypuN.mjs → openapi-C5AycKTR.mjs} +73 -16
- package/dist/openapi-C5AycKTR.mjs.map +1 -0
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +7 -0
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +7 -0
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{reconcile-DAAKa0YR.mjs → reconcile-CYGmqXvq.mjs} +2 -2
- package/dist/{reconcile-DAAKa0YR.mjs.map → reconcile-CYGmqXvq.mjs.map} +1 -1
- package/dist/{reconcile-CZtzdrnc.cjs → reconcile-D93YG7G5.cjs} +2 -2
- package/dist/{reconcile-CZtzdrnc.cjs.map → reconcile-D93YG7G5.cjs.map} +1 -1
- package/dist/workspace/index.cjs +4 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +2 -2
- package/dist/{workspace-CGYykWfn.cjs → workspace-CQTT7HJQ.cjs} +107 -27
- package/dist/workspace-CQTT7HJQ.cjs.map +1 -0
- package/dist/{workspace-Bi4X7Yzy.mjs → workspace-D-37Ylqo.mjs} +90 -28
- package/dist/workspace-D-37Ylqo.mjs.map +1 -0
- package/docs/dev-server-resilience-design.md +189 -0
- package/package.json +4 -4
- package/src/__tests__/openapi.spec.ts +99 -9
- package/src/build/__tests__/workspace-build.spec.ts +7 -7
- package/src/build/index.ts +25 -9
- package/src/credentials/__tests__/helpers.ts +1 -1
- package/src/credentials/__tests__/workspaceCredentials.spec.ts +1 -1
- package/src/deploy/__tests__/domain.spec.ts +12 -12
- package/src/deploy/__tests__/env-resolver.spec.ts +29 -1
- package/src/deploy/__tests__/index.spec.ts +14 -14
- package/src/deploy/__tests__/sniffer.spec.ts +9 -9
- package/src/deploy/domain.ts +19 -11
- package/src/deploy/env-resolver.ts +5 -10
- package/src/deploy/index.ts +25 -8
- package/src/deploy/sniffer.ts +11 -5
- package/src/deploy/types.ts +1 -1
- package/src/dev/__tests__/index.spec.ts +11 -11
- package/src/dev/index.ts +43 -9
- package/src/docker/__tests__/compose.spec.ts +4 -4
- package/src/docker/compose.ts +5 -3
- package/src/docker/index.ts +34 -6
- package/src/docker/templates.ts +138 -0
- package/src/generators/OpenApiTsGenerator.ts +22 -0
- package/src/generators/__tests__/OpenApiTsGenerator.registry.spec.ts +77 -0
- package/src/index.ts +6 -2
- package/src/init/__tests__/generators.spec.ts +147 -0
- package/src/init/__tests__/init.spec.ts +1 -1
- package/src/init/generators/mobile-expo.ts +576 -0
- package/src/init/generators/monorepo.ts +42 -11
- package/src/init/generators/web-tanstack.ts +348 -0
- package/src/init/index.ts +40 -3
- package/src/init/templates/index.ts +31 -0
- package/src/init/versions.ts +2 -2
- package/src/openapi.ts +110 -23
- package/src/secrets/__tests__/reconcile.spec.ts +1 -1
- package/src/setup/__tests__/reconcile-secrets.spec.ts +2 -2
- package/src/setup/fullstack-secrets.ts +7 -1
- package/src/setup/index.ts +2 -1
- package/src/test/__tests__/__fixtures__/workspace.ts +1 -1
- package/src/test/__tests__/web.spec.ts +2 -2
- package/src/workspace/__tests__/client-generator.spec.ts +6 -6
- package/src/workspace/__tests__/index.spec.ts +96 -4
- package/src/workspace/__tests__/publicEnv.spec.ts +64 -0
- package/src/workspace/__tests__/schema.spec.ts +27 -30
- package/src/workspace/__tests__/type-inference.spec.ts +5 -5
- package/src/workspace/client-generator.ts +10 -7
- package/src/workspace/index.ts +17 -3
- package/src/workspace/publicEnv.ts +57 -0
- package/src/workspace/schema.ts +126 -27
- package/src/workspace/types.ts +40 -64
- package/dist/index-C3t5VL4R.d.cts.map +0 -1
- package/dist/index-Dt_dZ7K4.d.mts.map +0 -1
- package/dist/openapi-B_rJjImN.cjs.map +0 -1
- package/dist/openapi-CflxypuN.mjs.map +0 -1
- package/dist/workspace-Bi4X7Yzy.mjs.map +0 -1
- package/dist/workspace-CGYykWfn.cjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
import { __require } from "./chunk-Duj1WY3L.mjs";
|
|
3
|
-
import { getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, isDeployTargetSupported } from "./workspace-
|
|
4
|
-
import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig } from "./config-
|
|
3
|
+
import { getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, getPublicEnvPrefix, isDeployTargetSupported, stripPublicPrefix } from "./workspace-D-37Ylqo.mjs";
|
|
4
|
+
import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig } from "./config-Bbu1JKvf.mjs";
|
|
5
5
|
import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId } from "./credentials-s1kLcIzK.mjs";
|
|
6
|
-
import { createStageSecrets, generateConnectionUrls, generateDbPassword, generateDbUrl, generateFullstackCustomSecrets, generateLocalStackCredentials, generatePgBossUrl, generateSecurePassword, generateServiceCredentials, rotateServicePassword, writeDockerEnvFromSecrets } from "./fullstack-secrets-
|
|
6
|
+
import { createStageSecrets, generateConnectionUrls, generateDbPassword, generateDbUrl, generateFullstackCustomSecrets, generateLocalStackCredentials, generatePgBossUrl, generateSecurePassword, generateServiceCredentials, rotateServicePassword, writeDockerEnvFromSecrets } from "./fullstack-secrets-l8s3II_H.mjs";
|
|
7
7
|
import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-dbb9RyBl.mjs";
|
|
8
|
-
import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, isPartitionedRoutes, normalizeRoutes, openapiCommand, resolveOpenApiConfig } from "./openapi-
|
|
8
|
+
import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, isPartitionedRoutes, normalizeRoutes, openapiCommand, resolveOpenApiConfig } from "./openapi-C5AycKTR.mjs";
|
|
9
9
|
import { DokployApi } from "./dokploy-api-BIT0683o.mjs";
|
|
10
10
|
import { encryptSecrets } from "./encryption-DSkIlEZ6.mjs";
|
|
11
11
|
import { CachedStateProvider } from "./CachedStateProvider-C8A-cCm5.mjs";
|
|
@@ -28,14 +28,14 @@ import { randomBytes } from "node:crypto";
|
|
|
28
28
|
import { Cron } from "@geekmidas/constructs/crons";
|
|
29
29
|
import { Function } from "@geekmidas/constructs/functions";
|
|
30
30
|
import { Subscriber } from "@geekmidas/constructs/subscribers";
|
|
31
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
31
32
|
import { Client } from "pg";
|
|
32
33
|
import { lookup } from "node:dns/promises";
|
|
33
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
34
34
|
import prompts from "prompts";
|
|
35
35
|
|
|
36
36
|
//#region package.json
|
|
37
37
|
var name = "@geekmidas/cli";
|
|
38
|
-
var version = "1.10.
|
|
38
|
+
var version = "1.10.36";
|
|
39
39
|
var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
|
|
40
40
|
var private$1 = false;
|
|
41
41
|
var type = "module";
|
|
@@ -1489,6 +1489,27 @@ const FRONTEND_FRAMEWORKS = {
|
|
|
1489
1489
|
],
|
|
1490
1490
|
dependency: "vite",
|
|
1491
1491
|
installCommand: "pnpm add -D vite"
|
|
1492
|
+
},
|
|
1493
|
+
"tanstack-start": {
|
|
1494
|
+
displayName: "TanStack Start",
|
|
1495
|
+
configFiles: [
|
|
1496
|
+
"vite.config.js",
|
|
1497
|
+
"vite.config.ts",
|
|
1498
|
+
"vite.config.mjs",
|
|
1499
|
+
"vite.config.cjs"
|
|
1500
|
+
],
|
|
1501
|
+
dependency: "@tanstack/react-start",
|
|
1502
|
+
installCommand: "pnpm add @tanstack/react-start @tanstack/react-router"
|
|
1503
|
+
},
|
|
1504
|
+
expo: {
|
|
1505
|
+
displayName: "Expo",
|
|
1506
|
+
configFiles: [
|
|
1507
|
+
"app.config.ts",
|
|
1508
|
+
"app.config.js",
|
|
1509
|
+
"app.json"
|
|
1510
|
+
],
|
|
1511
|
+
dependency: "expo",
|
|
1512
|
+
installCommand: "pnpm add expo"
|
|
1492
1513
|
}
|
|
1493
1514
|
};
|
|
1494
1515
|
/**
|
|
@@ -1498,6 +1519,8 @@ const FRONTEND_FRAMEWORKS = {
|
|
|
1498
1519
|
*/
|
|
1499
1520
|
function detectFrontendFramework(fullPath, deps) {
|
|
1500
1521
|
const order = [
|
|
1522
|
+
"expo",
|
|
1523
|
+
"tanstack-start",
|
|
1501
1524
|
"nextjs",
|
|
1502
1525
|
"remix",
|
|
1503
1526
|
"vite"
|
|
@@ -1572,7 +1595,7 @@ async function validateFrontendApp(appName, appPath, workspaceRoot, framework) {
|
|
|
1572
1595
|
*/
|
|
1573
1596
|
async function validateFrontendApps(workspace) {
|
|
1574
1597
|
const results = [];
|
|
1575
|
-
for (const [appName, app] of Object.entries(workspace.apps)) if (app.type === "
|
|
1598
|
+
for (const [appName, app] of Object.entries(workspace.apps)) if (app.type === "web") {
|
|
1576
1599
|
const result = await validateFrontendApp(appName, app.path, workspace.root, app.framework);
|
|
1577
1600
|
results.push(result);
|
|
1578
1601
|
}
|
|
@@ -1608,9 +1631,12 @@ async function loadDevSecrets(workspace) {
|
|
|
1608
1631
|
async function workspaceDevCommand(workspace, options) {
|
|
1609
1632
|
const appCount = Object.keys(workspace.apps).length;
|
|
1610
1633
|
const backendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "backend");
|
|
1611
|
-
const frontendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "
|
|
1634
|
+
const frontendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "web");
|
|
1635
|
+
const mobileApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "mobile");
|
|
1612
1636
|
logger$11.log(`\n🚀 Starting workspace: ${workspace.name}`);
|
|
1613
|
-
|
|
1637
|
+
const counts = [`${backendApps.length} backend`, `${frontendApps.length} web`];
|
|
1638
|
+
if (mobileApps.length > 0) counts.push(`${mobileApps.length} mobile`);
|
|
1639
|
+
logger$11.log(` ${counts.join(", ")} app(s)`);
|
|
1614
1640
|
const conflicts = checkPortConflicts(workspace);
|
|
1615
1641
|
if (conflicts.length > 0) {
|
|
1616
1642
|
for (const conflict of conflicts) logger$11.error(`❌ Port conflict: Apps "${conflict.app1}" and "${conflict.app2}" both use port ${conflict.port}`);
|
|
@@ -1659,7 +1685,8 @@ async function workspaceDevCommand(workspace, options) {
|
|
|
1659
1685
|
const app = workspace.apps[appName];
|
|
1660
1686
|
if (!app) continue;
|
|
1661
1687
|
const deps = app.dependencies.length > 0 ? ` (depends on: ${app.dependencies.join(", ")})` : "";
|
|
1662
|
-
|
|
1688
|
+
const icon = app.type === "backend" ? "🔧" : app.type === "mobile" ? "📱" : "🌐";
|
|
1689
|
+
logger$11.log(` ${icon} ${appName} → http://localhost:${app.port}${deps}`);
|
|
1663
1690
|
}
|
|
1664
1691
|
const configFiles = [
|
|
1665
1692
|
"gkm.config.ts",
|
|
@@ -2376,7 +2403,7 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
2376
2403
|
const results = [];
|
|
2377
2404
|
const apps = Object.entries(workspace.apps);
|
|
2378
2405
|
const backendApps = apps.filter(([, app]) => app.type === "backend");
|
|
2379
|
-
const frontendApps = apps.filter(([, app]) => app.type === "
|
|
2406
|
+
const frontendApps = apps.filter(([, app]) => app.type === "web");
|
|
2380
2407
|
logger$9.log(`\n🏗️ Building workspace: ${workspace.name}`);
|
|
2381
2408
|
logger$9.log(` Backend apps: ${backendApps.map(([name$1]) => name$1).join(", ") || "none"}`);
|
|
2382
2409
|
logger$9.log(` Frontend apps: ${frontendApps.map(([name$1]) => name$1).join(", ") || "none"}`);
|
|
@@ -2419,7 +2446,7 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
2419
2446
|
await openapiCommand({ cwd: workspace.root });
|
|
2420
2447
|
logger$9.log(`\n📋 Build Summary:`);
|
|
2421
2448
|
for (const result of results) {
|
|
2422
|
-
const icon = result.type === "backend" ? "⚙️" : "🌐";
|
|
2449
|
+
const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
|
|
2423
2450
|
logger$9.log(` ${icon} ${result.appName}: ${result.outputPath || "built"}`);
|
|
2424
2451
|
}
|
|
2425
2452
|
} catch (error) {
|
|
@@ -2440,8 +2467,14 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
2440
2467
|
*/
|
|
2441
2468
|
function getAppOutputPath(workspace, _appName, app) {
|
|
2442
2469
|
const appPath = join(workspace.root, app.path);
|
|
2443
|
-
if (app.type === "
|
|
2444
|
-
|
|
2470
|
+
if (app.type === "mobile") return "";
|
|
2471
|
+
if (app.type === "web") switch (app.framework) {
|
|
2472
|
+
case "vite":
|
|
2473
|
+
case "tanstack-start": return join(appPath, "dist");
|
|
2474
|
+
case "remix": return join(appPath, "build");
|
|
2475
|
+
default: return join(appPath, ".next");
|
|
2476
|
+
}
|
|
2477
|
+
return join(appPath, ".gkm");
|
|
2445
2478
|
}
|
|
2446
2479
|
/**
|
|
2447
2480
|
* Format routes for logging, handling PartitionedRoutes.
|
|
@@ -3003,6 +3036,66 @@ async function verifyDnsRecords(appHostnames, serverIp, state) {
|
|
|
3003
3036
|
return results;
|
|
3004
3037
|
}
|
|
3005
3038
|
|
|
3039
|
+
//#endregion
|
|
3040
|
+
//#region src/deploy/domain.ts
|
|
3041
|
+
/**
|
|
3042
|
+
* Resolve the hostname for an app based on stage configuration.
|
|
3043
|
+
*
|
|
3044
|
+
* Domain resolution priority:
|
|
3045
|
+
* 1. Explicit app.domain override (string or stage-specific)
|
|
3046
|
+
* 2. Default pattern based on app type:
|
|
3047
|
+
* - Main frontend app gets base domain (e.g., 'myapp.com')
|
|
3048
|
+
* - Other apps get prefixed domain (e.g., 'api.myapp.com')
|
|
3049
|
+
*
|
|
3050
|
+
* @param appName - The name of the app
|
|
3051
|
+
* @param app - The normalized app configuration
|
|
3052
|
+
* @param stage - The deployment stage (e.g., 'production', 'development')
|
|
3053
|
+
* @param dokployConfig - Dokploy workspace configuration with domain mappings
|
|
3054
|
+
* @param isMainFrontend - Whether this is the main frontend app
|
|
3055
|
+
* @returns The resolved hostname for the app
|
|
3056
|
+
* @throws Error if no domain configuration is found for the stage
|
|
3057
|
+
*/
|
|
3058
|
+
function resolveHost(appName, app, stage, dokployConfig, isMainFrontend) {
|
|
3059
|
+
if (app.domain) {
|
|
3060
|
+
if (typeof app.domain === "string") return app.domain;
|
|
3061
|
+
if (app.domain[stage]) return app.domain[stage];
|
|
3062
|
+
}
|
|
3063
|
+
const baseDomain = dokployConfig?.domains?.[stage];
|
|
3064
|
+
if (!baseDomain) throw new Error(`No domain configured for stage "${stage}". Add deploy.dokploy.domains.${stage} to gkm.config.ts`);
|
|
3065
|
+
if (isMainFrontend) return baseDomain;
|
|
3066
|
+
return `${appName}.${baseDomain}`;
|
|
3067
|
+
}
|
|
3068
|
+
/**
|
|
3069
|
+
* Determine if an app is the "main" frontend (gets base domain).
|
|
3070
|
+
*
|
|
3071
|
+
* An app is considered the main frontend if:
|
|
3072
|
+
* 1. It's named 'web' and is a frontend type
|
|
3073
|
+
* 2. It's the first frontend app in the apps list
|
|
3074
|
+
*
|
|
3075
|
+
* @param appName - The name of the app to check
|
|
3076
|
+
* @param app - The app configuration
|
|
3077
|
+
* @param allApps - All apps in the workspace
|
|
3078
|
+
* @returns True if this is the main frontend app
|
|
3079
|
+
*/
|
|
3080
|
+
function isMainFrontendApp(appName, app, allApps) {
|
|
3081
|
+
if (app.type !== "web") return false;
|
|
3082
|
+
if (appName === "web") return true;
|
|
3083
|
+
for (const [name$1, a] of Object.entries(allApps)) if (a.type === "web") return name$1 === appName;
|
|
3084
|
+
return false;
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Get public URL arg names from app dependencies.
|
|
3088
|
+
*
|
|
3089
|
+
* @param app - The web/mobile app configuration
|
|
3090
|
+
* @returns Array of arg names like 'NEXT_PUBLIC_API_URL', or [] for frameworks
|
|
3091
|
+
* without a public prefix (e.g. Remix).
|
|
3092
|
+
*/
|
|
3093
|
+
function getPublicUrlArgNames(app) {
|
|
3094
|
+
const prefix = getPublicEnvPrefix(app);
|
|
3095
|
+
if (!prefix) return [];
|
|
3096
|
+
return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3006
3099
|
//#endregion
|
|
3007
3100
|
//#region src/docker/compose.ts
|
|
3008
3101
|
/** Default Docker images for services */
|
|
@@ -3314,15 +3407,18 @@ function generateWorkspaceCompose(workspace, options = {}) {
|
|
|
3314
3407
|
|
|
3315
3408
|
services:
|
|
3316
3409
|
`;
|
|
3317
|
-
for (const [appName, app] of apps)
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3410
|
+
for (const [appName, app] of apps) {
|
|
3411
|
+
if (app.type === "mobile") continue;
|
|
3412
|
+
yaml += generateAppService(appName, app, apps, {
|
|
3413
|
+
registry,
|
|
3414
|
+
projectName: workspace.name,
|
|
3415
|
+
hasPostgres,
|
|
3416
|
+
hasRedis,
|
|
3417
|
+
hasMinio,
|
|
3418
|
+
hasMail,
|
|
3419
|
+
eventsBackend
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3326
3422
|
if (hasPostgres) yaml += `
|
|
3327
3423
|
postgres:
|
|
3328
3424
|
image: ${postgresImage}
|
|
@@ -3491,8 +3587,8 @@ function getInfraServiceImage(serviceName, config$1) {
|
|
|
3491
3587
|
function generateAppService(appName, app, allApps, options) {
|
|
3492
3588
|
const { registry, projectName, hasPostgres, hasRedis, hasMinio, hasMail, eventsBackend } = options;
|
|
3493
3589
|
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
|
|
3494
|
-
const healthCheckPath = app.type === "
|
|
3495
|
-
const healthCheckCmd = app.type === "
|
|
3590
|
+
const healthCheckPath = app.type === "web" ? "/" : "/health";
|
|
3591
|
+
const healthCheckCmd = app.type === "web" ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]` : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
|
|
3496
3592
|
let yaml = `
|
|
3497
3593
|
${appName}:
|
|
3498
3594
|
build:
|
|
@@ -4295,6 +4391,115 @@ ENTRYPOINT ["/sbin/tini", "--"]
|
|
|
4295
4391
|
CMD ["node", "index.mjs"]
|
|
4296
4392
|
`;
|
|
4297
4393
|
}
|
|
4394
|
+
/**
|
|
4395
|
+
* Generate a Dockerfile for Node-based SSR web frameworks (TanStack Start,
|
|
4396
|
+
* Remix). Builds with turbo, then runs the framework's production start
|
|
4397
|
+
* script via `npm start`.
|
|
4398
|
+
*
|
|
4399
|
+
* Public URL build args are declared so framework bundlers (Vite, Vinxi)
|
|
4400
|
+
* can inline them at build time. Per-framework prefix is the caller's job.
|
|
4401
|
+
* @internal Exported for testing
|
|
4402
|
+
*/
|
|
4403
|
+
function generateNodeWebDockerfile(options) {
|
|
4404
|
+
const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = [] } = options;
|
|
4405
|
+
const pm = getPmConfig(packageManager);
|
|
4406
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
4407
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
4408
|
+
const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
|
|
4409
|
+
const startCmd = packageManager === "pnpm" ? "pnpm start" : `${pm.lockfile.startsWith("package-lock") ? "npm" : packageManager} start`;
|
|
4410
|
+
const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join("\n");
|
|
4411
|
+
const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join("\n");
|
|
4412
|
+
return `# syntax=docker/dockerfile:1
|
|
4413
|
+
# Node SSR web Dockerfile (TanStack Start / Remix) with turbo prune
|
|
4414
|
+
|
|
4415
|
+
FROM ${baseImage} AS pruner
|
|
4416
|
+
WORKDIR /app
|
|
4417
|
+
${installPm}
|
|
4418
|
+
COPY . .
|
|
4419
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
4420
|
+
|
|
4421
|
+
FROM ${baseImage} AS deps
|
|
4422
|
+
WORKDIR /app
|
|
4423
|
+
${installPm}
|
|
4424
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
4425
|
+
COPY --from=pruner /app/out/json/ ./
|
|
4426
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
4427
|
+
${turboInstallCmd}
|
|
4428
|
+
|
|
4429
|
+
FROM deps AS builder
|
|
4430
|
+
WORKDIR /app
|
|
4431
|
+
${argDecls}
|
|
4432
|
+
${envDecls}
|
|
4433
|
+
COPY --from=pruner /app/out/full/ ./
|
|
4434
|
+
COPY --from=pruner /app/tsconfig.* ./
|
|
4435
|
+
RUN ${turboCmd} run build --filter=${turboPackage}
|
|
4436
|
+
|
|
4437
|
+
FROM ${baseImage} AS runner
|
|
4438
|
+
WORKDIR /app
|
|
4439
|
+
RUN apk add --no-cache tini
|
|
4440
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
4441
|
+
adduser --system --uid 1001 app
|
|
4442
|
+
|
|
4443
|
+
# Copy the whole built workspace — frameworks differ on output paths
|
|
4444
|
+
COPY --from=builder --chown=app:nodejs /app/ ./
|
|
4445
|
+
|
|
4446
|
+
ENV NODE_ENV=production
|
|
4447
|
+
ENV PORT=${port}
|
|
4448
|
+
ENV HOSTNAME="0.0.0.0"
|
|
4449
|
+
|
|
4450
|
+
USER app
|
|
4451
|
+
EXPOSE ${port}
|
|
4452
|
+
|
|
4453
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
4454
|
+
CMD ["sh", "-c", "cd ${appPath} && ${startCmd}"]
|
|
4455
|
+
`;
|
|
4456
|
+
}
|
|
4457
|
+
/**
|
|
4458
|
+
* Generate a Dockerfile for Vite SPA apps. Builds the static bundle with
|
|
4459
|
+
* turbo, then serves it via nginx.
|
|
4460
|
+
* @internal Exported for testing
|
|
4461
|
+
*/
|
|
4462
|
+
function generateViteStaticDockerfile(options) {
|
|
4463
|
+
const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = [] } = options;
|
|
4464
|
+
const pm = getPmConfig(packageManager);
|
|
4465
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
4466
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
4467
|
+
const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
|
|
4468
|
+
const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join("\n");
|
|
4469
|
+
const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join("\n");
|
|
4470
|
+
return `# syntax=docker/dockerfile:1
|
|
4471
|
+
# Vite SPA Dockerfile — builds static assets, serves via nginx
|
|
4472
|
+
|
|
4473
|
+
FROM ${baseImage} AS pruner
|
|
4474
|
+
WORKDIR /app
|
|
4475
|
+
${installPm}
|
|
4476
|
+
COPY . .
|
|
4477
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
4478
|
+
|
|
4479
|
+
FROM ${baseImage} AS deps
|
|
4480
|
+
WORKDIR /app
|
|
4481
|
+
${installPm}
|
|
4482
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
4483
|
+
COPY --from=pruner /app/out/json/ ./
|
|
4484
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
4485
|
+
${turboInstallCmd}
|
|
4486
|
+
|
|
4487
|
+
FROM deps AS builder
|
|
4488
|
+
WORKDIR /app
|
|
4489
|
+
${argDecls}
|
|
4490
|
+
${envDecls}
|
|
4491
|
+
COPY --from=pruner /app/out/full/ ./
|
|
4492
|
+
COPY --from=pruner /app/tsconfig.* ./
|
|
4493
|
+
RUN ${turboCmd} run build --filter=${turboPackage}
|
|
4494
|
+
|
|
4495
|
+
FROM nginx:alpine AS runner
|
|
4496
|
+
COPY --from=builder /app/${appPath}/dist /usr/share/nginx/html
|
|
4497
|
+
RUN printf 'server {\\n listen ${port};\\n root /usr/share/nginx/html;\\n index index.html;\\n location / { try_files $uri $uri/ /index.html; }\\n}\\n' \\
|
|
4498
|
+
> /etc/nginx/conf.d/default.conf
|
|
4499
|
+
EXPOSE ${port}
|
|
4500
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
4501
|
+
`;
|
|
4502
|
+
}
|
|
4298
4503
|
|
|
4299
4504
|
//#endregion
|
|
4300
4505
|
//#region src/docker/index.ts
|
|
@@ -4480,6 +4685,10 @@ async function workspaceDockerCommand(workspace, options) {
|
|
|
4480
4685
|
const packageManager = detectPackageManager$1(workspace.root);
|
|
4481
4686
|
logger$7.log(` Package manager: ${packageManager}`);
|
|
4482
4687
|
for (const [appName, app] of apps) {
|
|
4688
|
+
if (app.type === "mobile") {
|
|
4689
|
+
logger$7.log(`\n 📱 Skipping Docker for ${appName} (mobile app — deploy via framework toolchain)`);
|
|
4690
|
+
continue;
|
|
4691
|
+
}
|
|
4483
4692
|
const appPath = app.path;
|
|
4484
4693
|
const fullAppPath = join(workspace.root, appPath);
|
|
4485
4694
|
const turboPackage = getAppPackageName(fullAppPath) ?? appName;
|
|
@@ -4488,15 +4697,28 @@ async function workspaceDockerCommand(workspace, options) {
|
|
|
4488
4697
|
const buildType = hasEntry ? "entry" : app.type;
|
|
4489
4698
|
logger$7.log(`\n 📄 Generating Dockerfile for ${appName} (${buildType})`);
|
|
4490
4699
|
let dockerfile;
|
|
4491
|
-
if (app.type === "
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4700
|
+
if (app.type === "web") {
|
|
4701
|
+
const publicUrlArgs = getPublicUrlArgNames(app);
|
|
4702
|
+
const webOpts = {
|
|
4703
|
+
imageName,
|
|
4704
|
+
baseImage: "node:22-alpine",
|
|
4705
|
+
port: app.port,
|
|
4706
|
+
appPath,
|
|
4707
|
+
turboPackage,
|
|
4708
|
+
packageManager,
|
|
4709
|
+
publicUrlArgs
|
|
4710
|
+
};
|
|
4711
|
+
switch (app.framework) {
|
|
4712
|
+
case "vite":
|
|
4713
|
+
dockerfile = generateViteStaticDockerfile(webOpts);
|
|
4714
|
+
break;
|
|
4715
|
+
case "tanstack-start":
|
|
4716
|
+
case "remix":
|
|
4717
|
+
dockerfile = generateNodeWebDockerfile(webOpts);
|
|
4718
|
+
break;
|
|
4719
|
+
default: dockerfile = generateNextjsDockerfile(webOpts);
|
|
4720
|
+
}
|
|
4721
|
+
} else if (app.entry) dockerfile = generateEntryDockerfile({
|
|
4500
4722
|
imageName,
|
|
4501
4723
|
baseImage: "node:22-alpine",
|
|
4502
4724
|
port: app.port,
|
|
@@ -4536,7 +4758,7 @@ async function workspaceDockerCommand(workspace, options) {
|
|
|
4536
4758
|
logger$7.log(`\n✅ Generated ${results.length} Dockerfile(s) + docker-compose.yml`);
|
|
4537
4759
|
logger$7.log("\n📋 Build commands:");
|
|
4538
4760
|
for (const result of results) {
|
|
4539
|
-
const icon = result.type === "backend" ? "⚙️" : "🌐";
|
|
4761
|
+
const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
|
|
4540
4762
|
logger$7.log(` ${icon} docker build -f .gkm/docker/Dockerfile.${result.appName} -t ${result.imageName} .`);
|
|
4541
4763
|
}
|
|
4542
4764
|
logger$7.log("\n📋 Run all services:");
|
|
@@ -4773,54 +4995,6 @@ async function deployDokploy(options) {
|
|
|
4773
4995
|
};
|
|
4774
4996
|
}
|
|
4775
4997
|
|
|
4776
|
-
//#endregion
|
|
4777
|
-
//#region src/deploy/domain.ts
|
|
4778
|
-
/**
|
|
4779
|
-
* Resolve the hostname for an app based on stage configuration.
|
|
4780
|
-
*
|
|
4781
|
-
* Domain resolution priority:
|
|
4782
|
-
* 1. Explicit app.domain override (string or stage-specific)
|
|
4783
|
-
* 2. Default pattern based on app type:
|
|
4784
|
-
* - Main frontend app gets base domain (e.g., 'myapp.com')
|
|
4785
|
-
* - Other apps get prefixed domain (e.g., 'api.myapp.com')
|
|
4786
|
-
*
|
|
4787
|
-
* @param appName - The name of the app
|
|
4788
|
-
* @param app - The normalized app configuration
|
|
4789
|
-
* @param stage - The deployment stage (e.g., 'production', 'development')
|
|
4790
|
-
* @param dokployConfig - Dokploy workspace configuration with domain mappings
|
|
4791
|
-
* @param isMainFrontend - Whether this is the main frontend app
|
|
4792
|
-
* @returns The resolved hostname for the app
|
|
4793
|
-
* @throws Error if no domain configuration is found for the stage
|
|
4794
|
-
*/
|
|
4795
|
-
function resolveHost(appName, app, stage, dokployConfig, isMainFrontend) {
|
|
4796
|
-
if (app.domain) {
|
|
4797
|
-
if (typeof app.domain === "string") return app.domain;
|
|
4798
|
-
if (app.domain[stage]) return app.domain[stage];
|
|
4799
|
-
}
|
|
4800
|
-
const baseDomain = dokployConfig?.domains?.[stage];
|
|
4801
|
-
if (!baseDomain) throw new Error(`No domain configured for stage "${stage}". Add deploy.dokploy.domains.${stage} to gkm.config.ts`);
|
|
4802
|
-
if (isMainFrontend) return baseDomain;
|
|
4803
|
-
return `${appName}.${baseDomain}`;
|
|
4804
|
-
}
|
|
4805
|
-
/**
|
|
4806
|
-
* Determine if an app is the "main" frontend (gets base domain).
|
|
4807
|
-
*
|
|
4808
|
-
* An app is considered the main frontend if:
|
|
4809
|
-
* 1. It's named 'web' and is a frontend type
|
|
4810
|
-
* 2. It's the first frontend app in the apps list
|
|
4811
|
-
*
|
|
4812
|
-
* @param appName - The name of the app to check
|
|
4813
|
-
* @param app - The app configuration
|
|
4814
|
-
* @param allApps - All apps in the workspace
|
|
4815
|
-
* @returns True if this is the main frontend app
|
|
4816
|
-
*/
|
|
4817
|
-
function isMainFrontendApp(appName, app, allApps) {
|
|
4818
|
-
if (app.type !== "frontend") return false;
|
|
4819
|
-
if (appName === "web") return true;
|
|
4820
|
-
for (const [name$1, a] of Object.entries(allApps)) if (a.type === "frontend") return name$1 === appName;
|
|
4821
|
-
return false;
|
|
4822
|
-
}
|
|
4823
|
-
|
|
4824
4998
|
//#endregion
|
|
4825
4999
|
//#region src/deploy/env-resolver.ts
|
|
4826
5000
|
/**
|
|
@@ -4881,9 +5055,8 @@ function resolveEnvVar(varName, context) {
|
|
|
4881
5055
|
break;
|
|
4882
5056
|
}
|
|
4883
5057
|
if (context.dependencyUrls && varName.endsWith("_URL")) {
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
else depName = varName.slice(0, -4).toLowerCase();
|
|
5058
|
+
const stripped = stripPublicPrefix(varName) ?? varName;
|
|
5059
|
+
const depName = stripped.slice(0, -4).toLowerCase();
|
|
4887
5060
|
if (context.dependencyUrls[depName]) return context.dependencyUrls[depName];
|
|
4888
5061
|
}
|
|
4889
5062
|
if (context.userSecrets) {
|
|
@@ -5295,8 +5468,9 @@ function resolveSnifferFile(baseName) {
|
|
|
5295
5468
|
*/
|
|
5296
5469
|
async function sniffAppEnvironment(app, appName, workspacePath, options = {}) {
|
|
5297
5470
|
const { logWarnings = true, markOptional = false } = options;
|
|
5298
|
-
if (app.type === "
|
|
5299
|
-
const
|
|
5471
|
+
if (app.type === "web" || app.type === "mobile") {
|
|
5472
|
+
const publicPrefix = getPublicEnvPrefix(app.framework);
|
|
5473
|
+
const depVars = publicPrefix ? (app.dependencies ?? []).map((dep) => `${publicPrefix}${dep.toUpperCase()}_URL`) : [];
|
|
5300
5474
|
if (app.config) {
|
|
5301
5475
|
const sniffedVars = [];
|
|
5302
5476
|
const sniffedOptional = [];
|
|
@@ -6221,8 +6395,10 @@ async function workspaceDeployCommand(workspace, options) {
|
|
|
6221
6395
|
}
|
|
6222
6396
|
}
|
|
6223
6397
|
}
|
|
6398
|
+
const skippedMobileApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "mobile");
|
|
6399
|
+
if (skippedMobileApps.length > 0) logger$3.log(`\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(", ")}`);
|
|
6224
6400
|
const backendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "backend");
|
|
6225
|
-
const frontendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "
|
|
6401
|
+
const frontendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "web");
|
|
6226
6402
|
const perAppDbCredentials = /* @__PURE__ */ new Map();
|
|
6227
6403
|
if (provisionedPostgres && backendApps.length > 0) {
|
|
6228
6404
|
const appsNeedingDb = backendApps.filter((appName) => {
|
|
@@ -6473,11 +6649,14 @@ async function workspaceDeployCommand(workspace, options) {
|
|
|
6473
6649
|
const { valid, missing, resolved } = validateEnvVars(sniffedVars, envContext);
|
|
6474
6650
|
if (!valid) throw new Error(formatMissingVarsError(appName, missing, stage));
|
|
6475
6651
|
if (Object.keys(resolved).length > 0) logger$3.log(` Resolved ${Object.keys(resolved).length} env vars: ${Object.keys(resolved).join(", ")}`);
|
|
6652
|
+
const publicPrefix = getPublicEnvPrefix(app.framework);
|
|
6476
6653
|
const buildArgs = [];
|
|
6477
6654
|
const publicUrlArgNames = [];
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6655
|
+
if (publicPrefix) {
|
|
6656
|
+
for (const [key, value] of Object.entries(resolved)) if (key.startsWith(publicPrefix)) {
|
|
6657
|
+
buildArgs.push(`${key}=${value}`);
|
|
6658
|
+
publicUrlArgNames.push(key);
|
|
6659
|
+
}
|
|
6481
6660
|
}
|
|
6482
6661
|
if (buildArgs.length > 0) logger$3.log(` Build args: ${publicUrlArgNames.join(", ")}`);
|
|
6483
6662
|
const imageName = `${workspace.name}-${appName}`;
|
|
@@ -6914,9 +7093,9 @@ const GEEKMIDAS_VERSIONS = {
|
|
|
6914
7093
|
"@geekmidas/constructs": "~3.0.9",
|
|
6915
7094
|
"@geekmidas/db": "~1.0.1",
|
|
6916
7095
|
"@geekmidas/emailkit": "~1.0.0",
|
|
6917
|
-
"@geekmidas/envkit": "~1.0.
|
|
7096
|
+
"@geekmidas/envkit": "~1.0.6",
|
|
6918
7097
|
"@geekmidas/errors": "~1.0.0",
|
|
6919
|
-
"@geekmidas/events": "~1.1.
|
|
7098
|
+
"@geekmidas/events": "~1.1.1",
|
|
6920
7099
|
"@geekmidas/logger": "~1.0.1",
|
|
6921
7100
|
"@geekmidas/rate-limit": "~2.0.0",
|
|
6922
7101
|
"@geekmidas/schema": "~1.0.0",
|
|
@@ -7723,48 +7902,646 @@ coverage/
|
|
|
7723
7902
|
}
|
|
7724
7903
|
|
|
7725
7904
|
//#endregion
|
|
7726
|
-
//#region src/init/generators/
|
|
7905
|
+
//#region src/init/generators/mobile-expo.ts
|
|
7727
7906
|
/**
|
|
7728
|
-
* Generate
|
|
7907
|
+
* Generate an Expo mobile app for the fullstack template.
|
|
7908
|
+
*
|
|
7909
|
+
* Mirrors the structure of rezgo/apps/app: NativeWind for styling,
|
|
7910
|
+
* expo-router for navigation, better-auth + magic link, React Query
|
|
7911
|
+
* for data, all wired into the toolbox's typed API client.
|
|
7729
7912
|
*/
|
|
7730
|
-
function
|
|
7731
|
-
if (!options.monorepo) return [];
|
|
7732
|
-
const packageName = `@${options.name}/
|
|
7913
|
+
function generateExpoAppFiles(options) {
|
|
7914
|
+
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
7915
|
+
const packageName = `@${options.name}/app`;
|
|
7916
|
+
const apiPackage = `@${options.name}/api`;
|
|
7917
|
+
const modelsPackage = `@${options.name}/models`;
|
|
7918
|
+
const slug = options.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
7919
|
+
const scheme = slug.replace(/-/g, "");
|
|
7920
|
+
const bundleId = `com.${scheme}.app`;
|
|
7733
7921
|
const packageJson = {
|
|
7734
7922
|
name: packageName,
|
|
7735
7923
|
version: "0.0.1",
|
|
7924
|
+
main: "expo-router/entry",
|
|
7736
7925
|
private: true,
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
const tsConfig = {
|
|
7744
|
-
extends: "../../tsconfig.json",
|
|
7745
|
-
compilerOptions: {
|
|
7746
|
-
noEmit: true,
|
|
7747
|
-
allowImportingTsExtensions: true
|
|
7926
|
+
scripts: {
|
|
7927
|
+
dev: "gkm exec -- expo start -c",
|
|
7928
|
+
ios: "expo start --ios",
|
|
7929
|
+
android: "expo start --android",
|
|
7930
|
+
web: "expo start --web",
|
|
7931
|
+
typecheck: "tsc --noEmit"
|
|
7748
7932
|
},
|
|
7749
|
-
|
|
7750
|
-
|
|
7933
|
+
dependencies: {
|
|
7934
|
+
[apiPackage]: "workspace:*",
|
|
7935
|
+
[modelsPackage]: "workspace:*",
|
|
7936
|
+
"@better-auth/expo": "~1.2.0",
|
|
7937
|
+
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
7938
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
7939
|
+
"@react-navigation/native": "^7.1.0",
|
|
7940
|
+
"@tanstack/react-query": "~5.80.0",
|
|
7941
|
+
"better-auth": "~1.2.0",
|
|
7942
|
+
expo: "~55.0.0",
|
|
7943
|
+
"expo-constants": "~55.0.0",
|
|
7944
|
+
"expo-dev-client": "~55.0.0",
|
|
7945
|
+
"expo-linking": "~55.0.0",
|
|
7946
|
+
"expo-router": "~55.0.0",
|
|
7947
|
+
"expo-secure-store": "~14.2.0",
|
|
7948
|
+
"expo-splash-screen": "~55.0.0",
|
|
7949
|
+
"expo-status-bar": "~55.0.0",
|
|
7950
|
+
nativewind: "~4.2.0",
|
|
7951
|
+
react: "19.2.0",
|
|
7952
|
+
"react-dom": "19.2.0",
|
|
7953
|
+
"react-native": "0.83.6",
|
|
7954
|
+
"react-native-gesture-handler": "~2.30.0",
|
|
7955
|
+
"react-native-reanimated": "~4.2.0",
|
|
7956
|
+
"react-native-safe-area-context": "~5.6.0",
|
|
7957
|
+
"react-native-screens": "~4.23.0",
|
|
7958
|
+
"react-native-web": "~0.21.0",
|
|
7959
|
+
tailwindcss: "~3.4.0"
|
|
7960
|
+
},
|
|
7961
|
+
devDependencies: {
|
|
7962
|
+
"@babel/core": "^7.25.0",
|
|
7963
|
+
"@types/react": "~19.0.0",
|
|
7964
|
+
typescript: "~5.8.2"
|
|
7965
|
+
}
|
|
7751
7966
|
};
|
|
7752
|
-
const
|
|
7753
|
-
|
|
7754
|
-
// ============================================
|
|
7755
|
-
// Common Schemas
|
|
7756
|
-
// ============================================
|
|
7757
|
-
|
|
7758
|
-
export const IdSchema = z.uuid();
|
|
7967
|
+
const appConfig = `import type { ConfigContext, ExpoConfig } from '@expo/config';
|
|
7759
7968
|
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
export
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
}
|
|
7969
|
+
/**
|
|
7970
|
+
* Expo runtime config. Public env vars (\`EXPO_PUBLIC_*\`) are inlined
|
|
7971
|
+
* at build time; everything else is read at runtime from \`extra\`.
|
|
7972
|
+
*/
|
|
7973
|
+
export default function config(_context: ConfigContext): ExpoConfig {
|
|
7974
|
+
return {
|
|
7975
|
+
name: '${options.name}',
|
|
7976
|
+
slug: '${slug}',
|
|
7977
|
+
version: '0.0.1',
|
|
7978
|
+
orientation: 'portrait',
|
|
7979
|
+
icon: './assets/icon.png',
|
|
7980
|
+
scheme: '${scheme}',
|
|
7981
|
+
userInterfaceStyle: 'automatic',
|
|
7982
|
+
newArchEnabled: true,
|
|
7983
|
+
ios: {
|
|
7984
|
+
supportsTablet: true,
|
|
7985
|
+
bundleIdentifier: '${bundleId}',
|
|
7986
|
+
},
|
|
7987
|
+
android: {
|
|
7988
|
+
package: '${bundleId}',
|
|
7989
|
+
adaptiveIcon: {
|
|
7990
|
+
foregroundImage: './assets/adaptive-icon.png',
|
|
7991
|
+
backgroundColor: '#ffffff',
|
|
7992
|
+
},
|
|
7993
|
+
edgeToEdgeEnabled: true,
|
|
7994
|
+
},
|
|
7995
|
+
web: {
|
|
7996
|
+
output: 'static',
|
|
7997
|
+
},
|
|
7998
|
+
plugins: [
|
|
7999
|
+
'expo-router',
|
|
8000
|
+
'expo-secure-store',
|
|
8001
|
+
[
|
|
8002
|
+
'expo-splash-screen',
|
|
8003
|
+
{
|
|
8004
|
+
image: './assets/splash.png',
|
|
8005
|
+
imageWidth: 200,
|
|
8006
|
+
resizeMode: 'contain',
|
|
8007
|
+
backgroundColor: '#ffffff',
|
|
8008
|
+
},
|
|
8009
|
+
],
|
|
8010
|
+
],
|
|
8011
|
+
experiments: {
|
|
8012
|
+
typedRoutes: true,
|
|
8013
|
+
},
|
|
8014
|
+
};
|
|
8015
|
+
}
|
|
8016
|
+
`;
|
|
8017
|
+
const easJson = {
|
|
8018
|
+
cli: {
|
|
8019
|
+
version: ">= 10.2.1",
|
|
8020
|
+
appVersionSource: "remote"
|
|
8021
|
+
},
|
|
8022
|
+
build: {
|
|
8023
|
+
dev: {
|
|
8024
|
+
developmentClient: true,
|
|
8025
|
+
distribution: "internal",
|
|
8026
|
+
environment: "development",
|
|
8027
|
+
env: {
|
|
8028
|
+
EXPO_PUBLIC_API_URL: "http://localhost:3000",
|
|
8029
|
+
EXPO_PUBLIC_AUTH_URL: "http://localhost:3002"
|
|
8030
|
+
}
|
|
8031
|
+
},
|
|
8032
|
+
preview: {
|
|
8033
|
+
distribution: "internal",
|
|
8034
|
+
environment: "preview",
|
|
8035
|
+
env: {
|
|
8036
|
+
EXPO_PUBLIC_API_URL: "https://api.example.com",
|
|
8037
|
+
EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
|
|
8038
|
+
}
|
|
8039
|
+
},
|
|
8040
|
+
production: {
|
|
8041
|
+
autoIncrement: true,
|
|
8042
|
+
environment: "production",
|
|
8043
|
+
env: {
|
|
8044
|
+
EXPO_PUBLIC_API_URL: "https://api.example.com",
|
|
8045
|
+
EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
|
|
8046
|
+
}
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
};
|
|
8050
|
+
const babelConfig = `module.exports = function (api) {
|
|
8051
|
+
api.cache(true);
|
|
8052
|
+
return {
|
|
8053
|
+
presets: [
|
|
8054
|
+
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
|
|
8055
|
+
'nativewind/babel',
|
|
8056
|
+
],
|
|
8057
|
+
};
|
|
8058
|
+
};
|
|
8059
|
+
`;
|
|
8060
|
+
const metroConfig = `const { getDefaultConfig } = require('expo/metro-config');
|
|
8061
|
+
const { withNativeWind } = require('nativewind/metro');
|
|
8062
|
+
|
|
8063
|
+
const config = getDefaultConfig(__dirname);
|
|
8064
|
+
|
|
8065
|
+
module.exports = withNativeWind(config, { input: './global.css' });
|
|
8066
|
+
`;
|
|
8067
|
+
const tsConfig = {
|
|
8068
|
+
extends: "expo/tsconfig.base",
|
|
8069
|
+
compilerOptions: {
|
|
8070
|
+
strict: true,
|
|
8071
|
+
allowImportingTsExtensions: true,
|
|
8072
|
+
types: ["nativewind/types"],
|
|
8073
|
+
paths: {
|
|
8074
|
+
"@/*": ["./*"],
|
|
8075
|
+
[modelsPackage]: ["../../packages/models/src"],
|
|
8076
|
+
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
8077
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
8078
|
+
}
|
|
8079
|
+
},
|
|
8080
|
+
include: [
|
|
8081
|
+
"**/*.ts",
|
|
8082
|
+
"**/*.tsx",
|
|
8083
|
+
".expo/types/**/*.ts",
|
|
8084
|
+
"expo-env.d.ts",
|
|
8085
|
+
"nativewind-env.d.ts"
|
|
8086
|
+
]
|
|
8087
|
+
};
|
|
8088
|
+
const tailwindConfig = `import type { Config } from 'tailwindcss';
|
|
8089
|
+
|
|
8090
|
+
export default {
|
|
8091
|
+
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
|
|
8092
|
+
presets: [require('nativewind/preset')],
|
|
8093
|
+
theme: {
|
|
8094
|
+
extend: {},
|
|
8095
|
+
},
|
|
8096
|
+
plugins: [],
|
|
8097
|
+
} satisfies Config;
|
|
8098
|
+
`;
|
|
8099
|
+
const globalCss = `@tailwind base;
|
|
8100
|
+
@tailwind components;
|
|
8101
|
+
@tailwind utilities;
|
|
8102
|
+
`;
|
|
8103
|
+
const expoEnvDts = `/// <reference types="expo/types" />\n`;
|
|
8104
|
+
const nativewindEnvDts = `/// <reference types="nativewind/types" />\n`;
|
|
8105
|
+
const configTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
8106
|
+
|
|
8107
|
+
/**
|
|
8108
|
+
* Public app config. EXPO_PUBLIC_* vars are inlined into the bundle at
|
|
8109
|
+
* build time, so they're safe to read in client code.
|
|
8110
|
+
*/
|
|
8111
|
+
const envParser = new EnvironmentParser({
|
|
8112
|
+
EXPO_PUBLIC_API_URL: process.env.EXPO_PUBLIC_API_URL,
|
|
8113
|
+
EXPO_PUBLIC_AUTH_URL: process.env.EXPO_PUBLIC_AUTH_URL,
|
|
8114
|
+
});
|
|
8115
|
+
|
|
8116
|
+
export const config = envParser
|
|
8117
|
+
.create((get) => ({
|
|
8118
|
+
apiUrl: get('EXPO_PUBLIC_API_URL').string(),
|
|
8119
|
+
authUrl: get('EXPO_PUBLIC_AUTH_URL').string(),
|
|
8120
|
+
}))
|
|
8121
|
+
.parse();
|
|
8122
|
+
`;
|
|
8123
|
+
const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
|
|
8124
|
+
|
|
8125
|
+
export const queryClient = new QueryClient({
|
|
8126
|
+
defaultOptions: {
|
|
8127
|
+
queries: {
|
|
8128
|
+
staleTime: 60 * 1000,
|
|
8129
|
+
},
|
|
8130
|
+
},
|
|
8131
|
+
});
|
|
8132
|
+
`;
|
|
8133
|
+
const authClientTs = `import { expoClient } from '@better-auth/expo/client';
|
|
8134
|
+
import { magicLinkClient } from 'better-auth/client/plugins';
|
|
8135
|
+
import { createAuthClient } from 'better-auth/react';
|
|
8136
|
+
import * as SecureStore from 'expo-secure-store';
|
|
8137
|
+
|
|
8138
|
+
import { config } from '@/config.ts';
|
|
8139
|
+
|
|
8140
|
+
export const STORAGE_PREFIX = '${scheme}';
|
|
8141
|
+
export const COOKIE_STORE_KEY = \`\${STORAGE_PREFIX}_cookie\`;
|
|
8142
|
+
|
|
8143
|
+
export const authClient = createAuthClient({
|
|
8144
|
+
baseURL: config.authUrl,
|
|
8145
|
+
plugins: [
|
|
8146
|
+
expoClient({
|
|
8147
|
+
scheme: '${scheme}',
|
|
8148
|
+
storagePrefix: STORAGE_PREFIX,
|
|
8149
|
+
storage: SecureStore,
|
|
8150
|
+
}),
|
|
8151
|
+
magicLinkClient(),
|
|
8152
|
+
],
|
|
8153
|
+
});
|
|
8154
|
+
|
|
8155
|
+
export const { signIn, useSession } = authClient;
|
|
8156
|
+
|
|
8157
|
+
export async function signOut() {
|
|
8158
|
+
try {
|
|
8159
|
+
await authClient.signOut();
|
|
8160
|
+
} finally {
|
|
8161
|
+
await SecureStore.deleteItemAsync(COOKIE_STORE_KEY);
|
|
8162
|
+
}
|
|
8163
|
+
}
|
|
8164
|
+
`;
|
|
8165
|
+
const apiTs = `import { createApi } from '${apiPackage}/client';
|
|
8166
|
+
|
|
8167
|
+
import { config } from '@/config.ts';
|
|
8168
|
+
import { authClient } from './auth-client.ts';
|
|
8169
|
+
import { queryClient } from './query-client.ts';
|
|
8170
|
+
|
|
8171
|
+
export function createAppApi(options?: { headers?: Record<string, string> }) {
|
|
8172
|
+
return createApi({
|
|
8173
|
+
baseURL: config.apiUrl,
|
|
8174
|
+
queryClient,
|
|
8175
|
+
headers: options?.headers,
|
|
8176
|
+
onRequest: (cfg) => {
|
|
8177
|
+
const cookie = authClient.getCookie();
|
|
8178
|
+
const next = { ...cfg, credentials: 'omit' as const };
|
|
8179
|
+
if (cookie) {
|
|
8180
|
+
return {
|
|
8181
|
+
...next,
|
|
8182
|
+
headers: { ...(next.headers ?? {}), Cookie: cookie },
|
|
8183
|
+
};
|
|
8184
|
+
}
|
|
8185
|
+
return next;
|
|
8186
|
+
},
|
|
8187
|
+
});
|
|
8188
|
+
}
|
|
8189
|
+
`;
|
|
8190
|
+
const apiContextTsx = `import { createContext, type ReactNode, use } from 'react';
|
|
8191
|
+
import type { createAppApi } from './api.ts';
|
|
8192
|
+
|
|
8193
|
+
export type Api = ReturnType<typeof createAppApi>;
|
|
8194
|
+
|
|
8195
|
+
const ApiContext = createContext<Api | null>(null);
|
|
8196
|
+
|
|
8197
|
+
export function ApiProvider({
|
|
8198
|
+
api,
|
|
8199
|
+
children,
|
|
8200
|
+
}: {
|
|
8201
|
+
api: Api;
|
|
8202
|
+
children: ReactNode;
|
|
8203
|
+
}) {
|
|
8204
|
+
return <ApiContext.Provider value={api}>{children}</ApiContext.Provider>;
|
|
8205
|
+
}
|
|
8206
|
+
|
|
8207
|
+
export function useApi(): Api {
|
|
8208
|
+
const api = use(ApiContext);
|
|
8209
|
+
if (!api) {
|
|
8210
|
+
throw new Error('useApi must be used within an ApiProvider');
|
|
8211
|
+
}
|
|
8212
|
+
return api;
|
|
8213
|
+
}
|
|
8214
|
+
`;
|
|
8215
|
+
const layoutTsx = `import '../global.css';
|
|
8216
|
+
|
|
8217
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
8218
|
+
import { Stack, useRouter, useSegments } from 'expo-router';
|
|
8219
|
+
import { StatusBar } from 'expo-status-bar';
|
|
8220
|
+
import { useEffect, useMemo } from 'react';
|
|
8221
|
+
import 'react-native-reanimated';
|
|
8222
|
+
|
|
8223
|
+
import { authClient } from '@/lib/auth-client.ts';
|
|
8224
|
+
import { ApiProvider } from '@/lib/api-context.tsx';
|
|
8225
|
+
import { createAppApi } from '@/lib/api.ts';
|
|
8226
|
+
import { queryClient } from '@/lib/query-client.ts';
|
|
8227
|
+
|
|
8228
|
+
function AuthGate({ children }: { children: React.ReactNode }) {
|
|
8229
|
+
const router = useRouter();
|
|
8230
|
+
const segments = useSegments();
|
|
8231
|
+
const { data: session, isPending } = authClient.useSession();
|
|
8232
|
+
|
|
8233
|
+
useEffect(() => {
|
|
8234
|
+
if (isPending) return;
|
|
8235
|
+
const onAuthRoute = segments[0] === 'login';
|
|
8236
|
+
if (!session && !onAuthRoute) {
|
|
8237
|
+
router.replace('/login');
|
|
8238
|
+
} else if (session && onAuthRoute) {
|
|
8239
|
+
router.replace('/');
|
|
8240
|
+
}
|
|
8241
|
+
}, [isPending, session, segments, router]);
|
|
8242
|
+
|
|
8243
|
+
return <>{children}</>;
|
|
8244
|
+
}
|
|
8245
|
+
|
|
8246
|
+
export default function RootLayout() {
|
|
8247
|
+
const api = useMemo(() => createAppApi(), []);
|
|
8248
|
+
|
|
8249
|
+
return (
|
|
8250
|
+
<QueryClientProvider client={queryClient}>
|
|
8251
|
+
<ApiProvider api={api}>
|
|
8252
|
+
<AuthGate>
|
|
8253
|
+
<Stack screenOptions={{ headerShown: false }}>
|
|
8254
|
+
<Stack.Screen name="index" />
|
|
8255
|
+
<Stack.Screen name="login" />
|
|
8256
|
+
</Stack>
|
|
8257
|
+
</AuthGate>
|
|
8258
|
+
<StatusBar style="auto" />
|
|
8259
|
+
</ApiProvider>
|
|
8260
|
+
</QueryClientProvider>
|
|
8261
|
+
);
|
|
8262
|
+
}
|
|
8263
|
+
`;
|
|
8264
|
+
const indexTsx = `import { Pressable, Text, View } from 'react-native';
|
|
8265
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
8266
|
+
|
|
8267
|
+
import { signOut, useSession } from '@/lib/auth-client.ts';
|
|
8268
|
+
import { useApi } from '@/lib/api-context.tsx';
|
|
8269
|
+
|
|
8270
|
+
export default function Home() {
|
|
8271
|
+
const { data: session } = useSession();
|
|
8272
|
+
const api = useApi();
|
|
8273
|
+
const { data: health } = api.useQuery('GET /health', {});
|
|
8274
|
+
|
|
8275
|
+
return (
|
|
8276
|
+
<SafeAreaView className="flex-1 bg-white">
|
|
8277
|
+
<View className="flex-1 items-center justify-center gap-4 px-6">
|
|
8278
|
+
<Text className="text-3xl font-bold text-slate-900">
|
|
8279
|
+
Welcome to ${options.name}
|
|
8280
|
+
</Text>
|
|
8281
|
+
{session ? (
|
|
8282
|
+
<Text className="text-sm text-slate-500">
|
|
8283
|
+
Signed in as {session.user.email}
|
|
8284
|
+
</Text>
|
|
8285
|
+
) : null}
|
|
8286
|
+
{health ? (
|
|
8287
|
+
<Text className="text-xs text-slate-400">
|
|
8288
|
+
API: {JSON.stringify(health)}
|
|
8289
|
+
</Text>
|
|
8290
|
+
) : null}
|
|
8291
|
+
<Pressable
|
|
8292
|
+
onPress={signOut}
|
|
8293
|
+
className="mt-4 rounded-lg bg-slate-900 px-6 py-3"
|
|
8294
|
+
>
|
|
8295
|
+
<Text className="font-semibold text-white">Sign out</Text>
|
|
8296
|
+
</Pressable>
|
|
8297
|
+
</View>
|
|
8298
|
+
</SafeAreaView>
|
|
8299
|
+
);
|
|
8300
|
+
}
|
|
8301
|
+
`;
|
|
8302
|
+
const loginTsx = `import * as Linking from 'expo-linking';
|
|
8303
|
+
import { useState } from 'react';
|
|
8304
|
+
import {
|
|
8305
|
+
KeyboardAvoidingView,
|
|
8306
|
+
Platform,
|
|
8307
|
+
Pressable,
|
|
8308
|
+
Text,
|
|
8309
|
+
TextInput,
|
|
8310
|
+
View,
|
|
8311
|
+
} from 'react-native';
|
|
8312
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
8313
|
+
|
|
8314
|
+
import { authClient } from '@/lib/auth-client.ts';
|
|
8315
|
+
|
|
8316
|
+
const EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
|
|
8317
|
+
|
|
8318
|
+
export default function LoginScreen() {
|
|
8319
|
+
const [email, setEmail] = useState('');
|
|
8320
|
+
const [submitting, setSubmitting] = useState(false);
|
|
8321
|
+
const [sent, setSent] = useState(false);
|
|
8322
|
+
const [error, setError] = useState<string | null>(null);
|
|
8323
|
+
|
|
8324
|
+
async function handleSubmit() {
|
|
8325
|
+
if (!EMAIL_RE.test(email.trim())) {
|
|
8326
|
+
setError('Enter a valid email address');
|
|
8327
|
+
return;
|
|
8328
|
+
}
|
|
8329
|
+
setError(null);
|
|
8330
|
+
setSubmitting(true);
|
|
8331
|
+
|
|
8332
|
+
const callbackURL = Linking.createURL('/').replace(/^(\\w+):\\/\\/\\//, '$1://');
|
|
8333
|
+
try {
|
|
8334
|
+
const result = await authClient.signIn.magicLink({
|
|
8335
|
+
email: email.trim(),
|
|
8336
|
+
callbackURL,
|
|
8337
|
+
});
|
|
8338
|
+
if (result.error) {
|
|
8339
|
+
setError(result.error.message ?? 'Could not send magic link');
|
|
8340
|
+
return;
|
|
8341
|
+
}
|
|
8342
|
+
setSent(true);
|
|
8343
|
+
} catch (err) {
|
|
8344
|
+
setError(err instanceof Error ? err.message : 'Something went wrong');
|
|
8345
|
+
} finally {
|
|
8346
|
+
setSubmitting(false);
|
|
8347
|
+
}
|
|
8348
|
+
}
|
|
8349
|
+
|
|
8350
|
+
if (sent) {
|
|
8351
|
+
return (
|
|
8352
|
+
<SafeAreaView className="flex-1 bg-white">
|
|
8353
|
+
<View className="flex-1 items-center justify-center px-6">
|
|
8354
|
+
<Text className="text-2xl font-bold text-slate-900">
|
|
8355
|
+
Check your inbox
|
|
8356
|
+
</Text>
|
|
8357
|
+
<Text className="mt-3 text-center text-slate-500">
|
|
8358
|
+
We sent a sign-in link to {email.trim()}
|
|
8359
|
+
</Text>
|
|
8360
|
+
</View>
|
|
8361
|
+
</SafeAreaView>
|
|
8362
|
+
);
|
|
8363
|
+
}
|
|
8364
|
+
|
|
8365
|
+
return (
|
|
8366
|
+
<SafeAreaView className="flex-1 bg-white">
|
|
8367
|
+
<KeyboardAvoidingView
|
|
8368
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
8369
|
+
className="flex-1"
|
|
8370
|
+
>
|
|
8371
|
+
<View className="flex-1 justify-center px-6">
|
|
8372
|
+
<Text className="text-3xl font-bold text-slate-900">Sign in</Text>
|
|
8373
|
+
<Text className="mt-1 text-sm text-slate-500">
|
|
8374
|
+
We'll email you a one-time sign-in link.
|
|
8375
|
+
</Text>
|
|
8376
|
+
|
|
8377
|
+
<Text className="mt-6 text-xs font-semibold uppercase text-slate-500">
|
|
8378
|
+
Email
|
|
8379
|
+
</Text>
|
|
8380
|
+
<TextInput
|
|
8381
|
+
value={email}
|
|
8382
|
+
onChangeText={(v) => {
|
|
8383
|
+
setEmail(v);
|
|
8384
|
+
if (error) setError(null);
|
|
8385
|
+
}}
|
|
8386
|
+
autoCapitalize="none"
|
|
8387
|
+
autoCorrect={false}
|
|
8388
|
+
keyboardType="email-address"
|
|
8389
|
+
placeholder="you@example.com"
|
|
8390
|
+
placeholderTextColor="#94a3b8"
|
|
8391
|
+
editable={!submitting}
|
|
8392
|
+
className="mt-2 rounded-lg border border-slate-300 px-4 py-3"
|
|
8393
|
+
/>
|
|
8394
|
+
{error ? (
|
|
8395
|
+
<Text className="mt-2 text-sm text-red-600">{error}</Text>
|
|
8396
|
+
) : null}
|
|
8397
|
+
|
|
8398
|
+
<Pressable
|
|
8399
|
+
onPress={handleSubmit}
|
|
8400
|
+
disabled={submitting}
|
|
8401
|
+
className="mt-6 items-center rounded-lg bg-slate-900 py-4 disabled:opacity-60"
|
|
8402
|
+
>
|
|
8403
|
+
<Text className="font-semibold text-white">
|
|
8404
|
+
{submitting ? 'Sending…' : 'Email me a sign-in link'}
|
|
8405
|
+
</Text>
|
|
8406
|
+
</Pressable>
|
|
8407
|
+
</View>
|
|
8408
|
+
</KeyboardAvoidingView>
|
|
8409
|
+
</SafeAreaView>
|
|
8410
|
+
);
|
|
8411
|
+
}
|
|
8412
|
+
`;
|
|
8413
|
+
const gitignore = `node_modules/
|
|
8414
|
+
.expo/
|
|
8415
|
+
dist/
|
|
8416
|
+
*.log
|
|
8417
|
+
.env*.local
|
|
8418
|
+
ios/
|
|
8419
|
+
android/
|
|
8420
|
+
.eas/
|
|
8421
|
+
`;
|
|
8422
|
+
return [
|
|
8423
|
+
{
|
|
8424
|
+
path: "apps/app/package.json",
|
|
8425
|
+
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
8426
|
+
},
|
|
8427
|
+
{
|
|
8428
|
+
path: "apps/app/app.config.ts",
|
|
8429
|
+
content: appConfig
|
|
8430
|
+
},
|
|
8431
|
+
{
|
|
8432
|
+
path: "apps/app/eas.json",
|
|
8433
|
+
content: `${JSON.stringify(easJson, null, 2)}\n`
|
|
8434
|
+
},
|
|
8435
|
+
{
|
|
8436
|
+
path: "apps/app/babel.config.js",
|
|
8437
|
+
content: babelConfig
|
|
8438
|
+
},
|
|
8439
|
+
{
|
|
8440
|
+
path: "apps/app/metro.config.js",
|
|
8441
|
+
content: metroConfig
|
|
8442
|
+
},
|
|
8443
|
+
{
|
|
8444
|
+
path: "apps/app/tsconfig.json",
|
|
8445
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
8446
|
+
},
|
|
8447
|
+
{
|
|
8448
|
+
path: "apps/app/tailwind.config.ts",
|
|
8449
|
+
content: tailwindConfig
|
|
8450
|
+
},
|
|
8451
|
+
{
|
|
8452
|
+
path: "apps/app/global.css",
|
|
8453
|
+
content: globalCss
|
|
8454
|
+
},
|
|
8455
|
+
{
|
|
8456
|
+
path: "apps/app/expo-env.d.ts",
|
|
8457
|
+
content: expoEnvDts
|
|
8458
|
+
},
|
|
8459
|
+
{
|
|
8460
|
+
path: "apps/app/nativewind-env.d.ts",
|
|
8461
|
+
content: nativewindEnvDts
|
|
8462
|
+
},
|
|
8463
|
+
{
|
|
8464
|
+
path: "apps/app/config.ts",
|
|
8465
|
+
content: configTs
|
|
8466
|
+
},
|
|
8467
|
+
{
|
|
8468
|
+
path: "apps/app/lib/query-client.ts",
|
|
8469
|
+
content: queryClientTs
|
|
8470
|
+
},
|
|
8471
|
+
{
|
|
8472
|
+
path: "apps/app/lib/auth-client.ts",
|
|
8473
|
+
content: authClientTs
|
|
8474
|
+
},
|
|
8475
|
+
{
|
|
8476
|
+
path: "apps/app/lib/api.ts",
|
|
8477
|
+
content: apiTs
|
|
8478
|
+
},
|
|
8479
|
+
{
|
|
8480
|
+
path: "apps/app/lib/api-context.tsx",
|
|
8481
|
+
content: apiContextTsx
|
|
8482
|
+
},
|
|
8483
|
+
{
|
|
8484
|
+
path: "apps/app/app/_layout.tsx",
|
|
8485
|
+
content: layoutTsx
|
|
8486
|
+
},
|
|
8487
|
+
{
|
|
8488
|
+
path: "apps/app/app/index.tsx",
|
|
8489
|
+
content: indexTsx
|
|
8490
|
+
},
|
|
8491
|
+
{
|
|
8492
|
+
path: "apps/app/app/login.tsx",
|
|
8493
|
+
content: loginTsx
|
|
8494
|
+
},
|
|
8495
|
+
{
|
|
8496
|
+
path: "apps/app/.gitignore",
|
|
8497
|
+
content: gitignore
|
|
8498
|
+
}
|
|
8499
|
+
];
|
|
8500
|
+
}
|
|
8501
|
+
|
|
8502
|
+
//#endregion
|
|
8503
|
+
//#region src/init/generators/models.ts
|
|
8504
|
+
/**
|
|
8505
|
+
* Generate packages/models for shared Zod schemas (monorepo only)
|
|
8506
|
+
*/
|
|
8507
|
+
function generateModelsPackage(options) {
|
|
8508
|
+
if (!options.monorepo) return [];
|
|
8509
|
+
const packageName = `@${options.name}/models`;
|
|
8510
|
+
const packageJson = {
|
|
8511
|
+
name: packageName,
|
|
8512
|
+
version: "0.0.1",
|
|
8513
|
+
private: true,
|
|
8514
|
+
type: "module",
|
|
8515
|
+
exports: { "./*": "./src/*.ts" },
|
|
8516
|
+
scripts: { typecheck: "tsc --noEmit" },
|
|
8517
|
+
dependencies: {},
|
|
8518
|
+
devDependencies: { typescript: "~5.8.2" }
|
|
8519
|
+
};
|
|
8520
|
+
const tsConfig = {
|
|
8521
|
+
extends: "../../tsconfig.json",
|
|
8522
|
+
compilerOptions: {
|
|
8523
|
+
noEmit: true,
|
|
8524
|
+
allowImportingTsExtensions: true
|
|
8525
|
+
},
|
|
8526
|
+
include: ["src/**/*.ts"],
|
|
8527
|
+
exclude: ["node_modules", "dist"]
|
|
8528
|
+
};
|
|
8529
|
+
const commonTs = `import { z } from 'zod';
|
|
8530
|
+
|
|
8531
|
+
// ============================================
|
|
8532
|
+
// Common Schemas
|
|
8533
|
+
// ============================================
|
|
8534
|
+
|
|
8535
|
+
export const IdSchema = z.uuid();
|
|
8536
|
+
|
|
8537
|
+
export const IdParamsSchema = z.object({
|
|
8538
|
+
id: IdSchema,
|
|
8539
|
+
});
|
|
8540
|
+
|
|
8541
|
+
export const TimestampsSchema = z.object({
|
|
8542
|
+
createdAt: z.coerce.date(),
|
|
8543
|
+
updatedAt: z.coerce.date(),
|
|
8544
|
+
});
|
|
7768
8545
|
|
|
7769
8546
|
export const PaginationSchema = z.object({
|
|
7770
8547
|
page: z.coerce.number().int().positive().default(1),
|
|
@@ -8111,6 +8888,43 @@ export default defineConfig({
|
|
|
8111
8888
|
return files;
|
|
8112
8889
|
}
|
|
8113
8890
|
/**
|
|
8891
|
+
* Generate the frontend app entry for the workspace config based on the
|
|
8892
|
+
* selected `frontendFramework`. Defaults to Next.js for backwards compat.
|
|
8893
|
+
*/
|
|
8894
|
+
function generateFrontendAppEntry(options) {
|
|
8895
|
+
switch (options.frontendFramework) {
|
|
8896
|
+
case "tanstack-start": return `web: {
|
|
8897
|
+
type: 'web',
|
|
8898
|
+
framework: 'tanstack-start',
|
|
8899
|
+
path: 'apps/web',
|
|
8900
|
+
port: 3001,
|
|
8901
|
+
dependencies: ['api', 'auth'],
|
|
8902
|
+
config: {
|
|
8903
|
+
client: './src/config/client.ts',
|
|
8904
|
+
server: './src/config/server.ts',
|
|
8905
|
+
},
|
|
8906
|
+
},`;
|
|
8907
|
+
case "expo": return `app: {
|
|
8908
|
+
type: 'mobile',
|
|
8909
|
+
framework: 'expo',
|
|
8910
|
+
path: 'apps/app',
|
|
8911
|
+
port: 8081,
|
|
8912
|
+
dependencies: ['api', 'auth'],
|
|
8913
|
+
},`;
|
|
8914
|
+
default: return `web: {
|
|
8915
|
+
type: 'web',
|
|
8916
|
+
framework: 'nextjs',
|
|
8917
|
+
path: 'apps/web',
|
|
8918
|
+
port: 3001,
|
|
8919
|
+
dependencies: ['api', 'auth'],
|
|
8920
|
+
config: {
|
|
8921
|
+
client: './src/config/client.ts',
|
|
8922
|
+
server: './src/config/server.ts',
|
|
8923
|
+
},
|
|
8924
|
+
},`;
|
|
8925
|
+
}
|
|
8926
|
+
}
|
|
8927
|
+
/**
|
|
8114
8928
|
* Generate gkm.config.ts with defineWorkspace for fullstack template
|
|
8115
8929
|
*/
|
|
8116
8930
|
function generateWorkspaceConfig(options) {
|
|
@@ -8151,20 +8965,10 @@ export default defineWorkspace({
|
|
|
8151
8965
|
port: 3002,
|
|
8152
8966
|
entry: './src/index.ts',
|
|
8153
8967
|
framework: 'better-auth',
|
|
8154
|
-
envParser: './src/config/env#envParser',
|
|
8155
|
-
logger: './src/config/logger#logger',
|
|
8156
|
-
},
|
|
8157
|
-
web: {
|
|
8158
|
-
type: 'frontend',
|
|
8159
|
-
framework: 'nextjs',
|
|
8160
|
-
path: 'apps/web',
|
|
8161
|
-
port: 3001,
|
|
8162
|
-
dependencies: ['api', 'auth'],
|
|
8163
|
-
config: {
|
|
8164
|
-
client: './src/config/client.ts',
|
|
8165
|
-
server: './src/config/server.ts',
|
|
8166
|
-
},
|
|
8968
|
+
envParser: './src/config/env#envParser',
|
|
8969
|
+
logger: './src/config/logger#logger',
|
|
8167
8970
|
},
|
|
8971
|
+
${generateFrontendAppEntry(options)}
|
|
8168
8972
|
},
|
|
8169
8973
|
shared: {
|
|
8170
8974
|
packages: ['packages/*'],
|
|
@@ -9067,6 +9871,26 @@ const templateChoices = [{
|
|
|
9067
9871
|
description: "Monorepo with API + Next.js + shared models"
|
|
9068
9872
|
}];
|
|
9069
9873
|
/**
|
|
9874
|
+
* Frontend framework choices for the fullstack template prompt.
|
|
9875
|
+
*/
|
|
9876
|
+
const frontendFrameworkChoices = [
|
|
9877
|
+
{
|
|
9878
|
+
title: "Next.js",
|
|
9879
|
+
value: "nextjs",
|
|
9880
|
+
description: "React server components, app router (default)"
|
|
9881
|
+
},
|
|
9882
|
+
{
|
|
9883
|
+
title: "TanStack Start",
|
|
9884
|
+
value: "tanstack-start",
|
|
9885
|
+
description: "File-based router on Vite + Nitro"
|
|
9886
|
+
},
|
|
9887
|
+
{
|
|
9888
|
+
title: "Expo",
|
|
9889
|
+
value: "expo",
|
|
9890
|
+
description: "React Native mobile app (iOS + Android)"
|
|
9891
|
+
}
|
|
9892
|
+
];
|
|
9893
|
+
/**
|
|
9070
9894
|
* Logger type choices for prompts
|
|
9071
9895
|
*/
|
|
9072
9896
|
const loggerTypeChoices = [{
|
|
@@ -11174,6 +11998,382 @@ node_modules/
|
|
|
11174
11998
|
];
|
|
11175
11999
|
}
|
|
11176
12000
|
|
|
12001
|
+
//#endregion
|
|
12002
|
+
//#region src/init/generators/web-tanstack.ts
|
|
12003
|
+
/**
|
|
12004
|
+
* Generate TanStack Start web app files for the fullstack template.
|
|
12005
|
+
*
|
|
12006
|
+
* Mirrors the Next.js scaffold (apps/web) but on Vite + TanStack Start +
|
|
12007
|
+
* TanStack Router with the `VITE_` env-var convention.
|
|
12008
|
+
*/
|
|
12009
|
+
function generateTanStackWebFiles(options) {
|
|
12010
|
+
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
12011
|
+
const packageName = `@${options.name}/web`;
|
|
12012
|
+
const apiPackage = `@${options.name}/api`;
|
|
12013
|
+
const modelsPackage = `@${options.name}/models`;
|
|
12014
|
+
const uiPackage = `@${options.name}/ui`;
|
|
12015
|
+
const packageJson = {
|
|
12016
|
+
name: packageName,
|
|
12017
|
+
version: "0.0.1",
|
|
12018
|
+
private: true,
|
|
12019
|
+
type: "module",
|
|
12020
|
+
scripts: {
|
|
12021
|
+
dev: "gkm exec -- vite dev",
|
|
12022
|
+
build: "gkm exec -- vite build",
|
|
12023
|
+
start: "node .output/server/index.mjs",
|
|
12024
|
+
typecheck: "tsc --noEmit"
|
|
12025
|
+
},
|
|
12026
|
+
dependencies: {
|
|
12027
|
+
[apiPackage]: "workspace:*",
|
|
12028
|
+
[modelsPackage]: "workspace:*",
|
|
12029
|
+
[uiPackage]: "workspace:*",
|
|
12030
|
+
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
12031
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
12032
|
+
"@tanstack/react-query": "~5.80.0",
|
|
12033
|
+
"@tanstack/react-router": "^1.87.0",
|
|
12034
|
+
"@tanstack/react-start": "^1.87.0",
|
|
12035
|
+
"better-auth": "~1.2.0",
|
|
12036
|
+
react: "~19.2.0",
|
|
12037
|
+
"react-dom": "~19.2.0"
|
|
12038
|
+
},
|
|
12039
|
+
devDependencies: {
|
|
12040
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
12041
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
12042
|
+
"@tanstack/router-plugin": "^1.87.0",
|
|
12043
|
+
"@types/node": "~22.0.0",
|
|
12044
|
+
"@types/react": "~19.0.0",
|
|
12045
|
+
"@types/react-dom": "~19.0.0",
|
|
12046
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
12047
|
+
tailwindcss: "^4.0.0",
|
|
12048
|
+
tsx: "~4.20.0",
|
|
12049
|
+
typescript: "~5.8.2",
|
|
12050
|
+
vite: "^7.0.0",
|
|
12051
|
+
"vite-tsconfig-paths": "~5.1.0"
|
|
12052
|
+
}
|
|
12053
|
+
};
|
|
12054
|
+
const viteConfig = `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
12055
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
12056
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
|
12057
|
+
import viteReact from '@vitejs/plugin-react';
|
|
12058
|
+
import { defineConfig } from 'vite';
|
|
12059
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
12060
|
+
|
|
12061
|
+
export default defineConfig({
|
|
12062
|
+
server: {
|
|
12063
|
+
port: Number(Credentials.PORT ?? 3001),
|
|
12064
|
+
},
|
|
12065
|
+
// VITE_* vars are inlined automatically; no manual loadEnv needed.
|
|
12066
|
+
envPrefix: 'VITE_',
|
|
12067
|
+
plugins: [
|
|
12068
|
+
tsconfigPaths({ root: import.meta.dirname }),
|
|
12069
|
+
tailwindcss(),
|
|
12070
|
+
tanstackStart(),
|
|
12071
|
+
viteReact(),
|
|
12072
|
+
],
|
|
12073
|
+
});
|
|
12074
|
+
`;
|
|
12075
|
+
const tsConfig = {
|
|
12076
|
+
extends: "../../tsconfig.json",
|
|
12077
|
+
compilerOptions: {
|
|
12078
|
+
lib: [
|
|
12079
|
+
"dom",
|
|
12080
|
+
"dom.iterable",
|
|
12081
|
+
"ES2022"
|
|
12082
|
+
],
|
|
12083
|
+
allowJs: true,
|
|
12084
|
+
skipLibCheck: true,
|
|
12085
|
+
strict: true,
|
|
12086
|
+
noEmit: true,
|
|
12087
|
+
allowImportingTsExtensions: true,
|
|
12088
|
+
esModuleInterop: true,
|
|
12089
|
+
module: "ESNext",
|
|
12090
|
+
moduleResolution: "bundler",
|
|
12091
|
+
resolveJsonModule: true,
|
|
12092
|
+
isolatedModules: true,
|
|
12093
|
+
jsx: "preserve",
|
|
12094
|
+
incremental: true,
|
|
12095
|
+
baseUrl: ".",
|
|
12096
|
+
types: ["vite/client"],
|
|
12097
|
+
paths: {
|
|
12098
|
+
"~/*": ["./src/*", "../../packages/ui/src/*"],
|
|
12099
|
+
[modelsPackage]: ["../../packages/models/src"],
|
|
12100
|
+
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
12101
|
+
[uiPackage]: ["../../packages/ui/src"],
|
|
12102
|
+
[`${uiPackage}/*`]: ["../../packages/ui/src/*"],
|
|
12103
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
12104
|
+
}
|
|
12105
|
+
},
|
|
12106
|
+
include: [
|
|
12107
|
+
"**/*.ts",
|
|
12108
|
+
"**/*.tsx",
|
|
12109
|
+
"src/routeTree.gen.ts"
|
|
12110
|
+
],
|
|
12111
|
+
exclude: [
|
|
12112
|
+
"node_modules",
|
|
12113
|
+
".output",
|
|
12114
|
+
".vinxi",
|
|
12115
|
+
"dist"
|
|
12116
|
+
]
|
|
12117
|
+
};
|
|
12118
|
+
const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
|
|
12119
|
+
|
|
12120
|
+
function makeQueryClient() {
|
|
12121
|
+
return new QueryClient({
|
|
12122
|
+
defaultOptions: {
|
|
12123
|
+
queries: {
|
|
12124
|
+
staleTime: 60 * 1000,
|
|
12125
|
+
},
|
|
12126
|
+
},
|
|
12127
|
+
});
|
|
12128
|
+
}
|
|
12129
|
+
|
|
12130
|
+
let browserQueryClient: QueryClient | undefined = undefined;
|
|
12131
|
+
|
|
12132
|
+
export function getQueryClient() {
|
|
12133
|
+
if (typeof window === 'undefined') {
|
|
12134
|
+
return makeQueryClient();
|
|
12135
|
+
}
|
|
12136
|
+
if (!browserQueryClient) browserQueryClient = makeQueryClient();
|
|
12137
|
+
return browserQueryClient;
|
|
12138
|
+
}
|
|
12139
|
+
`;
|
|
12140
|
+
const clientConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
12141
|
+
|
|
12142
|
+
// Client config - VITE_* vars are inlined at build time.
|
|
12143
|
+
const envParser = new EnvironmentParser({
|
|
12144
|
+
VITE_API_URL: import.meta.env.VITE_API_URL,
|
|
12145
|
+
VITE_AUTH_URL: import.meta.env.VITE_AUTH_URL,
|
|
12146
|
+
});
|
|
12147
|
+
|
|
12148
|
+
export const clientConfig = envParser
|
|
12149
|
+
.create((get) => ({
|
|
12150
|
+
apiUrl: get('VITE_API_URL').string(),
|
|
12151
|
+
authUrl: get('VITE_AUTH_URL').string(),
|
|
12152
|
+
}))
|
|
12153
|
+
.parse();
|
|
12154
|
+
`;
|
|
12155
|
+
const serverConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
12156
|
+
|
|
12157
|
+
// Server config - all env vars (server-side only, not exposed to browser).
|
|
12158
|
+
// Access these only in TanStack Start server functions / server routes.
|
|
12159
|
+
const envParser = new EnvironmentParser({ ...process.env });
|
|
12160
|
+
|
|
12161
|
+
export const serverConfig = envParser
|
|
12162
|
+
.create((_get) => ({
|
|
12163
|
+
// Add server-only secrets here
|
|
12164
|
+
// Example: stripeSecretKey: _get('STRIPE_SECRET_KEY').string(),
|
|
12165
|
+
}))
|
|
12166
|
+
.parse();
|
|
12167
|
+
`;
|
|
12168
|
+
const authClientTs = `import { magicLinkClient } from 'better-auth/client/plugins';
|
|
12169
|
+
import { createAuthClient } from 'better-auth/react';
|
|
12170
|
+
import { clientConfig } from '~/config/client.ts';
|
|
12171
|
+
|
|
12172
|
+
export const authClient = createAuthClient({
|
|
12173
|
+
baseURL: clientConfig.authUrl,
|
|
12174
|
+
plugins: [magicLinkClient()],
|
|
12175
|
+
});
|
|
12176
|
+
|
|
12177
|
+
export const { signIn, signUp, signOut, useSession, magicLink } = authClient;
|
|
12178
|
+
`;
|
|
12179
|
+
const apiIndexTs = `import { createApi } from '${apiPackage}/client';
|
|
12180
|
+
import { clientConfig } from '~/config/client.ts';
|
|
12181
|
+
import { getQueryClient } from '~/lib/query-client.ts';
|
|
12182
|
+
|
|
12183
|
+
export function createAppApi(options?: { headers?: Record<string, string> }) {
|
|
12184
|
+
return createApi({
|
|
12185
|
+
baseURL: clientConfig.apiUrl,
|
|
12186
|
+
queryClient: getQueryClient(),
|
|
12187
|
+
headers: options?.headers,
|
|
12188
|
+
onRequest: (config) => ({ ...config, credentials: 'include' }),
|
|
12189
|
+
});
|
|
12190
|
+
}
|
|
12191
|
+
|
|
12192
|
+
export const api = createAppApi();
|
|
12193
|
+
`;
|
|
12194
|
+
const routerTs = `import { createRouter } from '@tanstack/react-router';
|
|
12195
|
+
import { routeTree } from './routeTree.gen.ts';
|
|
12196
|
+
|
|
12197
|
+
export function getRouter() {
|
|
12198
|
+
return createRouter({
|
|
12199
|
+
routeTree,
|
|
12200
|
+
scrollRestoration: true,
|
|
12201
|
+
defaultPreload: 'intent',
|
|
12202
|
+
});
|
|
12203
|
+
}
|
|
12204
|
+
|
|
12205
|
+
declare module '@tanstack/react-router' {
|
|
12206
|
+
interface Register {
|
|
12207
|
+
router: ReturnType<typeof getRouter>;
|
|
12208
|
+
}
|
|
12209
|
+
}
|
|
12210
|
+
`;
|
|
12211
|
+
const routeTreeGen = `// This file is auto-generated by @tanstack/router-plugin.
|
|
12212
|
+
// It will be overwritten on the next \`vite dev\` run.
|
|
12213
|
+
import { rootRouteWithContext } from '@tanstack/react-router';
|
|
12214
|
+
|
|
12215
|
+
export const routeTree = rootRouteWithContext()({
|
|
12216
|
+
// Routes are populated by the plugin from src/routes/**.tsx
|
|
12217
|
+
} as never);
|
|
12218
|
+
`;
|
|
12219
|
+
const rootRouteTsx = `/// <reference types="vite/client" />
|
|
12220
|
+
import '~/styles/globals.css';
|
|
12221
|
+
|
|
12222
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
12223
|
+
import {
|
|
12224
|
+
HeadContent,
|
|
12225
|
+
Outlet,
|
|
12226
|
+
Scripts,
|
|
12227
|
+
createRootRoute,
|
|
12228
|
+
} from '@tanstack/react-router';
|
|
12229
|
+
import type { ReactNode } from 'react';
|
|
12230
|
+
import { getQueryClient } from '~/lib/query-client.ts';
|
|
12231
|
+
|
|
12232
|
+
export const Route = createRootRoute({
|
|
12233
|
+
head: () => ({
|
|
12234
|
+
meta: [
|
|
12235
|
+
{ charSet: 'utf-8' },
|
|
12236
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
12237
|
+
{ title: '${options.name}' },
|
|
12238
|
+
],
|
|
12239
|
+
}),
|
|
12240
|
+
component: RootComponent,
|
|
12241
|
+
});
|
|
12242
|
+
|
|
12243
|
+
function RootComponent() {
|
|
12244
|
+
const queryClient = getQueryClient();
|
|
12245
|
+
return (
|
|
12246
|
+
<RootDocument>
|
|
12247
|
+
<QueryClientProvider client={queryClient}>
|
|
12248
|
+
<Outlet />
|
|
12249
|
+
</QueryClientProvider>
|
|
12250
|
+
</RootDocument>
|
|
12251
|
+
);
|
|
12252
|
+
}
|
|
12253
|
+
|
|
12254
|
+
function RootDocument({ children }: { children: ReactNode }) {
|
|
12255
|
+
return (
|
|
12256
|
+
<html lang="en">
|
|
12257
|
+
<head>
|
|
12258
|
+
<HeadContent />
|
|
12259
|
+
</head>
|
|
12260
|
+
<body>
|
|
12261
|
+
{children}
|
|
12262
|
+
<Scripts />
|
|
12263
|
+
</body>
|
|
12264
|
+
</html>
|
|
12265
|
+
);
|
|
12266
|
+
}
|
|
12267
|
+
`;
|
|
12268
|
+
const indexRouteTsx = `import { createFileRoute } from '@tanstack/react-router';
|
|
12269
|
+
import { api } from '~/api/index.ts';
|
|
12270
|
+
|
|
12271
|
+
export const Route = createFileRoute('/')({
|
|
12272
|
+
component: HomeComponent,
|
|
12273
|
+
});
|
|
12274
|
+
|
|
12275
|
+
function HomeComponent() {
|
|
12276
|
+
const { data: health } = api.useQuery('GET /health', {});
|
|
12277
|
+
|
|
12278
|
+
return (
|
|
12279
|
+
<main className="min-h-screen bg-background p-8">
|
|
12280
|
+
<div className="mx-auto max-w-4xl space-y-8">
|
|
12281
|
+
<div className="space-y-2">
|
|
12282
|
+
<h1 className="text-4xl font-bold tracking-tight">
|
|
12283
|
+
Welcome to ${options.name}
|
|
12284
|
+
</h1>
|
|
12285
|
+
<p className="text-muted-foreground">
|
|
12286
|
+
Your TanStack Start app is ready.
|
|
12287
|
+
</p>
|
|
12288
|
+
</div>
|
|
12289
|
+
|
|
12290
|
+
<section className="rounded-lg border p-6">
|
|
12291
|
+
<h2 className="text-xl font-semibold">API Status</h2>
|
|
12292
|
+
{health ? (
|
|
12293
|
+
<pre className="mt-4 rounded bg-muted p-4 text-sm">
|
|
12294
|
+
{JSON.stringify(health, null, 2)}
|
|
12295
|
+
</pre>
|
|
12296
|
+
) : (
|
|
12297
|
+
<p className="mt-2 text-muted-foreground">Connecting…</p>
|
|
12298
|
+
)}
|
|
12299
|
+
</section>
|
|
12300
|
+
</div>
|
|
12301
|
+
</main>
|
|
12302
|
+
);
|
|
12303
|
+
}
|
|
12304
|
+
`;
|
|
12305
|
+
const globalsCss = `@import '${uiPackage}/styles';
|
|
12306
|
+
|
|
12307
|
+
@source "../..";
|
|
12308
|
+
@source "../../../../packages/ui/src";
|
|
12309
|
+
`;
|
|
12310
|
+
const gitignore = `.output/
|
|
12311
|
+
.vinxi/
|
|
12312
|
+
node_modules/
|
|
12313
|
+
.env.local
|
|
12314
|
+
*.log
|
|
12315
|
+
src/routeTree.gen.ts
|
|
12316
|
+
`;
|
|
12317
|
+
return [
|
|
12318
|
+
{
|
|
12319
|
+
path: "apps/web/package.json",
|
|
12320
|
+
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
12321
|
+
},
|
|
12322
|
+
{
|
|
12323
|
+
path: "apps/web/vite.config.ts",
|
|
12324
|
+
content: viteConfig
|
|
12325
|
+
},
|
|
12326
|
+
{
|
|
12327
|
+
path: "apps/web/tsconfig.json",
|
|
12328
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
12329
|
+
},
|
|
12330
|
+
{
|
|
12331
|
+
path: "apps/web/src/lib/query-client.ts",
|
|
12332
|
+
content: queryClientTs
|
|
12333
|
+
},
|
|
12334
|
+
{
|
|
12335
|
+
path: "apps/web/src/lib/auth-client.ts",
|
|
12336
|
+
content: authClientTs
|
|
12337
|
+
},
|
|
12338
|
+
{
|
|
12339
|
+
path: "apps/web/src/config/client.ts",
|
|
12340
|
+
content: clientConfigTs
|
|
12341
|
+
},
|
|
12342
|
+
{
|
|
12343
|
+
path: "apps/web/src/config/server.ts",
|
|
12344
|
+
content: serverConfigTs
|
|
12345
|
+
},
|
|
12346
|
+
{
|
|
12347
|
+
path: "apps/web/src/api/index.ts",
|
|
12348
|
+
content: apiIndexTs
|
|
12349
|
+
},
|
|
12350
|
+
{
|
|
12351
|
+
path: "apps/web/src/router.tsx",
|
|
12352
|
+
content: routerTs
|
|
12353
|
+
},
|
|
12354
|
+
{
|
|
12355
|
+
path: "apps/web/src/routeTree.gen.ts",
|
|
12356
|
+
content: routeTreeGen
|
|
12357
|
+
},
|
|
12358
|
+
{
|
|
12359
|
+
path: "apps/web/src/routes/__root.tsx",
|
|
12360
|
+
content: rootRouteTsx
|
|
12361
|
+
},
|
|
12362
|
+
{
|
|
12363
|
+
path: "apps/web/src/routes/index.tsx",
|
|
12364
|
+
content: indexRouteTsx
|
|
12365
|
+
},
|
|
12366
|
+
{
|
|
12367
|
+
path: "apps/web/src/styles/globals.css",
|
|
12368
|
+
content: globalsCss
|
|
12369
|
+
},
|
|
12370
|
+
{
|
|
12371
|
+
path: "apps/web/.gitignore",
|
|
12372
|
+
content: gitignore
|
|
12373
|
+
}
|
|
12374
|
+
];
|
|
12375
|
+
}
|
|
12376
|
+
|
|
11177
12377
|
//#endregion
|
|
11178
12378
|
//#region src/init/utils.ts
|
|
11179
12379
|
/**
|
|
@@ -11386,6 +12586,13 @@ async function initCommand(projectName, options = {}) {
|
|
|
11386
12586
|
message: "Routes structure:",
|
|
11387
12587
|
choices: routesStructureChoices,
|
|
11388
12588
|
initial: 0
|
|
12589
|
+
},
|
|
12590
|
+
{
|
|
12591
|
+
type: (_prev, values) => !options.yes && (options.template === "fullstack" || values.template === "fullstack") ? "select" : null,
|
|
12592
|
+
name: "frontendFramework",
|
|
12593
|
+
message: "Frontend framework:",
|
|
12594
|
+
choices: frontendFrameworkChoices,
|
|
12595
|
+
initial: 0
|
|
11389
12596
|
}
|
|
11390
12597
|
], { onCancel });
|
|
11391
12598
|
const name$1 = projectName || options.name || answers.name;
|
|
@@ -11427,6 +12634,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
11427
12634
|
const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
|
|
11428
12635
|
const deployTarget = options.yes ? "dokploy" : answers.deployTarget ?? "dokploy";
|
|
11429
12636
|
const database = services.db;
|
|
12637
|
+
const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
|
|
11430
12638
|
const templateOptions = {
|
|
11431
12639
|
name: name$1,
|
|
11432
12640
|
template,
|
|
@@ -11439,7 +12647,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
11439
12647
|
apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
|
|
11440
12648
|
packageManager: pkgManager,
|
|
11441
12649
|
deployTarget,
|
|
11442
|
-
services
|
|
12650
|
+
services,
|
|
12651
|
+
frontendFramework
|
|
11443
12652
|
};
|
|
11444
12653
|
const targetDir = join(cwd, name$1);
|
|
11445
12654
|
const baseTemplate = getTemplate(templateOptions.template);
|
|
@@ -11467,7 +12676,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
11467
12676
|
] : [];
|
|
11468
12677
|
const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
|
|
11469
12678
|
const rootFiles = baseTemplate ? [...generateMonorepoFiles(templateOptions, baseTemplate), ...generateModelsPackage(templateOptions)] : [];
|
|
11470
|
-
const webAppFiles = isFullstack ? generateWebAppFiles(templateOptions) : [];
|
|
12679
|
+
const webAppFiles = isFullstack ? frontendFramework === "tanstack-start" ? generateTanStackWebFiles(templateOptions) : frontendFramework === "expo" ? generateExpoAppFiles(templateOptions) : generateWebAppFiles(templateOptions) : [];
|
|
11471
12680
|
const authAppFiles = isFullstack ? generateAuthAppFiles(templateOptions) : [];
|
|
11472
12681
|
const uiPackageFiles = isFullstack ? generateUiPackageFiles(templateOptions) : [];
|
|
11473
12682
|
for (const { path, content } of rootFiles) {
|
|
@@ -11600,7 +12809,15 @@ function printNextSteps(projectName, options, pkgManager) {
|
|
|
11600
12809
|
console.log(` │ ├── api/ # Backend API`);
|
|
11601
12810
|
if (isFullstackTemplate(options.template)) {
|
|
11602
12811
|
console.log(` │ ├── auth/ # Auth service (better-auth)`);
|
|
11603
|
-
|
|
12812
|
+
switch (options.frontendFramework) {
|
|
12813
|
+
case "tanstack-start":
|
|
12814
|
+
console.log(` │ └── web/ # TanStack Start frontend`);
|
|
12815
|
+
break;
|
|
12816
|
+
case "expo":
|
|
12817
|
+
console.log(` │ └── app/ # Expo (React Native) app`);
|
|
12818
|
+
break;
|
|
12819
|
+
default: console.log(` │ └── web/ # Next.js frontend`);
|
|
12820
|
+
}
|
|
11604
12821
|
}
|
|
11605
12822
|
console.log(` ├── packages/`);
|
|
11606
12823
|
console.log(` │ ├── models/ # Shared Zod schemas`);
|
|
@@ -12075,7 +13292,7 @@ function printSummary(workspace, stage) {
|
|
|
12075
13292
|
logger$1.log("\n✅ Development environment ready!\n");
|
|
12076
13293
|
logger$1.log("📋 Apps:");
|
|
12077
13294
|
for (const [name$1, app] of Object.entries(workspace.apps)) {
|
|
12078
|
-
const icon = app.type === "
|
|
13295
|
+
const icon = app.type === "web" ? "🌐" : app.type === "mobile" ? "📱" : "🔧";
|
|
12079
13296
|
logger$1.log(` ${icon} ${name$1} → http://localhost:${app.port}`);
|
|
12080
13297
|
}
|
|
12081
13298
|
logger$1.log("\n🚀 Next steps:");
|
|
@@ -12434,11 +13651,11 @@ program.command("api").description("Manage REST API endpoints").action(() => {
|
|
|
12434
13651
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12435
13652
|
process.stdout.write("REST API management - coming soon\n");
|
|
12436
13653
|
});
|
|
12437
|
-
program.command("openapi").description("Generate OpenAPI specification from endpoints").action(async () => {
|
|
13654
|
+
program.command("openapi").description("Generate OpenAPI specification from endpoints").option("--app <name>", "Workspace mode: generate for a single named backend app").action(async (options) => {
|
|
12438
13655
|
try {
|
|
12439
13656
|
const globalOptions = program.opts();
|
|
12440
13657
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12441
|
-
await openapiCommand({});
|
|
13658
|
+
await openapiCommand({ app: options.app });
|
|
12442
13659
|
} catch (error) {
|
|
12443
13660
|
console.error(formatError(error));
|
|
12444
13661
|
process.exit(1);
|
|
@@ -12549,7 +13766,7 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
|
|
|
12549
13766
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12550
13767
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
|
|
12551
13768
|
const { pushSecrets: pushSecrets$1 } = await import("./sync-COnAugP-.mjs");
|
|
12552
|
-
const { reconcileMissingSecrets } = await import("./reconcile-
|
|
13769
|
+
const { reconcileMissingSecrets } = await import("./reconcile-CYGmqXvq.mjs");
|
|
12553
13770
|
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
|
|
12554
13771
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
12555
13772
|
const secrets = await readStageSecrets$1(options.stage, workspace.root);
|
|
@@ -12575,7 +13792,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
12575
13792
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
|
|
12576
13793
|
const { pullSecrets: pullSecrets$1 } = await import("./sync-COnAugP-.mjs");
|
|
12577
13794
|
const { writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
|
|
12578
|
-
const { reconcileMissingSecrets } = await import("./reconcile-
|
|
13795
|
+
const { reconcileMissingSecrets } = await import("./reconcile-CYGmqXvq.mjs");
|
|
12579
13796
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
12580
13797
|
let secrets = await pullSecrets$1(options.stage, workspace);
|
|
12581
13798
|
if (!secrets) {
|
|
@@ -12600,7 +13817,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
12600
13817
|
const globalOptions = program.opts();
|
|
12601
13818
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12602
13819
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
|
|
12603
|
-
const { reconcileMissingSecrets } = await import("./reconcile-
|
|
13820
|
+
const { reconcileMissingSecrets } = await import("./reconcile-CYGmqXvq.mjs");
|
|
12604
13821
|
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
|
|
12605
13822
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
12606
13823
|
const secrets = await readStageSecrets$1(options.stage, workspace.root);
|