Added support for building mobile applications with Expo

This commit is contained in:
Aman Varshney
2025-03-29 12:31:51 +05:30
parent 228f24d6db
commit 1c66d64be5
90 changed files with 981 additions and 204 deletions

View File

@@ -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"],

View File

@@ -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({

View File

@@ -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;

View File

@@ -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`;

View File

@@ -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());
}
}

View File

@@ -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");

View File

@@ -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`;
}

View File

@@ -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...");

View File

@@ -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"),
];