mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
Added support for building mobile applications with Expo
This commit is contained in:
@@ -87,7 +87,7 @@ async function setupPwa(projectDir: string) {
|
||||
await fs.copy(pwaTemplateDir, projectDir, { overwrite: true });
|
||||
}
|
||||
|
||||
const clientPackageDir = path.join(projectDir, "apps/client");
|
||||
const clientPackageDir = path.join(projectDir, "apps/web");
|
||||
|
||||
addPackageDependency({
|
||||
dependencies: ["vite-plugin-pwa"],
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function setupAuth(
|
||||
}
|
||||
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
const clientDir = path.join(projectDir, "apps/client");
|
||||
const clientDir = path.join(projectDir, "apps/web");
|
||||
|
||||
try {
|
||||
addPackageDependency({
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
fixGitignoreFiles,
|
||||
setupAuthTemplate,
|
||||
setupBackendFramework,
|
||||
setupFrontendTemplates,
|
||||
setupOrmTemplate,
|
||||
} from "./template-manager";
|
||||
|
||||
@@ -29,6 +30,8 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
||||
await fs.ensureDir(projectDir);
|
||||
|
||||
await copyBaseTemplate(projectDir);
|
||||
await setupFrontendTemplates(projectDir, options.frontend);
|
||||
|
||||
await fixGitignoreFiles(projectDir);
|
||||
|
||||
await setupBackendFramework(projectDir, options.backendFramework);
|
||||
@@ -89,6 +92,7 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
||||
options.orm,
|
||||
options.addons,
|
||||
options.runtime,
|
||||
options.frontend,
|
||||
);
|
||||
|
||||
return projectDir;
|
||||
|
||||
@@ -57,7 +57,7 @@ Then, run the development server:
|
||||
${packageManagerRunCmd} dev
|
||||
\`\`\`
|
||||
|
||||
Open [http://localhost:3001](http://localhost:3001) in your browser to see the client application.
|
||||
Open [http://localhost:3001](http://localhost:3001) in your browser to see the web application.
|
||||
The API is running at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
## Project Structure
|
||||
@@ -65,7 +65,7 @@ The API is running at [http://localhost:3000](http://localhost:3000).
|
||||
\`\`\`
|
||||
${projectName}/
|
||||
├── apps/
|
||||
│ ├── client/ # Frontend application (React, TanStack Router)
|
||||
│ ├── web/ # Frontend application (React, TanStack Router)
|
||||
│ └── server/ # Backend API (Hono, tRPC)
|
||||
\`\`\`
|
||||
|
||||
@@ -173,9 +173,9 @@ function generateScriptsList(
|
||||
orm: ProjectOrm,
|
||||
auth: boolean,
|
||||
): string {
|
||||
let scripts = `- \`${packageManagerRunCmd} dev\`: Start both client and server in development mode
|
||||
- \`${packageManagerRunCmd} build\`: Build both client and server
|
||||
- \`${packageManagerRunCmd} dev:client\`: Start only the client
|
||||
let scripts = `- \`${packageManagerRunCmd} dev\`: Start both web and server in development mode
|
||||
- \`${packageManagerRunCmd} build\`: Build both web and server
|
||||
- \`${packageManagerRunCmd} dev:web\`: Start only the web application
|
||||
- \`${packageManagerRunCmd} dev:server\`: Start only the server
|
||||
- \`${packageManagerRunCmd} check-types\`: Check TypeScript types across all apps`;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ export async function setupEnvironmentVariables(
|
||||
options: ProjectConfig,
|
||||
): Promise<void> {
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
const clientDir = path.join(projectDir, "apps/client");
|
||||
|
||||
const envPath = path.join(serverDir, ".env");
|
||||
let envContent = "";
|
||||
@@ -49,16 +48,35 @@ export async function setupEnvironmentVariables(
|
||||
|
||||
await fs.writeFile(envPath, envContent.trim());
|
||||
|
||||
const clientEnvPath = path.join(clientDir, ".env");
|
||||
let clientEnvContent = "";
|
||||
if (options.frontend.includes("web")) {
|
||||
const clientDir = path.join(projectDir, "apps/web");
|
||||
const clientEnvPath = path.join(clientDir, ".env");
|
||||
let clientEnvContent = "";
|
||||
|
||||
if (await fs.pathExists(clientEnvPath)) {
|
||||
clientEnvContent = await fs.readFile(clientEnvPath, "utf8");
|
||||
if (await fs.pathExists(clientEnvPath)) {
|
||||
clientEnvContent = await fs.readFile(clientEnvPath, "utf8");
|
||||
}
|
||||
|
||||
if (!clientEnvContent.includes("VITE_SERVER_URL")) {
|
||||
clientEnvContent += "VITE_SERVER_URL=http://localhost:3000\n";
|
||||
}
|
||||
|
||||
await fs.writeFile(clientEnvPath, clientEnvContent.trim());
|
||||
}
|
||||
|
||||
if (!clientEnvContent.includes("VITE_SERVER_URL")) {
|
||||
clientEnvContent += "VITE_SERVER_URL=http://localhost:3000\n";
|
||||
}
|
||||
if (options.frontend.includes("native")) {
|
||||
const nativeDir = path.join(projectDir, "apps/native");
|
||||
const nativeEnvPath = path.join(nativeDir, ".env");
|
||||
let nativeEnvContent = "";
|
||||
|
||||
await fs.writeFile(clientEnvPath, clientEnvContent.trim());
|
||||
if (await fs.pathExists(nativeEnvPath)) {
|
||||
nativeEnvContent = await fs.readFile(nativeEnvPath, "utf8");
|
||||
}
|
||||
|
||||
if (!nativeEnvContent.includes("EXPO_PUBLIC_SERVER_URL")) {
|
||||
nativeEnvContent += "EXPO_PUBLIC_SERVER_URL=http://localhost:3000\n";
|
||||
}
|
||||
|
||||
await fs.writeFile(nativeEnvPath, nativeEnvContent.trim());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ async function setupTodoExample(
|
||||
): Promise<void> {
|
||||
const todoExampleDir = path.join(PKG_ROOT, "template/examples/todo");
|
||||
if (await fs.pathExists(todoExampleDir)) {
|
||||
const todoRouteDir = path.join(todoExampleDir, "apps/client/src/routes");
|
||||
const targetRouteDir = path.join(projectDir, "apps/client/src/routes");
|
||||
const todoRouteDir = path.join(todoExampleDir, "apps/web/src/routes");
|
||||
const targetRouteDir = path.join(projectDir, "apps/web/src/routes");
|
||||
await fs.copy(todoRouteDir, targetRouteDir, { overwrite: true });
|
||||
|
||||
if (orm !== "none") {
|
||||
@@ -55,7 +55,7 @@ async function updateHeaderWithTodoLink(
|
||||
): Promise<void> {
|
||||
const headerPath = path.join(
|
||||
projectDir,
|
||||
"apps/client/src/components/header.tsx",
|
||||
"apps/web/src/components/header.tsx",
|
||||
);
|
||||
|
||||
if (await fs.pathExists(headerPath)) {
|
||||
@@ -125,7 +125,7 @@ async function updateRouterIndex(projectDir: string): Promise<void> {
|
||||
}
|
||||
|
||||
async function addTodoButtonToHomepage(projectDir: string): Promise<void> {
|
||||
const indexPath = path.join(projectDir, "apps/client/src/routes/index.tsx");
|
||||
const indexPath = path.join(projectDir, "apps/web/src/routes/index.tsx");
|
||||
|
||||
if (await fs.pathExists(indexPath)) {
|
||||
let indexContent = await fs.readFile(indexPath, "utf8");
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
PackageManager,
|
||||
ProjectAddons,
|
||||
ProjectDatabase,
|
||||
ProjectFrontend,
|
||||
ProjectOrm,
|
||||
Runtime,
|
||||
} from "../types";
|
||||
@@ -13,9 +14,10 @@ export function displayPostInstallInstructions(
|
||||
projectName: string,
|
||||
packageManager: PackageManager,
|
||||
depsInstalled: boolean,
|
||||
orm?: ProjectOrm,
|
||||
addons?: ProjectAddons[],
|
||||
runtime?: Runtime,
|
||||
orm: ProjectOrm,
|
||||
addons: ProjectAddons[],
|
||||
runtime: Runtime,
|
||||
frontends: ProjectFrontend[],
|
||||
) {
|
||||
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
||||
const cdCmd = `cd ${projectName}`;
|
||||
@@ -32,15 +34,29 @@ export function displayPostInstallInstructions(
|
||||
const lintingInstructions = hasHuskyOrBiome
|
||||
? getLintingInstructions(runCmd)
|
||||
: "";
|
||||
const nativeInstructions = frontends?.includes("native")
|
||||
? getNativeInstructions()
|
||||
: "";
|
||||
|
||||
const hasWebFrontend = frontends?.includes("web");
|
||||
const hasNativeFrontend = frontends?.includes("native");
|
||||
const hasFrontend = hasWebFrontend || hasNativeFrontend;
|
||||
|
||||
log.info(`${pc.bold("Next steps:")}
|
||||
${pc.cyan("1.")} ${cdCmd}
|
||||
${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev
|
||||
|
||||
${pc.bold("Your project will be available at:")}
|
||||
${pc.cyan("•")} Frontend: http://localhost:3001
|
||||
${pc.cyan("•")} API: http://localhost:3000
|
||||
${databaseInstructions ? `\n${databaseInstructions.trim()}` : ""}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${lintingInstructions ? `\n${lintingInstructions.trim()}` : ""}`);
|
||||
${
|
||||
hasFrontend
|
||||
? `${hasWebFrontend ? `${pc.cyan("•")} Frontend: http://localhost:3001\n` : ""}`
|
||||
: `${pc.yellow("NOTE:")} You are creating a backend-only app (no frontend selected)\n`
|
||||
}${pc.cyan("•")} API: http://localhost:3000
|
||||
${nativeInstructions ? `\n${nativeInstructions.trim()}` : ""}${databaseInstructions ? `\n${databaseInstructions.trim()}` : ""}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${lintingInstructions ? `\n${lintingInstructions.trim()}` : ""}`);
|
||||
}
|
||||
|
||||
function getNativeInstructions(): string {
|
||||
return `${pc.yellow("NOTE:")} If the Expo app cannot connect to the server, update the EXPO_PUBLIC_SERVER_URL in apps/native/.env to use your local IP address instead of localhost:\n${pc.dim("EXPO_PUBLIC_SERVER_URL=http://192.168.0.103:3000")}\n`;
|
||||
}
|
||||
|
||||
function getLintingInstructions(runCmd?: string): string {
|
||||
@@ -95,5 +111,5 @@ function getDatabaseInstructions(
|
||||
}
|
||||
|
||||
function getTauriInstructions(runCmd?: string): string {
|
||||
return `${pc.bold("Desktop app with Tauri:")}\n${pc.cyan("•")} Start desktop app: ${pc.dim(`cd apps/client && ${runCmd} desktop:dev`)}\n${pc.cyan("•")} Build desktop app: ${pc.dim(`cd apps/client && ${runCmd} desktop:build`)}\n${pc.yellow("NOTE:")} Tauri requires Rust and platform-specific dependencies. See: ${pc.dim("https://v2.tauri.app/start/prerequisites/")}\n\n`;
|
||||
return `${pc.bold("Desktop app with Tauri:")}\n${pc.cyan("•")} Start desktop app: ${pc.dim(`cd apps/web && ${runCmd} desktop:dev`)}\n${pc.cyan("•")} Build desktop app: ${pc.dim(`cd apps/web && ${runCmd} desktop:build`)}\n${pc.yellow("NOTE:")} Tauri requires Rust and platform-specific dependencies. See: ${pc.dim("https://v2.tauri.app/start/prerequisites/")}\n\n`;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function setupTauri(
|
||||
packageManager: PackageManager,
|
||||
): Promise<void> {
|
||||
const s = spinner();
|
||||
const clientPackageDir = path.join(projectDir, "apps/client");
|
||||
const clientPackageDir = path.join(projectDir, "apps/web");
|
||||
|
||||
try {
|
||||
s.start("Setting up Tauri desktop app support...");
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { PKG_ROOT } from "../constants";
|
||||
import type { BackendFramework, ProjectDatabase, ProjectOrm } from "../types";
|
||||
import type {
|
||||
BackendFramework,
|
||||
ProjectDatabase,
|
||||
ProjectFrontend,
|
||||
ProjectOrm,
|
||||
} from "../types";
|
||||
|
||||
export async function copyBaseTemplate(projectDir: string): Promise<void> {
|
||||
const templateDir = path.join(PKG_ROOT, "template/base");
|
||||
@@ -11,6 +16,30 @@ export async function copyBaseTemplate(projectDir: string): Promise<void> {
|
||||
await fs.copy(templateDir, projectDir);
|
||||
}
|
||||
|
||||
export async function setupFrontendTemplates(
|
||||
projectDir: string,
|
||||
frontends: ProjectFrontend[],
|
||||
): Promise<void> {
|
||||
if (!frontends.includes("web")) {
|
||||
const webDir = path.join(projectDir, "apps/web");
|
||||
if (await fs.pathExists(webDir)) {
|
||||
await fs.remove(webDir);
|
||||
}
|
||||
}
|
||||
|
||||
if (!frontends.includes("native")) {
|
||||
const nativeDir = path.join(projectDir, "apps/native");
|
||||
if (await fs.pathExists(nativeDir)) {
|
||||
await fs.remove(nativeDir);
|
||||
}
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
path.join(projectDir, ".npmrc"),
|
||||
"node-linker=hoisted\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupBackendFramework(
|
||||
projectDir: string,
|
||||
framework: BackendFramework,
|
||||
@@ -67,8 +96,8 @@ export async function setupAuthTemplate(
|
||||
|
||||
const authTemplateDir = path.join(PKG_ROOT, "template/with-auth");
|
||||
if (await fs.pathExists(authTemplateDir)) {
|
||||
const clientAuthDir = path.join(authTemplateDir, "apps/client");
|
||||
const projectClientDir = path.join(projectDir, "apps/client");
|
||||
const clientAuthDir = path.join(authTemplateDir, "apps/web");
|
||||
const projectClientDir = path.join(projectDir, "apps/web");
|
||||
await fs.copy(clientAuthDir, projectClientDir, { overwrite: true });
|
||||
|
||||
const serverAuthDir = path.join(authTemplateDir, "apps/server/src");
|
||||
@@ -118,7 +147,8 @@ export async function setupAuthTemplate(
|
||||
export async function fixGitignoreFiles(projectDir: string): Promise<void> {
|
||||
const gitignorePaths = [
|
||||
path.join(projectDir, "_gitignore"),
|
||||
path.join(projectDir, "apps/client/_gitignore"),
|
||||
path.join(projectDir, "apps/web/_gitignore"),
|
||||
path.join(projectDir, "apps/native/_gitignore"),
|
||||
path.join(projectDir, "apps/server/_gitignore"),
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user