@geekmidas/cli 1.10.34 → 1.10.36
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 +16 -0
- package/dist/{config-Cuo8vFsp.cjs → config-QnuOcYXp.cjs} +2 -2
- package/dist/{config-Cuo8vFsp.cjs.map → config-QnuOcYXp.cjs.map} +1 -1
- package/dist/{config-B62g483e.mjs → config-U-gojtxn.mjs} +2 -2
- package/dist/{config-B62g483e.mjs.map → config-U-gojtxn.mjs.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/{index-Dt_dZ7K4.d.mts → index-D7iT4dnv.d.mts} +41 -4
- package/dist/index-D7iT4dnv.d.mts.map +1 -0
- package/dist/{index-C3t5VL4R.d.cts → index-DRQq26DF.d.cts} +41 -4
- package/dist/index-DRQq26DF.d.cts.map +1 -0
- package/dist/index.cjs +1200 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1200 -63
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CflxypuN.mjs → openapi-BA0e3I_s.mjs} +65 -15
- package/dist/openapi-BA0e3I_s.mjs.map +1 -0
- package/dist/{openapi-B_rJjImN.cjs → openapi-Bb4UEyZN.cjs} +65 -15
- package/dist/openapi-Bb4UEyZN.cjs.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/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-BobDOIG9.cjs} +74 -3
- package/dist/workspace-BobDOIG9.cjs.map +1 -0
- package/dist/{workspace-Bi4X7Yzy.mjs → workspace-Cgmvgwh8.mjs} +57 -4
- package/dist/workspace-Cgmvgwh8.mjs.map +1 -0
- package/package.json +4 -4
- package/src/__tests__/openapi.spec.ts +99 -9
- package/src/deploy/__tests__/env-resolver.spec.ts +28 -0
- package/src/deploy/env-resolver.ts +5 -10
- package/src/deploy/index.ts +13 -7
- package/src/deploy/sniffer.ts +10 -4
- package/src/dev/__tests__/index.spec.ts +119 -0
- package/src/dev/index.ts +162 -37
- package/src/index.ts +6 -2
- package/src/init/__tests__/generators.spec.ts +147 -0
- 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 +3 -3
- package/src/openapi.ts +110 -23
- package/src/workspace/__tests__/index.spec.ts +92 -0
- package/src/workspace/__tests__/publicEnv.spec.ts +64 -0
- package/src/workspace/index.ts +16 -2
- package/src/workspace/publicEnv.ts +59 -0
- package/src/workspace/types.ts +6 -1
- 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-Cgmvgwh8.mjs";
|
|
4
|
+
import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig } from "./config-U-gojtxn.mjs";
|
|
5
5
|
import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId } from "./credentials-s1kLcIzK.mjs";
|
|
6
6
|
import { createStageSecrets, generateConnectionUrls, generateDbPassword, generateDbUrl, generateFullstackCustomSecrets, generateLocalStackCredentials, generatePgBossUrl, generateSecurePassword, generateServiceCredentials, rotateServicePassword, writeDockerEnvFromSecrets } from "./fullstack-secrets-P84v8zWn.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-BA0e3I_s.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.35";
|
|
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";
|
|
@@ -1453,38 +1453,134 @@ function checkPortConflicts(workspace) {
|
|
|
1453
1453
|
}
|
|
1454
1454
|
return conflicts;
|
|
1455
1455
|
}
|
|
1456
|
+
const FRONTEND_FRAMEWORKS = {
|
|
1457
|
+
nextjs: {
|
|
1458
|
+
displayName: "Next.js",
|
|
1459
|
+
configFiles: [
|
|
1460
|
+
"next.config.js",
|
|
1461
|
+
"next.config.ts",
|
|
1462
|
+
"next.config.mjs",
|
|
1463
|
+
"next.config.cjs"
|
|
1464
|
+
],
|
|
1465
|
+
dependency: "next",
|
|
1466
|
+
installCommand: "pnpm add next react react-dom"
|
|
1467
|
+
},
|
|
1468
|
+
remix: {
|
|
1469
|
+
displayName: "Remix",
|
|
1470
|
+
configFiles: [
|
|
1471
|
+
"remix.config.js",
|
|
1472
|
+
"remix.config.mjs",
|
|
1473
|
+
"remix.config.ts",
|
|
1474
|
+
"vite.config.js",
|
|
1475
|
+
"vite.config.ts",
|
|
1476
|
+
"vite.config.mjs",
|
|
1477
|
+
"vite.config.cjs"
|
|
1478
|
+
],
|
|
1479
|
+
dependency: "@remix-run/dev",
|
|
1480
|
+
installCommand: "pnpm add -D @remix-run/dev"
|
|
1481
|
+
},
|
|
1482
|
+
vite: {
|
|
1483
|
+
displayName: "Vite",
|
|
1484
|
+
configFiles: [
|
|
1485
|
+
"vite.config.js",
|
|
1486
|
+
"vite.config.ts",
|
|
1487
|
+
"vite.config.mjs",
|
|
1488
|
+
"vite.config.cjs"
|
|
1489
|
+
],
|
|
1490
|
+
dependency: "vite",
|
|
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"
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1456
1515
|
/**
|
|
1457
|
-
*
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1516
|
+
* Auto-detect the frontend framework by scanning package.json deps and config
|
|
1517
|
+
* files. Dependency match wins over config file match because deps are more
|
|
1518
|
+
* distinctive (a Remix app has both vite.config.ts AND @remix-run/dev).
|
|
1519
|
+
*/
|
|
1520
|
+
function detectFrontendFramework(fullPath, deps) {
|
|
1521
|
+
const order = [
|
|
1522
|
+
"expo",
|
|
1523
|
+
"tanstack-start",
|
|
1524
|
+
"nextjs",
|
|
1525
|
+
"remix",
|
|
1526
|
+
"vite"
|
|
1527
|
+
];
|
|
1528
|
+
for (const name$1 of order) if (deps[FRONTEND_FRAMEWORKS[name$1].dependency]) return name$1;
|
|
1529
|
+
for (const name$1 of order) {
|
|
1530
|
+
const hasConfig = FRONTEND_FRAMEWORKS[name$1].configFiles.some((file) => existsSync(join(fullPath, file)));
|
|
1531
|
+
if (hasConfig) return name$1;
|
|
1532
|
+
}
|
|
1533
|
+
return void 0;
|
|
1534
|
+
}
|
|
1464
1535
|
/**
|
|
1465
|
-
* Validate a frontend
|
|
1466
|
-
*
|
|
1536
|
+
* Validate a frontend app configuration.
|
|
1537
|
+
*
|
|
1538
|
+
* If `framework` is provided, validates strictly against that framework's
|
|
1539
|
+
* expected config file and dependency. Otherwise auto-detects the framework
|
|
1540
|
+
* from package.json and config files. If no recognized framework is found, the
|
|
1541
|
+
* app is allowed through with a warning provided it has a `dev` script.
|
|
1542
|
+
*
|
|
1467
1543
|
* @internal Exported for testing
|
|
1468
1544
|
*/
|
|
1469
|
-
async function validateFrontendApp(appName, appPath, workspaceRoot) {
|
|
1545
|
+
async function validateFrontendApp(appName, appPath, workspaceRoot, framework) {
|
|
1470
1546
|
const errors = [];
|
|
1471
1547
|
const warnings = [];
|
|
1472
1548
|
const fullPath = join(workspaceRoot, appPath);
|
|
1473
|
-
const hasConfigFile = NEXTJS_CONFIG_FILES.some((file) => existsSync(join(fullPath, file)));
|
|
1474
|
-
if (!hasConfigFile) errors.push(`Next.js config file not found. Expected one of: ${NEXTJS_CONFIG_FILES.join(", ")}`);
|
|
1475
1549
|
const packageJsonPath = join(fullPath, "package.json");
|
|
1476
|
-
if (existsSync(packageJsonPath))
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1550
|
+
if (!existsSync(packageJsonPath)) {
|
|
1551
|
+
errors.push(`package.json not found at ${appPath}. Run: pnpm init in the app directory.`);
|
|
1552
|
+
return {
|
|
1553
|
+
appName,
|
|
1554
|
+
valid: false,
|
|
1555
|
+
errors,
|
|
1556
|
+
warnings
|
|
1481
1557
|
};
|
|
1482
|
-
|
|
1483
|
-
|
|
1558
|
+
}
|
|
1559
|
+
let pkg$1;
|
|
1560
|
+
try {
|
|
1561
|
+
pkg$1 = __require(packageJsonPath);
|
|
1484
1562
|
} catch {
|
|
1485
1563
|
errors.push(`Failed to read package.json at ${packageJsonPath}`);
|
|
1564
|
+
return {
|
|
1565
|
+
appName,
|
|
1566
|
+
valid: false,
|
|
1567
|
+
errors,
|
|
1568
|
+
warnings
|
|
1569
|
+
};
|
|
1486
1570
|
}
|
|
1487
|
-
|
|
1571
|
+
const deps = {
|
|
1572
|
+
...pkg$1.dependencies,
|
|
1573
|
+
...pkg$1.devDependencies
|
|
1574
|
+
};
|
|
1575
|
+
const resolvedFramework = framework ?? detectFrontendFramework(fullPath, deps);
|
|
1576
|
+
if (resolvedFramework) {
|
|
1577
|
+
const spec = FRONTEND_FRAMEWORKS[resolvedFramework];
|
|
1578
|
+
const hasConfig = spec.configFiles.some((file) => existsSync(join(fullPath, file)));
|
|
1579
|
+
if (!hasConfig) errors.push(`${spec.displayName} config file not found. Expected one of: ${spec.configFiles.join(", ")}`);
|
|
1580
|
+
if (!deps[spec.dependency]) errors.push(`${spec.displayName} not found in dependencies. Run: ${spec.installCommand}`);
|
|
1581
|
+
} else warnings.push("No recognized frontend framework detected (Next.js, Vite, Remix). The app will run via its \"dev\" script. Set `framework` in your app config to enable strict validation.");
|
|
1582
|
+
if (!pkg$1.scripts?.dev) if (resolvedFramework) warnings.push("No \"dev\" script found in package.json. Turbo expects a \"dev\" script to run.");
|
|
1583
|
+
else errors.push("No \"dev\" script found in package.json. Without a recognized framework or a dev script, there is nothing to run.");
|
|
1488
1584
|
return {
|
|
1489
1585
|
appName,
|
|
1490
1586
|
valid: errors.length === 0,
|
|
@@ -1500,7 +1596,7 @@ async function validateFrontendApp(appName, appPath, workspaceRoot) {
|
|
|
1500
1596
|
async function validateFrontendApps(workspace) {
|
|
1501
1597
|
const results = [];
|
|
1502
1598
|
for (const [appName, app] of Object.entries(workspace.apps)) if (app.type === "frontend") {
|
|
1503
|
-
const result = await validateFrontendApp(appName, app.path, workspace.root);
|
|
1599
|
+
const result = await validateFrontendApp(appName, app.path, workspace.root, app.framework);
|
|
1504
1600
|
results.push(result);
|
|
1505
1601
|
}
|
|
1506
1602
|
return results;
|
|
@@ -4808,9 +4904,8 @@ function resolveEnvVar(varName, context) {
|
|
|
4808
4904
|
break;
|
|
4809
4905
|
}
|
|
4810
4906
|
if (context.dependencyUrls && varName.endsWith("_URL")) {
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
else depName = varName.slice(0, -4).toLowerCase();
|
|
4907
|
+
const stripped = stripPublicPrefix(varName) ?? varName;
|
|
4908
|
+
const depName = stripped.slice(0, -4).toLowerCase();
|
|
4814
4909
|
if (context.dependencyUrls[depName]) return context.dependencyUrls[depName];
|
|
4815
4910
|
}
|
|
4816
4911
|
if (context.userSecrets) {
|
|
@@ -5223,7 +5318,8 @@ function resolveSnifferFile(baseName) {
|
|
|
5223
5318
|
async function sniffAppEnvironment(app, appName, workspacePath, options = {}) {
|
|
5224
5319
|
const { logWarnings = true, markOptional = false } = options;
|
|
5225
5320
|
if (app.type === "frontend") {
|
|
5226
|
-
const
|
|
5321
|
+
const publicPrefix = getPublicEnvPrefix(app.framework);
|
|
5322
|
+
const depVars = publicPrefix ? (app.dependencies ?? []).map((dep) => `${publicPrefix}${dep.toUpperCase()}_URL`) : [];
|
|
5227
5323
|
if (app.config) {
|
|
5228
5324
|
const sniffedVars = [];
|
|
5229
5325
|
const sniffedOptional = [];
|
|
@@ -6400,11 +6496,14 @@ async function workspaceDeployCommand(workspace, options) {
|
|
|
6400
6496
|
const { valid, missing, resolved } = validateEnvVars(sniffedVars, envContext);
|
|
6401
6497
|
if (!valid) throw new Error(formatMissingVarsError(appName, missing, stage));
|
|
6402
6498
|
if (Object.keys(resolved).length > 0) logger$3.log(` Resolved ${Object.keys(resolved).length} env vars: ${Object.keys(resolved).join(", ")}`);
|
|
6499
|
+
const publicPrefix = getPublicEnvPrefix(app.framework);
|
|
6403
6500
|
const buildArgs = [];
|
|
6404
6501
|
const publicUrlArgNames = [];
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6502
|
+
if (publicPrefix) {
|
|
6503
|
+
for (const [key, value] of Object.entries(resolved)) if (key.startsWith(publicPrefix)) {
|
|
6504
|
+
buildArgs.push(`${key}=${value}`);
|
|
6505
|
+
publicUrlArgNames.push(key);
|
|
6506
|
+
}
|
|
6408
6507
|
}
|
|
6409
6508
|
if (buildArgs.length > 0) logger$3.log(` Build args: ${publicUrlArgNames.join(", ")}`);
|
|
6410
6509
|
const imageName = `${workspace.name}-${appName}`;
|
|
@@ -6841,9 +6940,9 @@ const GEEKMIDAS_VERSIONS = {
|
|
|
6841
6940
|
"@geekmidas/constructs": "~3.0.9",
|
|
6842
6941
|
"@geekmidas/db": "~1.0.1",
|
|
6843
6942
|
"@geekmidas/emailkit": "~1.0.0",
|
|
6844
|
-
"@geekmidas/envkit": "~1.0.
|
|
6943
|
+
"@geekmidas/envkit": "~1.0.6",
|
|
6845
6944
|
"@geekmidas/errors": "~1.0.0",
|
|
6846
|
-
"@geekmidas/events": "~1.1.
|
|
6945
|
+
"@geekmidas/events": "~1.1.1",
|
|
6847
6946
|
"@geekmidas/logger": "~1.0.1",
|
|
6848
6947
|
"@geekmidas/rate-limit": "~2.0.0",
|
|
6849
6948
|
"@geekmidas/schema": "~1.0.0",
|
|
@@ -6851,7 +6950,7 @@ const GEEKMIDAS_VERSIONS = {
|
|
|
6851
6950
|
"@geekmidas/storage": "~2.0.2",
|
|
6852
6951
|
"@geekmidas/studio": "~1.0.0",
|
|
6853
6952
|
"@geekmidas/telescope": "~1.0.0",
|
|
6854
|
-
"@geekmidas/testkit": "~1.0.
|
|
6953
|
+
"@geekmidas/testkit": "~1.0.6",
|
|
6855
6954
|
"@geekmidas/cli": CLI_VERSION
|
|
6856
6955
|
};
|
|
6857
6956
|
|
|
@@ -7649,6 +7748,604 @@ coverage/
|
|
|
7649
7748
|
return files;
|
|
7650
7749
|
}
|
|
7651
7750
|
|
|
7751
|
+
//#endregion
|
|
7752
|
+
//#region src/init/generators/mobile-expo.ts
|
|
7753
|
+
/**
|
|
7754
|
+
* Generate an Expo mobile app for the fullstack template.
|
|
7755
|
+
*
|
|
7756
|
+
* Mirrors the structure of rezgo/apps/app: NativeWind for styling,
|
|
7757
|
+
* expo-router for navigation, better-auth + magic link, React Query
|
|
7758
|
+
* for data, all wired into the toolbox's typed API client.
|
|
7759
|
+
*/
|
|
7760
|
+
function generateExpoAppFiles(options) {
|
|
7761
|
+
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
7762
|
+
const packageName = `@${options.name}/app`;
|
|
7763
|
+
const apiPackage = `@${options.name}/api`;
|
|
7764
|
+
const modelsPackage = `@${options.name}/models`;
|
|
7765
|
+
const slug = options.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
7766
|
+
const scheme = slug.replace(/-/g, "");
|
|
7767
|
+
const bundleId = `com.${scheme}.app`;
|
|
7768
|
+
const packageJson = {
|
|
7769
|
+
name: packageName,
|
|
7770
|
+
version: "0.0.1",
|
|
7771
|
+
main: "expo-router/entry",
|
|
7772
|
+
private: true,
|
|
7773
|
+
scripts: {
|
|
7774
|
+
dev: "gkm exec -- expo start -c",
|
|
7775
|
+
ios: "expo start --ios",
|
|
7776
|
+
android: "expo start --android",
|
|
7777
|
+
web: "expo start --web",
|
|
7778
|
+
typecheck: "tsc --noEmit"
|
|
7779
|
+
},
|
|
7780
|
+
dependencies: {
|
|
7781
|
+
[apiPackage]: "workspace:*",
|
|
7782
|
+
[modelsPackage]: "workspace:*",
|
|
7783
|
+
"@better-auth/expo": "~1.2.0",
|
|
7784
|
+
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
7785
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
7786
|
+
"@react-navigation/native": "^7.1.0",
|
|
7787
|
+
"@tanstack/react-query": "~5.80.0",
|
|
7788
|
+
"better-auth": "~1.2.0",
|
|
7789
|
+
expo: "~55.0.0",
|
|
7790
|
+
"expo-constants": "~55.0.0",
|
|
7791
|
+
"expo-dev-client": "~55.0.0",
|
|
7792
|
+
"expo-linking": "~55.0.0",
|
|
7793
|
+
"expo-router": "~55.0.0",
|
|
7794
|
+
"expo-secure-store": "~14.2.0",
|
|
7795
|
+
"expo-splash-screen": "~55.0.0",
|
|
7796
|
+
"expo-status-bar": "~55.0.0",
|
|
7797
|
+
nativewind: "~4.2.0",
|
|
7798
|
+
react: "19.2.0",
|
|
7799
|
+
"react-dom": "19.2.0",
|
|
7800
|
+
"react-native": "0.83.6",
|
|
7801
|
+
"react-native-gesture-handler": "~2.30.0",
|
|
7802
|
+
"react-native-reanimated": "~4.2.0",
|
|
7803
|
+
"react-native-safe-area-context": "~5.6.0",
|
|
7804
|
+
"react-native-screens": "~4.23.0",
|
|
7805
|
+
"react-native-web": "~0.21.0",
|
|
7806
|
+
tailwindcss: "~3.4.0"
|
|
7807
|
+
},
|
|
7808
|
+
devDependencies: {
|
|
7809
|
+
"@babel/core": "^7.25.0",
|
|
7810
|
+
"@types/react": "~19.0.0",
|
|
7811
|
+
typescript: "~5.8.2"
|
|
7812
|
+
}
|
|
7813
|
+
};
|
|
7814
|
+
const appConfig = `import type { ConfigContext, ExpoConfig } from '@expo/config';
|
|
7815
|
+
|
|
7816
|
+
/**
|
|
7817
|
+
* Expo runtime config. Public env vars (\`EXPO_PUBLIC_*\`) are inlined
|
|
7818
|
+
* at build time; everything else is read at runtime from \`extra\`.
|
|
7819
|
+
*/
|
|
7820
|
+
export default function config(_context: ConfigContext): ExpoConfig {
|
|
7821
|
+
return {
|
|
7822
|
+
name: '${options.name}',
|
|
7823
|
+
slug: '${slug}',
|
|
7824
|
+
version: '0.0.1',
|
|
7825
|
+
orientation: 'portrait',
|
|
7826
|
+
icon: './assets/icon.png',
|
|
7827
|
+
scheme: '${scheme}',
|
|
7828
|
+
userInterfaceStyle: 'automatic',
|
|
7829
|
+
newArchEnabled: true,
|
|
7830
|
+
ios: {
|
|
7831
|
+
supportsTablet: true,
|
|
7832
|
+
bundleIdentifier: '${bundleId}',
|
|
7833
|
+
},
|
|
7834
|
+
android: {
|
|
7835
|
+
package: '${bundleId}',
|
|
7836
|
+
adaptiveIcon: {
|
|
7837
|
+
foregroundImage: './assets/adaptive-icon.png',
|
|
7838
|
+
backgroundColor: '#ffffff',
|
|
7839
|
+
},
|
|
7840
|
+
edgeToEdgeEnabled: true,
|
|
7841
|
+
},
|
|
7842
|
+
web: {
|
|
7843
|
+
output: 'static',
|
|
7844
|
+
},
|
|
7845
|
+
plugins: [
|
|
7846
|
+
'expo-router',
|
|
7847
|
+
'expo-secure-store',
|
|
7848
|
+
[
|
|
7849
|
+
'expo-splash-screen',
|
|
7850
|
+
{
|
|
7851
|
+
image: './assets/splash.png',
|
|
7852
|
+
imageWidth: 200,
|
|
7853
|
+
resizeMode: 'contain',
|
|
7854
|
+
backgroundColor: '#ffffff',
|
|
7855
|
+
},
|
|
7856
|
+
],
|
|
7857
|
+
],
|
|
7858
|
+
experiments: {
|
|
7859
|
+
typedRoutes: true,
|
|
7860
|
+
},
|
|
7861
|
+
};
|
|
7862
|
+
}
|
|
7863
|
+
`;
|
|
7864
|
+
const easJson = {
|
|
7865
|
+
cli: {
|
|
7866
|
+
version: ">= 10.2.1",
|
|
7867
|
+
appVersionSource: "remote"
|
|
7868
|
+
},
|
|
7869
|
+
build: {
|
|
7870
|
+
dev: {
|
|
7871
|
+
developmentClient: true,
|
|
7872
|
+
distribution: "internal",
|
|
7873
|
+
environment: "development",
|
|
7874
|
+
env: {
|
|
7875
|
+
EXPO_PUBLIC_API_URL: "http://localhost:3000",
|
|
7876
|
+
EXPO_PUBLIC_AUTH_URL: "http://localhost:3002"
|
|
7877
|
+
}
|
|
7878
|
+
},
|
|
7879
|
+
preview: {
|
|
7880
|
+
distribution: "internal",
|
|
7881
|
+
environment: "preview",
|
|
7882
|
+
env: {
|
|
7883
|
+
EXPO_PUBLIC_API_URL: "https://api.example.com",
|
|
7884
|
+
EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
|
|
7885
|
+
}
|
|
7886
|
+
},
|
|
7887
|
+
production: {
|
|
7888
|
+
autoIncrement: true,
|
|
7889
|
+
environment: "production",
|
|
7890
|
+
env: {
|
|
7891
|
+
EXPO_PUBLIC_API_URL: "https://api.example.com",
|
|
7892
|
+
EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
|
|
7893
|
+
}
|
|
7894
|
+
}
|
|
7895
|
+
}
|
|
7896
|
+
};
|
|
7897
|
+
const babelConfig = `module.exports = function (api) {
|
|
7898
|
+
api.cache(true);
|
|
7899
|
+
return {
|
|
7900
|
+
presets: [
|
|
7901
|
+
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
|
|
7902
|
+
'nativewind/babel',
|
|
7903
|
+
],
|
|
7904
|
+
};
|
|
7905
|
+
};
|
|
7906
|
+
`;
|
|
7907
|
+
const metroConfig = `const { getDefaultConfig } = require('expo/metro-config');
|
|
7908
|
+
const { withNativeWind } = require('nativewind/metro');
|
|
7909
|
+
|
|
7910
|
+
const config = getDefaultConfig(__dirname);
|
|
7911
|
+
|
|
7912
|
+
module.exports = withNativeWind(config, { input: './global.css' });
|
|
7913
|
+
`;
|
|
7914
|
+
const tsConfig = {
|
|
7915
|
+
extends: "expo/tsconfig.base",
|
|
7916
|
+
compilerOptions: {
|
|
7917
|
+
strict: true,
|
|
7918
|
+
allowImportingTsExtensions: true,
|
|
7919
|
+
types: ["nativewind/types"],
|
|
7920
|
+
paths: {
|
|
7921
|
+
"@/*": ["./*"],
|
|
7922
|
+
[modelsPackage]: ["../../packages/models/src"],
|
|
7923
|
+
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
7924
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
7925
|
+
}
|
|
7926
|
+
},
|
|
7927
|
+
include: [
|
|
7928
|
+
"**/*.ts",
|
|
7929
|
+
"**/*.tsx",
|
|
7930
|
+
".expo/types/**/*.ts",
|
|
7931
|
+
"expo-env.d.ts",
|
|
7932
|
+
"nativewind-env.d.ts"
|
|
7933
|
+
]
|
|
7934
|
+
};
|
|
7935
|
+
const tailwindConfig = `import type { Config } from 'tailwindcss';
|
|
7936
|
+
|
|
7937
|
+
export default {
|
|
7938
|
+
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
|
|
7939
|
+
presets: [require('nativewind/preset')],
|
|
7940
|
+
theme: {
|
|
7941
|
+
extend: {},
|
|
7942
|
+
},
|
|
7943
|
+
plugins: [],
|
|
7944
|
+
} satisfies Config;
|
|
7945
|
+
`;
|
|
7946
|
+
const globalCss = `@tailwind base;
|
|
7947
|
+
@tailwind components;
|
|
7948
|
+
@tailwind utilities;
|
|
7949
|
+
`;
|
|
7950
|
+
const expoEnvDts = `/// <reference types="expo/types" />\n`;
|
|
7951
|
+
const nativewindEnvDts = `/// <reference types="nativewind/types" />\n`;
|
|
7952
|
+
const configTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
7953
|
+
|
|
7954
|
+
/**
|
|
7955
|
+
* Public app config. EXPO_PUBLIC_* vars are inlined into the bundle at
|
|
7956
|
+
* build time, so they're safe to read in client code.
|
|
7957
|
+
*/
|
|
7958
|
+
const envParser = new EnvironmentParser({
|
|
7959
|
+
EXPO_PUBLIC_API_URL: process.env.EXPO_PUBLIC_API_URL,
|
|
7960
|
+
EXPO_PUBLIC_AUTH_URL: process.env.EXPO_PUBLIC_AUTH_URL,
|
|
7961
|
+
});
|
|
7962
|
+
|
|
7963
|
+
export const config = envParser
|
|
7964
|
+
.create((get) => ({
|
|
7965
|
+
apiUrl: get('EXPO_PUBLIC_API_URL').string(),
|
|
7966
|
+
authUrl: get('EXPO_PUBLIC_AUTH_URL').string(),
|
|
7967
|
+
}))
|
|
7968
|
+
.parse();
|
|
7969
|
+
`;
|
|
7970
|
+
const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
|
|
7971
|
+
|
|
7972
|
+
export const queryClient = new QueryClient({
|
|
7973
|
+
defaultOptions: {
|
|
7974
|
+
queries: {
|
|
7975
|
+
staleTime: 60 * 1000,
|
|
7976
|
+
},
|
|
7977
|
+
},
|
|
7978
|
+
});
|
|
7979
|
+
`;
|
|
7980
|
+
const authClientTs = `import { expoClient } from '@better-auth/expo/client';
|
|
7981
|
+
import { magicLinkClient } from 'better-auth/client/plugins';
|
|
7982
|
+
import { createAuthClient } from 'better-auth/react';
|
|
7983
|
+
import * as SecureStore from 'expo-secure-store';
|
|
7984
|
+
|
|
7985
|
+
import { config } from '@/config.ts';
|
|
7986
|
+
|
|
7987
|
+
export const STORAGE_PREFIX = '${scheme}';
|
|
7988
|
+
export const COOKIE_STORE_KEY = \`\${STORAGE_PREFIX}_cookie\`;
|
|
7989
|
+
|
|
7990
|
+
export const authClient = createAuthClient({
|
|
7991
|
+
baseURL: config.authUrl,
|
|
7992
|
+
plugins: [
|
|
7993
|
+
expoClient({
|
|
7994
|
+
scheme: '${scheme}',
|
|
7995
|
+
storagePrefix: STORAGE_PREFIX,
|
|
7996
|
+
storage: SecureStore,
|
|
7997
|
+
}),
|
|
7998
|
+
magicLinkClient(),
|
|
7999
|
+
],
|
|
8000
|
+
});
|
|
8001
|
+
|
|
8002
|
+
export const { signIn, useSession } = authClient;
|
|
8003
|
+
|
|
8004
|
+
export async function signOut() {
|
|
8005
|
+
try {
|
|
8006
|
+
await authClient.signOut();
|
|
8007
|
+
} finally {
|
|
8008
|
+
await SecureStore.deleteItemAsync(COOKIE_STORE_KEY);
|
|
8009
|
+
}
|
|
8010
|
+
}
|
|
8011
|
+
`;
|
|
8012
|
+
const apiTs = `import { createApi } from '${apiPackage}/client';
|
|
8013
|
+
|
|
8014
|
+
import { config } from '@/config.ts';
|
|
8015
|
+
import { authClient } from './auth-client.ts';
|
|
8016
|
+
import { queryClient } from './query-client.ts';
|
|
8017
|
+
|
|
8018
|
+
export function createAppApi(options?: { headers?: Record<string, string> }) {
|
|
8019
|
+
return createApi({
|
|
8020
|
+
baseURL: config.apiUrl,
|
|
8021
|
+
queryClient,
|
|
8022
|
+
headers: options?.headers,
|
|
8023
|
+
onRequest: (cfg) => {
|
|
8024
|
+
const cookie = authClient.getCookie();
|
|
8025
|
+
const next = { ...cfg, credentials: 'omit' as const };
|
|
8026
|
+
if (cookie) {
|
|
8027
|
+
return {
|
|
8028
|
+
...next,
|
|
8029
|
+
headers: { ...(next.headers ?? {}), Cookie: cookie },
|
|
8030
|
+
};
|
|
8031
|
+
}
|
|
8032
|
+
return next;
|
|
8033
|
+
},
|
|
8034
|
+
});
|
|
8035
|
+
}
|
|
8036
|
+
`;
|
|
8037
|
+
const apiContextTsx = `import { createContext, type ReactNode, use } from 'react';
|
|
8038
|
+
import type { createAppApi } from './api.ts';
|
|
8039
|
+
|
|
8040
|
+
export type Api = ReturnType<typeof createAppApi>;
|
|
8041
|
+
|
|
8042
|
+
const ApiContext = createContext<Api | null>(null);
|
|
8043
|
+
|
|
8044
|
+
export function ApiProvider({
|
|
8045
|
+
api,
|
|
8046
|
+
children,
|
|
8047
|
+
}: {
|
|
8048
|
+
api: Api;
|
|
8049
|
+
children: ReactNode;
|
|
8050
|
+
}) {
|
|
8051
|
+
return <ApiContext.Provider value={api}>{children}</ApiContext.Provider>;
|
|
8052
|
+
}
|
|
8053
|
+
|
|
8054
|
+
export function useApi(): Api {
|
|
8055
|
+
const api = use(ApiContext);
|
|
8056
|
+
if (!api) {
|
|
8057
|
+
throw new Error('useApi must be used within an ApiProvider');
|
|
8058
|
+
}
|
|
8059
|
+
return api;
|
|
8060
|
+
}
|
|
8061
|
+
`;
|
|
8062
|
+
const layoutTsx = `import '../global.css';
|
|
8063
|
+
|
|
8064
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
8065
|
+
import { Stack, useRouter, useSegments } from 'expo-router';
|
|
8066
|
+
import { StatusBar } from 'expo-status-bar';
|
|
8067
|
+
import { useEffect, useMemo } from 'react';
|
|
8068
|
+
import 'react-native-reanimated';
|
|
8069
|
+
|
|
8070
|
+
import { authClient } from '@/lib/auth-client.ts';
|
|
8071
|
+
import { ApiProvider } from '@/lib/api-context.tsx';
|
|
8072
|
+
import { createAppApi } from '@/lib/api.ts';
|
|
8073
|
+
import { queryClient } from '@/lib/query-client.ts';
|
|
8074
|
+
|
|
8075
|
+
function AuthGate({ children }: { children: React.ReactNode }) {
|
|
8076
|
+
const router = useRouter();
|
|
8077
|
+
const segments = useSegments();
|
|
8078
|
+
const { data: session, isPending } = authClient.useSession();
|
|
8079
|
+
|
|
8080
|
+
useEffect(() => {
|
|
8081
|
+
if (isPending) return;
|
|
8082
|
+
const onAuthRoute = segments[0] === 'login';
|
|
8083
|
+
if (!session && !onAuthRoute) {
|
|
8084
|
+
router.replace('/login');
|
|
8085
|
+
} else if (session && onAuthRoute) {
|
|
8086
|
+
router.replace('/');
|
|
8087
|
+
}
|
|
8088
|
+
}, [isPending, session, segments, router]);
|
|
8089
|
+
|
|
8090
|
+
return <>{children}</>;
|
|
8091
|
+
}
|
|
8092
|
+
|
|
8093
|
+
export default function RootLayout() {
|
|
8094
|
+
const api = useMemo(() => createAppApi(), []);
|
|
8095
|
+
|
|
8096
|
+
return (
|
|
8097
|
+
<QueryClientProvider client={queryClient}>
|
|
8098
|
+
<ApiProvider api={api}>
|
|
8099
|
+
<AuthGate>
|
|
8100
|
+
<Stack screenOptions={{ headerShown: false }}>
|
|
8101
|
+
<Stack.Screen name="index" />
|
|
8102
|
+
<Stack.Screen name="login" />
|
|
8103
|
+
</Stack>
|
|
8104
|
+
</AuthGate>
|
|
8105
|
+
<StatusBar style="auto" />
|
|
8106
|
+
</ApiProvider>
|
|
8107
|
+
</QueryClientProvider>
|
|
8108
|
+
);
|
|
8109
|
+
}
|
|
8110
|
+
`;
|
|
8111
|
+
const indexTsx = `import { Pressable, Text, View } from 'react-native';
|
|
8112
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
8113
|
+
|
|
8114
|
+
import { signOut, useSession } from '@/lib/auth-client.ts';
|
|
8115
|
+
import { useApi } from '@/lib/api-context.tsx';
|
|
8116
|
+
|
|
8117
|
+
export default function Home() {
|
|
8118
|
+
const { data: session } = useSession();
|
|
8119
|
+
const api = useApi();
|
|
8120
|
+
const { data: health } = api.useQuery('GET /health', {});
|
|
8121
|
+
|
|
8122
|
+
return (
|
|
8123
|
+
<SafeAreaView className="flex-1 bg-white">
|
|
8124
|
+
<View className="flex-1 items-center justify-center gap-4 px-6">
|
|
8125
|
+
<Text className="text-3xl font-bold text-slate-900">
|
|
8126
|
+
Welcome to ${options.name}
|
|
8127
|
+
</Text>
|
|
8128
|
+
{session ? (
|
|
8129
|
+
<Text className="text-sm text-slate-500">
|
|
8130
|
+
Signed in as {session.user.email}
|
|
8131
|
+
</Text>
|
|
8132
|
+
) : null}
|
|
8133
|
+
{health ? (
|
|
8134
|
+
<Text className="text-xs text-slate-400">
|
|
8135
|
+
API: {JSON.stringify(health)}
|
|
8136
|
+
</Text>
|
|
8137
|
+
) : null}
|
|
8138
|
+
<Pressable
|
|
8139
|
+
onPress={signOut}
|
|
8140
|
+
className="mt-4 rounded-lg bg-slate-900 px-6 py-3"
|
|
8141
|
+
>
|
|
8142
|
+
<Text className="font-semibold text-white">Sign out</Text>
|
|
8143
|
+
</Pressable>
|
|
8144
|
+
</View>
|
|
8145
|
+
</SafeAreaView>
|
|
8146
|
+
);
|
|
8147
|
+
}
|
|
8148
|
+
`;
|
|
8149
|
+
const loginTsx = `import * as Linking from 'expo-linking';
|
|
8150
|
+
import { useState } from 'react';
|
|
8151
|
+
import {
|
|
8152
|
+
KeyboardAvoidingView,
|
|
8153
|
+
Platform,
|
|
8154
|
+
Pressable,
|
|
8155
|
+
Text,
|
|
8156
|
+
TextInput,
|
|
8157
|
+
View,
|
|
8158
|
+
} from 'react-native';
|
|
8159
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
8160
|
+
|
|
8161
|
+
import { authClient } from '@/lib/auth-client.ts';
|
|
8162
|
+
|
|
8163
|
+
const EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
|
|
8164
|
+
|
|
8165
|
+
export default function LoginScreen() {
|
|
8166
|
+
const [email, setEmail] = useState('');
|
|
8167
|
+
const [submitting, setSubmitting] = useState(false);
|
|
8168
|
+
const [sent, setSent] = useState(false);
|
|
8169
|
+
const [error, setError] = useState<string | null>(null);
|
|
8170
|
+
|
|
8171
|
+
async function handleSubmit() {
|
|
8172
|
+
if (!EMAIL_RE.test(email.trim())) {
|
|
8173
|
+
setError('Enter a valid email address');
|
|
8174
|
+
return;
|
|
8175
|
+
}
|
|
8176
|
+
setError(null);
|
|
8177
|
+
setSubmitting(true);
|
|
8178
|
+
|
|
8179
|
+
const callbackURL = Linking.createURL('/').replace(/^(\\w+):\\/\\/\\//, '$1://');
|
|
8180
|
+
try {
|
|
8181
|
+
const result = await authClient.signIn.magicLink({
|
|
8182
|
+
email: email.trim(),
|
|
8183
|
+
callbackURL,
|
|
8184
|
+
});
|
|
8185
|
+
if (result.error) {
|
|
8186
|
+
setError(result.error.message ?? 'Could not send magic link');
|
|
8187
|
+
return;
|
|
8188
|
+
}
|
|
8189
|
+
setSent(true);
|
|
8190
|
+
} catch (err) {
|
|
8191
|
+
setError(err instanceof Error ? err.message : 'Something went wrong');
|
|
8192
|
+
} finally {
|
|
8193
|
+
setSubmitting(false);
|
|
8194
|
+
}
|
|
8195
|
+
}
|
|
8196
|
+
|
|
8197
|
+
if (sent) {
|
|
8198
|
+
return (
|
|
8199
|
+
<SafeAreaView className="flex-1 bg-white">
|
|
8200
|
+
<View className="flex-1 items-center justify-center px-6">
|
|
8201
|
+
<Text className="text-2xl font-bold text-slate-900">
|
|
8202
|
+
Check your inbox
|
|
8203
|
+
</Text>
|
|
8204
|
+
<Text className="mt-3 text-center text-slate-500">
|
|
8205
|
+
We sent a sign-in link to {email.trim()}
|
|
8206
|
+
</Text>
|
|
8207
|
+
</View>
|
|
8208
|
+
</SafeAreaView>
|
|
8209
|
+
);
|
|
8210
|
+
}
|
|
8211
|
+
|
|
8212
|
+
return (
|
|
8213
|
+
<SafeAreaView className="flex-1 bg-white">
|
|
8214
|
+
<KeyboardAvoidingView
|
|
8215
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
8216
|
+
className="flex-1"
|
|
8217
|
+
>
|
|
8218
|
+
<View className="flex-1 justify-center px-6">
|
|
8219
|
+
<Text className="text-3xl font-bold text-slate-900">Sign in</Text>
|
|
8220
|
+
<Text className="mt-1 text-sm text-slate-500">
|
|
8221
|
+
We'll email you a one-time sign-in link.
|
|
8222
|
+
</Text>
|
|
8223
|
+
|
|
8224
|
+
<Text className="mt-6 text-xs font-semibold uppercase text-slate-500">
|
|
8225
|
+
Email
|
|
8226
|
+
</Text>
|
|
8227
|
+
<TextInput
|
|
8228
|
+
value={email}
|
|
8229
|
+
onChangeText={(v) => {
|
|
8230
|
+
setEmail(v);
|
|
8231
|
+
if (error) setError(null);
|
|
8232
|
+
}}
|
|
8233
|
+
autoCapitalize="none"
|
|
8234
|
+
autoCorrect={false}
|
|
8235
|
+
keyboardType="email-address"
|
|
8236
|
+
placeholder="you@example.com"
|
|
8237
|
+
placeholderTextColor="#94a3b8"
|
|
8238
|
+
editable={!submitting}
|
|
8239
|
+
className="mt-2 rounded-lg border border-slate-300 px-4 py-3"
|
|
8240
|
+
/>
|
|
8241
|
+
{error ? (
|
|
8242
|
+
<Text className="mt-2 text-sm text-red-600">{error}</Text>
|
|
8243
|
+
) : null}
|
|
8244
|
+
|
|
8245
|
+
<Pressable
|
|
8246
|
+
onPress={handleSubmit}
|
|
8247
|
+
disabled={submitting}
|
|
8248
|
+
className="mt-6 items-center rounded-lg bg-slate-900 py-4 disabled:opacity-60"
|
|
8249
|
+
>
|
|
8250
|
+
<Text className="font-semibold text-white">
|
|
8251
|
+
{submitting ? 'Sending…' : 'Email me a sign-in link'}
|
|
8252
|
+
</Text>
|
|
8253
|
+
</Pressable>
|
|
8254
|
+
</View>
|
|
8255
|
+
</KeyboardAvoidingView>
|
|
8256
|
+
</SafeAreaView>
|
|
8257
|
+
);
|
|
8258
|
+
}
|
|
8259
|
+
`;
|
|
8260
|
+
const gitignore = `node_modules/
|
|
8261
|
+
.expo/
|
|
8262
|
+
dist/
|
|
8263
|
+
*.log
|
|
8264
|
+
.env*.local
|
|
8265
|
+
ios/
|
|
8266
|
+
android/
|
|
8267
|
+
.eas/
|
|
8268
|
+
`;
|
|
8269
|
+
return [
|
|
8270
|
+
{
|
|
8271
|
+
path: "apps/app/package.json",
|
|
8272
|
+
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
8273
|
+
},
|
|
8274
|
+
{
|
|
8275
|
+
path: "apps/app/app.config.ts",
|
|
8276
|
+
content: appConfig
|
|
8277
|
+
},
|
|
8278
|
+
{
|
|
8279
|
+
path: "apps/app/eas.json",
|
|
8280
|
+
content: `${JSON.stringify(easJson, null, 2)}\n`
|
|
8281
|
+
},
|
|
8282
|
+
{
|
|
8283
|
+
path: "apps/app/babel.config.js",
|
|
8284
|
+
content: babelConfig
|
|
8285
|
+
},
|
|
8286
|
+
{
|
|
8287
|
+
path: "apps/app/metro.config.js",
|
|
8288
|
+
content: metroConfig
|
|
8289
|
+
},
|
|
8290
|
+
{
|
|
8291
|
+
path: "apps/app/tsconfig.json",
|
|
8292
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
8293
|
+
},
|
|
8294
|
+
{
|
|
8295
|
+
path: "apps/app/tailwind.config.ts",
|
|
8296
|
+
content: tailwindConfig
|
|
8297
|
+
},
|
|
8298
|
+
{
|
|
8299
|
+
path: "apps/app/global.css",
|
|
8300
|
+
content: globalCss
|
|
8301
|
+
},
|
|
8302
|
+
{
|
|
8303
|
+
path: "apps/app/expo-env.d.ts",
|
|
8304
|
+
content: expoEnvDts
|
|
8305
|
+
},
|
|
8306
|
+
{
|
|
8307
|
+
path: "apps/app/nativewind-env.d.ts",
|
|
8308
|
+
content: nativewindEnvDts
|
|
8309
|
+
},
|
|
8310
|
+
{
|
|
8311
|
+
path: "apps/app/config.ts",
|
|
8312
|
+
content: configTs
|
|
8313
|
+
},
|
|
8314
|
+
{
|
|
8315
|
+
path: "apps/app/lib/query-client.ts",
|
|
8316
|
+
content: queryClientTs
|
|
8317
|
+
},
|
|
8318
|
+
{
|
|
8319
|
+
path: "apps/app/lib/auth-client.ts",
|
|
8320
|
+
content: authClientTs
|
|
8321
|
+
},
|
|
8322
|
+
{
|
|
8323
|
+
path: "apps/app/lib/api.ts",
|
|
8324
|
+
content: apiTs
|
|
8325
|
+
},
|
|
8326
|
+
{
|
|
8327
|
+
path: "apps/app/lib/api-context.tsx",
|
|
8328
|
+
content: apiContextTsx
|
|
8329
|
+
},
|
|
8330
|
+
{
|
|
8331
|
+
path: "apps/app/app/_layout.tsx",
|
|
8332
|
+
content: layoutTsx
|
|
8333
|
+
},
|
|
8334
|
+
{
|
|
8335
|
+
path: "apps/app/app/index.tsx",
|
|
8336
|
+
content: indexTsx
|
|
8337
|
+
},
|
|
8338
|
+
{
|
|
8339
|
+
path: "apps/app/app/login.tsx",
|
|
8340
|
+
content: loginTsx
|
|
8341
|
+
},
|
|
8342
|
+
{
|
|
8343
|
+
path: "apps/app/.gitignore",
|
|
8344
|
+
content: gitignore
|
|
8345
|
+
}
|
|
8346
|
+
];
|
|
8347
|
+
}
|
|
8348
|
+
|
|
7652
8349
|
//#endregion
|
|
7653
8350
|
//#region src/init/generators/models.ts
|
|
7654
8351
|
/**
|
|
@@ -8038,18 +8735,55 @@ export default defineConfig({
|
|
|
8038
8735
|
return files;
|
|
8039
8736
|
}
|
|
8040
8737
|
/**
|
|
8041
|
-
* Generate
|
|
8738
|
+
* Generate the frontend app entry for the workspace config based on the
|
|
8739
|
+
* selected `frontendFramework`. Defaults to Next.js for backwards compat.
|
|
8042
8740
|
*/
|
|
8043
|
-
function
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8741
|
+
function generateFrontendAppEntry(options) {
|
|
8742
|
+
switch (options.frontendFramework) {
|
|
8743
|
+
case "tanstack-start": return `web: {
|
|
8744
|
+
type: 'frontend',
|
|
8745
|
+
framework: 'tanstack-start',
|
|
8746
|
+
path: 'apps/web',
|
|
8747
|
+
port: 3001,
|
|
8748
|
+
dependencies: ['api', 'auth'],
|
|
8749
|
+
config: {
|
|
8750
|
+
client: './src/config/client.ts',
|
|
8751
|
+
server: './src/config/server.ts',
|
|
8752
|
+
},
|
|
8753
|
+
},`;
|
|
8754
|
+
case "expo": return `app: {
|
|
8755
|
+
type: 'frontend',
|
|
8756
|
+
framework: 'expo',
|
|
8757
|
+
path: 'apps/app',
|
|
8758
|
+
port: 8081,
|
|
8759
|
+
dependencies: ['api', 'auth'],
|
|
8760
|
+
},`;
|
|
8761
|
+
default: return `web: {
|
|
8762
|
+
type: 'frontend',
|
|
8763
|
+
framework: 'nextjs',
|
|
8764
|
+
path: 'apps/web',
|
|
8765
|
+
port: 3001,
|
|
8766
|
+
dependencies: ['api', 'auth'],
|
|
8767
|
+
config: {
|
|
8768
|
+
client: './src/config/client.ts',
|
|
8769
|
+
server: './src/config/server.ts',
|
|
8770
|
+
},
|
|
8771
|
+
},`;
|
|
8772
|
+
}
|
|
8773
|
+
}
|
|
8774
|
+
/**
|
|
8775
|
+
* Generate gkm.config.ts with defineWorkspace for fullstack template
|
|
8776
|
+
*/
|
|
8777
|
+
function generateWorkspaceConfig(options) {
|
|
8778
|
+
const { telescope, services, deployTarget, routesStructure } = options;
|
|
8779
|
+
const getRoutesGlob = () => {
|
|
8780
|
+
switch (routesStructure) {
|
|
8781
|
+
case "centralized-endpoints": return "./src/endpoints/**/*.ts";
|
|
8782
|
+
case "centralized-routes": return "./src/routes/**/*.ts";
|
|
8783
|
+
case "domain-based": return "./src/**/routes/*.ts";
|
|
8784
|
+
}
|
|
8785
|
+
};
|
|
8786
|
+
let config$1 = `import { defineWorkspace } from '@geekmidas/cli/config';
|
|
8053
8787
|
|
|
8054
8788
|
export default defineWorkspace({
|
|
8055
8789
|
name: '${options.name}',
|
|
@@ -8081,17 +8815,7 @@ export default defineWorkspace({
|
|
|
8081
8815
|
envParser: './src/config/env#envParser',
|
|
8082
8816
|
logger: './src/config/logger#logger',
|
|
8083
8817
|
},
|
|
8084
|
-
|
|
8085
|
-
type: 'frontend',
|
|
8086
|
-
framework: 'nextjs',
|
|
8087
|
-
path: 'apps/web',
|
|
8088
|
-
port: 3001,
|
|
8089
|
-
dependencies: ['api', 'auth'],
|
|
8090
|
-
config: {
|
|
8091
|
-
client: './src/config/client.ts',
|
|
8092
|
-
server: './src/config/server.ts',
|
|
8093
|
-
},
|
|
8094
|
-
},
|
|
8818
|
+
${generateFrontendAppEntry(options)}
|
|
8095
8819
|
},
|
|
8096
8820
|
shared: {
|
|
8097
8821
|
packages: ['packages/*'],
|
|
@@ -8994,6 +9718,26 @@ const templateChoices = [{
|
|
|
8994
9718
|
description: "Monorepo with API + Next.js + shared models"
|
|
8995
9719
|
}];
|
|
8996
9720
|
/**
|
|
9721
|
+
* Frontend framework choices for the fullstack template prompt.
|
|
9722
|
+
*/
|
|
9723
|
+
const frontendFrameworkChoices = [
|
|
9724
|
+
{
|
|
9725
|
+
title: "Next.js",
|
|
9726
|
+
value: "nextjs",
|
|
9727
|
+
description: "React server components, app router (default)"
|
|
9728
|
+
},
|
|
9729
|
+
{
|
|
9730
|
+
title: "TanStack Start",
|
|
9731
|
+
value: "tanstack-start",
|
|
9732
|
+
description: "File-based router on Vite + Nitro"
|
|
9733
|
+
},
|
|
9734
|
+
{
|
|
9735
|
+
title: "Expo",
|
|
9736
|
+
value: "expo",
|
|
9737
|
+
description: "React Native mobile app (iOS + Android)"
|
|
9738
|
+
}
|
|
9739
|
+
];
|
|
9740
|
+
/**
|
|
8997
9741
|
* Logger type choices for prompts
|
|
8998
9742
|
*/
|
|
8999
9743
|
const loggerTypeChoices = [{
|
|
@@ -11101,6 +11845,382 @@ node_modules/
|
|
|
11101
11845
|
];
|
|
11102
11846
|
}
|
|
11103
11847
|
|
|
11848
|
+
//#endregion
|
|
11849
|
+
//#region src/init/generators/web-tanstack.ts
|
|
11850
|
+
/**
|
|
11851
|
+
* Generate TanStack Start web app files for the fullstack template.
|
|
11852
|
+
*
|
|
11853
|
+
* Mirrors the Next.js scaffold (apps/web) but on Vite + TanStack Start +
|
|
11854
|
+
* TanStack Router with the `VITE_` env-var convention.
|
|
11855
|
+
*/
|
|
11856
|
+
function generateTanStackWebFiles(options) {
|
|
11857
|
+
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
11858
|
+
const packageName = `@${options.name}/web`;
|
|
11859
|
+
const apiPackage = `@${options.name}/api`;
|
|
11860
|
+
const modelsPackage = `@${options.name}/models`;
|
|
11861
|
+
const uiPackage = `@${options.name}/ui`;
|
|
11862
|
+
const packageJson = {
|
|
11863
|
+
name: packageName,
|
|
11864
|
+
version: "0.0.1",
|
|
11865
|
+
private: true,
|
|
11866
|
+
type: "module",
|
|
11867
|
+
scripts: {
|
|
11868
|
+
dev: "gkm exec -- vite dev",
|
|
11869
|
+
build: "gkm exec -- vite build",
|
|
11870
|
+
start: "node .output/server/index.mjs",
|
|
11871
|
+
typecheck: "tsc --noEmit"
|
|
11872
|
+
},
|
|
11873
|
+
dependencies: {
|
|
11874
|
+
[apiPackage]: "workspace:*",
|
|
11875
|
+
[modelsPackage]: "workspace:*",
|
|
11876
|
+
[uiPackage]: "workspace:*",
|
|
11877
|
+
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
11878
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
11879
|
+
"@tanstack/react-query": "~5.80.0",
|
|
11880
|
+
"@tanstack/react-router": "^1.87.0",
|
|
11881
|
+
"@tanstack/react-start": "^1.87.0",
|
|
11882
|
+
"better-auth": "~1.2.0",
|
|
11883
|
+
react: "~19.2.0",
|
|
11884
|
+
"react-dom": "~19.2.0"
|
|
11885
|
+
},
|
|
11886
|
+
devDependencies: {
|
|
11887
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
11888
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
11889
|
+
"@tanstack/router-plugin": "^1.87.0",
|
|
11890
|
+
"@types/node": "~22.0.0",
|
|
11891
|
+
"@types/react": "~19.0.0",
|
|
11892
|
+
"@types/react-dom": "~19.0.0",
|
|
11893
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
11894
|
+
tailwindcss: "^4.0.0",
|
|
11895
|
+
tsx: "~4.20.0",
|
|
11896
|
+
typescript: "~5.8.2",
|
|
11897
|
+
vite: "^7.0.0",
|
|
11898
|
+
"vite-tsconfig-paths": "~5.1.0"
|
|
11899
|
+
}
|
|
11900
|
+
};
|
|
11901
|
+
const viteConfig = `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
11902
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
11903
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
|
11904
|
+
import viteReact from '@vitejs/plugin-react';
|
|
11905
|
+
import { defineConfig } from 'vite';
|
|
11906
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
11907
|
+
|
|
11908
|
+
export default defineConfig({
|
|
11909
|
+
server: {
|
|
11910
|
+
port: Number(Credentials.PORT ?? 3001),
|
|
11911
|
+
},
|
|
11912
|
+
// VITE_* vars are inlined automatically; no manual loadEnv needed.
|
|
11913
|
+
envPrefix: 'VITE_',
|
|
11914
|
+
plugins: [
|
|
11915
|
+
tsconfigPaths({ root: import.meta.dirname }),
|
|
11916
|
+
tailwindcss(),
|
|
11917
|
+
tanstackStart(),
|
|
11918
|
+
viteReact(),
|
|
11919
|
+
],
|
|
11920
|
+
});
|
|
11921
|
+
`;
|
|
11922
|
+
const tsConfig = {
|
|
11923
|
+
extends: "../../tsconfig.json",
|
|
11924
|
+
compilerOptions: {
|
|
11925
|
+
lib: [
|
|
11926
|
+
"dom",
|
|
11927
|
+
"dom.iterable",
|
|
11928
|
+
"ES2022"
|
|
11929
|
+
],
|
|
11930
|
+
allowJs: true,
|
|
11931
|
+
skipLibCheck: true,
|
|
11932
|
+
strict: true,
|
|
11933
|
+
noEmit: true,
|
|
11934
|
+
allowImportingTsExtensions: true,
|
|
11935
|
+
esModuleInterop: true,
|
|
11936
|
+
module: "ESNext",
|
|
11937
|
+
moduleResolution: "bundler",
|
|
11938
|
+
resolveJsonModule: true,
|
|
11939
|
+
isolatedModules: true,
|
|
11940
|
+
jsx: "preserve",
|
|
11941
|
+
incremental: true,
|
|
11942
|
+
baseUrl: ".",
|
|
11943
|
+
types: ["vite/client"],
|
|
11944
|
+
paths: {
|
|
11945
|
+
"~/*": ["./src/*", "../../packages/ui/src/*"],
|
|
11946
|
+
[modelsPackage]: ["../../packages/models/src"],
|
|
11947
|
+
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
11948
|
+
[uiPackage]: ["../../packages/ui/src"],
|
|
11949
|
+
[`${uiPackage}/*`]: ["../../packages/ui/src/*"],
|
|
11950
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
11951
|
+
}
|
|
11952
|
+
},
|
|
11953
|
+
include: [
|
|
11954
|
+
"**/*.ts",
|
|
11955
|
+
"**/*.tsx",
|
|
11956
|
+
"src/routeTree.gen.ts"
|
|
11957
|
+
],
|
|
11958
|
+
exclude: [
|
|
11959
|
+
"node_modules",
|
|
11960
|
+
".output",
|
|
11961
|
+
".vinxi",
|
|
11962
|
+
"dist"
|
|
11963
|
+
]
|
|
11964
|
+
};
|
|
11965
|
+
const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
|
|
11966
|
+
|
|
11967
|
+
function makeQueryClient() {
|
|
11968
|
+
return new QueryClient({
|
|
11969
|
+
defaultOptions: {
|
|
11970
|
+
queries: {
|
|
11971
|
+
staleTime: 60 * 1000,
|
|
11972
|
+
},
|
|
11973
|
+
},
|
|
11974
|
+
});
|
|
11975
|
+
}
|
|
11976
|
+
|
|
11977
|
+
let browserQueryClient: QueryClient | undefined = undefined;
|
|
11978
|
+
|
|
11979
|
+
export function getQueryClient() {
|
|
11980
|
+
if (typeof window === 'undefined') {
|
|
11981
|
+
return makeQueryClient();
|
|
11982
|
+
}
|
|
11983
|
+
if (!browserQueryClient) browserQueryClient = makeQueryClient();
|
|
11984
|
+
return browserQueryClient;
|
|
11985
|
+
}
|
|
11986
|
+
`;
|
|
11987
|
+
const clientConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
11988
|
+
|
|
11989
|
+
// Client config - VITE_* vars are inlined at build time.
|
|
11990
|
+
const envParser = new EnvironmentParser({
|
|
11991
|
+
VITE_API_URL: import.meta.env.VITE_API_URL,
|
|
11992
|
+
VITE_AUTH_URL: import.meta.env.VITE_AUTH_URL,
|
|
11993
|
+
});
|
|
11994
|
+
|
|
11995
|
+
export const clientConfig = envParser
|
|
11996
|
+
.create((get) => ({
|
|
11997
|
+
apiUrl: get('VITE_API_URL').string(),
|
|
11998
|
+
authUrl: get('VITE_AUTH_URL').string(),
|
|
11999
|
+
}))
|
|
12000
|
+
.parse();
|
|
12001
|
+
`;
|
|
12002
|
+
const serverConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
12003
|
+
|
|
12004
|
+
// Server config - all env vars (server-side only, not exposed to browser).
|
|
12005
|
+
// Access these only in TanStack Start server functions / server routes.
|
|
12006
|
+
const envParser = new EnvironmentParser({ ...process.env });
|
|
12007
|
+
|
|
12008
|
+
export const serverConfig = envParser
|
|
12009
|
+
.create((_get) => ({
|
|
12010
|
+
// Add server-only secrets here
|
|
12011
|
+
// Example: stripeSecretKey: _get('STRIPE_SECRET_KEY').string(),
|
|
12012
|
+
}))
|
|
12013
|
+
.parse();
|
|
12014
|
+
`;
|
|
12015
|
+
const authClientTs = `import { magicLinkClient } from 'better-auth/client/plugins';
|
|
12016
|
+
import { createAuthClient } from 'better-auth/react';
|
|
12017
|
+
import { clientConfig } from '~/config/client.ts';
|
|
12018
|
+
|
|
12019
|
+
export const authClient = createAuthClient({
|
|
12020
|
+
baseURL: clientConfig.authUrl,
|
|
12021
|
+
plugins: [magicLinkClient()],
|
|
12022
|
+
});
|
|
12023
|
+
|
|
12024
|
+
export const { signIn, signUp, signOut, useSession, magicLink } = authClient;
|
|
12025
|
+
`;
|
|
12026
|
+
const apiIndexTs = `import { createApi } from '${apiPackage}/client';
|
|
12027
|
+
import { clientConfig } from '~/config/client.ts';
|
|
12028
|
+
import { getQueryClient } from '~/lib/query-client.ts';
|
|
12029
|
+
|
|
12030
|
+
export function createAppApi(options?: { headers?: Record<string, string> }) {
|
|
12031
|
+
return createApi({
|
|
12032
|
+
baseURL: clientConfig.apiUrl,
|
|
12033
|
+
queryClient: getQueryClient(),
|
|
12034
|
+
headers: options?.headers,
|
|
12035
|
+
onRequest: (config) => ({ ...config, credentials: 'include' }),
|
|
12036
|
+
});
|
|
12037
|
+
}
|
|
12038
|
+
|
|
12039
|
+
export const api = createAppApi();
|
|
12040
|
+
`;
|
|
12041
|
+
const routerTs = `import { createRouter } from '@tanstack/react-router';
|
|
12042
|
+
import { routeTree } from './routeTree.gen.ts';
|
|
12043
|
+
|
|
12044
|
+
export function getRouter() {
|
|
12045
|
+
return createRouter({
|
|
12046
|
+
routeTree,
|
|
12047
|
+
scrollRestoration: true,
|
|
12048
|
+
defaultPreload: 'intent',
|
|
12049
|
+
});
|
|
12050
|
+
}
|
|
12051
|
+
|
|
12052
|
+
declare module '@tanstack/react-router' {
|
|
12053
|
+
interface Register {
|
|
12054
|
+
router: ReturnType<typeof getRouter>;
|
|
12055
|
+
}
|
|
12056
|
+
}
|
|
12057
|
+
`;
|
|
12058
|
+
const routeTreeGen = `// This file is auto-generated by @tanstack/router-plugin.
|
|
12059
|
+
// It will be overwritten on the next \`vite dev\` run.
|
|
12060
|
+
import { rootRouteWithContext } from '@tanstack/react-router';
|
|
12061
|
+
|
|
12062
|
+
export const routeTree = rootRouteWithContext()({
|
|
12063
|
+
// Routes are populated by the plugin from src/routes/**.tsx
|
|
12064
|
+
} as never);
|
|
12065
|
+
`;
|
|
12066
|
+
const rootRouteTsx = `/// <reference types="vite/client" />
|
|
12067
|
+
import '~/styles/globals.css';
|
|
12068
|
+
|
|
12069
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
12070
|
+
import {
|
|
12071
|
+
HeadContent,
|
|
12072
|
+
Outlet,
|
|
12073
|
+
Scripts,
|
|
12074
|
+
createRootRoute,
|
|
12075
|
+
} from '@tanstack/react-router';
|
|
12076
|
+
import type { ReactNode } from 'react';
|
|
12077
|
+
import { getQueryClient } from '~/lib/query-client.ts';
|
|
12078
|
+
|
|
12079
|
+
export const Route = createRootRoute({
|
|
12080
|
+
head: () => ({
|
|
12081
|
+
meta: [
|
|
12082
|
+
{ charSet: 'utf-8' },
|
|
12083
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
12084
|
+
{ title: '${options.name}' },
|
|
12085
|
+
],
|
|
12086
|
+
}),
|
|
12087
|
+
component: RootComponent,
|
|
12088
|
+
});
|
|
12089
|
+
|
|
12090
|
+
function RootComponent() {
|
|
12091
|
+
const queryClient = getQueryClient();
|
|
12092
|
+
return (
|
|
12093
|
+
<RootDocument>
|
|
12094
|
+
<QueryClientProvider client={queryClient}>
|
|
12095
|
+
<Outlet />
|
|
12096
|
+
</QueryClientProvider>
|
|
12097
|
+
</RootDocument>
|
|
12098
|
+
);
|
|
12099
|
+
}
|
|
12100
|
+
|
|
12101
|
+
function RootDocument({ children }: { children: ReactNode }) {
|
|
12102
|
+
return (
|
|
12103
|
+
<html lang="en">
|
|
12104
|
+
<head>
|
|
12105
|
+
<HeadContent />
|
|
12106
|
+
</head>
|
|
12107
|
+
<body>
|
|
12108
|
+
{children}
|
|
12109
|
+
<Scripts />
|
|
12110
|
+
</body>
|
|
12111
|
+
</html>
|
|
12112
|
+
);
|
|
12113
|
+
}
|
|
12114
|
+
`;
|
|
12115
|
+
const indexRouteTsx = `import { createFileRoute } from '@tanstack/react-router';
|
|
12116
|
+
import { api } from '~/api/index.ts';
|
|
12117
|
+
|
|
12118
|
+
export const Route = createFileRoute('/')({
|
|
12119
|
+
component: HomeComponent,
|
|
12120
|
+
});
|
|
12121
|
+
|
|
12122
|
+
function HomeComponent() {
|
|
12123
|
+
const { data: health } = api.useQuery('GET /health', {});
|
|
12124
|
+
|
|
12125
|
+
return (
|
|
12126
|
+
<main className="min-h-screen bg-background p-8">
|
|
12127
|
+
<div className="mx-auto max-w-4xl space-y-8">
|
|
12128
|
+
<div className="space-y-2">
|
|
12129
|
+
<h1 className="text-4xl font-bold tracking-tight">
|
|
12130
|
+
Welcome to ${options.name}
|
|
12131
|
+
</h1>
|
|
12132
|
+
<p className="text-muted-foreground">
|
|
12133
|
+
Your TanStack Start app is ready.
|
|
12134
|
+
</p>
|
|
12135
|
+
</div>
|
|
12136
|
+
|
|
12137
|
+
<section className="rounded-lg border p-6">
|
|
12138
|
+
<h2 className="text-xl font-semibold">API Status</h2>
|
|
12139
|
+
{health ? (
|
|
12140
|
+
<pre className="mt-4 rounded bg-muted p-4 text-sm">
|
|
12141
|
+
{JSON.stringify(health, null, 2)}
|
|
12142
|
+
</pre>
|
|
12143
|
+
) : (
|
|
12144
|
+
<p className="mt-2 text-muted-foreground">Connecting…</p>
|
|
12145
|
+
)}
|
|
12146
|
+
</section>
|
|
12147
|
+
</div>
|
|
12148
|
+
</main>
|
|
12149
|
+
);
|
|
12150
|
+
}
|
|
12151
|
+
`;
|
|
12152
|
+
const globalsCss = `@import '${uiPackage}/styles';
|
|
12153
|
+
|
|
12154
|
+
@source "../..";
|
|
12155
|
+
@source "../../../../packages/ui/src";
|
|
12156
|
+
`;
|
|
12157
|
+
const gitignore = `.output/
|
|
12158
|
+
.vinxi/
|
|
12159
|
+
node_modules/
|
|
12160
|
+
.env.local
|
|
12161
|
+
*.log
|
|
12162
|
+
src/routeTree.gen.ts
|
|
12163
|
+
`;
|
|
12164
|
+
return [
|
|
12165
|
+
{
|
|
12166
|
+
path: "apps/web/package.json",
|
|
12167
|
+
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
12168
|
+
},
|
|
12169
|
+
{
|
|
12170
|
+
path: "apps/web/vite.config.ts",
|
|
12171
|
+
content: viteConfig
|
|
12172
|
+
},
|
|
12173
|
+
{
|
|
12174
|
+
path: "apps/web/tsconfig.json",
|
|
12175
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
12176
|
+
},
|
|
12177
|
+
{
|
|
12178
|
+
path: "apps/web/src/lib/query-client.ts",
|
|
12179
|
+
content: queryClientTs
|
|
12180
|
+
},
|
|
12181
|
+
{
|
|
12182
|
+
path: "apps/web/src/lib/auth-client.ts",
|
|
12183
|
+
content: authClientTs
|
|
12184
|
+
},
|
|
12185
|
+
{
|
|
12186
|
+
path: "apps/web/src/config/client.ts",
|
|
12187
|
+
content: clientConfigTs
|
|
12188
|
+
},
|
|
12189
|
+
{
|
|
12190
|
+
path: "apps/web/src/config/server.ts",
|
|
12191
|
+
content: serverConfigTs
|
|
12192
|
+
},
|
|
12193
|
+
{
|
|
12194
|
+
path: "apps/web/src/api/index.ts",
|
|
12195
|
+
content: apiIndexTs
|
|
12196
|
+
},
|
|
12197
|
+
{
|
|
12198
|
+
path: "apps/web/src/router.tsx",
|
|
12199
|
+
content: routerTs
|
|
12200
|
+
},
|
|
12201
|
+
{
|
|
12202
|
+
path: "apps/web/src/routeTree.gen.ts",
|
|
12203
|
+
content: routeTreeGen
|
|
12204
|
+
},
|
|
12205
|
+
{
|
|
12206
|
+
path: "apps/web/src/routes/__root.tsx",
|
|
12207
|
+
content: rootRouteTsx
|
|
12208
|
+
},
|
|
12209
|
+
{
|
|
12210
|
+
path: "apps/web/src/routes/index.tsx",
|
|
12211
|
+
content: indexRouteTsx
|
|
12212
|
+
},
|
|
12213
|
+
{
|
|
12214
|
+
path: "apps/web/src/styles/globals.css",
|
|
12215
|
+
content: globalsCss
|
|
12216
|
+
},
|
|
12217
|
+
{
|
|
12218
|
+
path: "apps/web/.gitignore",
|
|
12219
|
+
content: gitignore
|
|
12220
|
+
}
|
|
12221
|
+
];
|
|
12222
|
+
}
|
|
12223
|
+
|
|
11104
12224
|
//#endregion
|
|
11105
12225
|
//#region src/init/utils.ts
|
|
11106
12226
|
/**
|
|
@@ -11313,6 +12433,13 @@ async function initCommand(projectName, options = {}) {
|
|
|
11313
12433
|
message: "Routes structure:",
|
|
11314
12434
|
choices: routesStructureChoices,
|
|
11315
12435
|
initial: 0
|
|
12436
|
+
},
|
|
12437
|
+
{
|
|
12438
|
+
type: (_prev, values) => !options.yes && (options.template === "fullstack" || values.template === "fullstack") ? "select" : null,
|
|
12439
|
+
name: "frontendFramework",
|
|
12440
|
+
message: "Frontend framework:",
|
|
12441
|
+
choices: frontendFrameworkChoices,
|
|
12442
|
+
initial: 0
|
|
11316
12443
|
}
|
|
11317
12444
|
], { onCancel });
|
|
11318
12445
|
const name$1 = projectName || options.name || answers.name;
|
|
@@ -11354,6 +12481,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
11354
12481
|
const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
|
|
11355
12482
|
const deployTarget = options.yes ? "dokploy" : answers.deployTarget ?? "dokploy";
|
|
11356
12483
|
const database = services.db;
|
|
12484
|
+
const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
|
|
11357
12485
|
const templateOptions = {
|
|
11358
12486
|
name: name$1,
|
|
11359
12487
|
template,
|
|
@@ -11366,7 +12494,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
11366
12494
|
apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
|
|
11367
12495
|
packageManager: pkgManager,
|
|
11368
12496
|
deployTarget,
|
|
11369
|
-
services
|
|
12497
|
+
services,
|
|
12498
|
+
frontendFramework
|
|
11370
12499
|
};
|
|
11371
12500
|
const targetDir = join(cwd, name$1);
|
|
11372
12501
|
const baseTemplate = getTemplate(templateOptions.template);
|
|
@@ -11394,7 +12523,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
11394
12523
|
] : [];
|
|
11395
12524
|
const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
|
|
11396
12525
|
const rootFiles = baseTemplate ? [...generateMonorepoFiles(templateOptions, baseTemplate), ...generateModelsPackage(templateOptions)] : [];
|
|
11397
|
-
const webAppFiles = isFullstack ? generateWebAppFiles(templateOptions) : [];
|
|
12526
|
+
const webAppFiles = isFullstack ? frontendFramework === "tanstack-start" ? generateTanStackWebFiles(templateOptions) : frontendFramework === "expo" ? generateExpoAppFiles(templateOptions) : generateWebAppFiles(templateOptions) : [];
|
|
11398
12527
|
const authAppFiles = isFullstack ? generateAuthAppFiles(templateOptions) : [];
|
|
11399
12528
|
const uiPackageFiles = isFullstack ? generateUiPackageFiles(templateOptions) : [];
|
|
11400
12529
|
for (const { path, content } of rootFiles) {
|
|
@@ -11527,7 +12656,15 @@ function printNextSteps(projectName, options, pkgManager) {
|
|
|
11527
12656
|
console.log(` │ ├── api/ # Backend API`);
|
|
11528
12657
|
if (isFullstackTemplate(options.template)) {
|
|
11529
12658
|
console.log(` │ ├── auth/ # Auth service (better-auth)`);
|
|
11530
|
-
|
|
12659
|
+
switch (options.frontendFramework) {
|
|
12660
|
+
case "tanstack-start":
|
|
12661
|
+
console.log(` │ └── web/ # TanStack Start frontend`);
|
|
12662
|
+
break;
|
|
12663
|
+
case "expo":
|
|
12664
|
+
console.log(` │ └── app/ # Expo (React Native) app`);
|
|
12665
|
+
break;
|
|
12666
|
+
default: console.log(` │ └── web/ # Next.js frontend`);
|
|
12667
|
+
}
|
|
11531
12668
|
}
|
|
11532
12669
|
console.log(` ├── packages/`);
|
|
11533
12670
|
console.log(` │ ├── models/ # Shared Zod schemas`);
|
|
@@ -12361,11 +13498,11 @@ program.command("api").description("Manage REST API endpoints").action(() => {
|
|
|
12361
13498
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12362
13499
|
process.stdout.write("REST API management - coming soon\n");
|
|
12363
13500
|
});
|
|
12364
|
-
program.command("openapi").description("Generate OpenAPI specification from endpoints").action(async () => {
|
|
13501
|
+
program.command("openapi").description("Generate OpenAPI specification from endpoints").option("--app <name>", "Workspace mode: generate for a single named backend app").action(async (options) => {
|
|
12365
13502
|
try {
|
|
12366
13503
|
const globalOptions = program.opts();
|
|
12367
13504
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12368
|
-
await openapiCommand({});
|
|
13505
|
+
await openapiCommand({ app: options.app });
|
|
12369
13506
|
} catch (error) {
|
|
12370
13507
|
console.error(formatError(error));
|
|
12371
13508
|
process.exit(1);
|