mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add nuxt and expo with orpc
This commit is contained in:
5
.changeset/long-schools-trade.md
Normal file
5
.changeset/long-schools-trade.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add nuxt and expo with orpc
|
||||||
@@ -25,8 +25,8 @@ export const DEFAULT_CONFIG: ProjectConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const dependencyVersionMap = {
|
export const dependencyVersionMap = {
|
||||||
"better-auth": "^1.2.6",
|
"better-auth": "^1.2.7",
|
||||||
"@better-auth/expo": "^1.2.6",
|
"@better-auth/expo": "^1.2.7",
|
||||||
|
|
||||||
"drizzle-orm": "^0.38.4",
|
"drizzle-orm": "^0.38.4",
|
||||||
"drizzle-kit": "^0.30.5",
|
"drizzle-kit": "^0.30.5",
|
||||||
@@ -74,12 +74,14 @@ export const dependencyVersionMap = {
|
|||||||
|
|
||||||
ai: "^4.2.8",
|
ai: "^4.2.8",
|
||||||
"@ai-sdk/google": "^1.2.3",
|
"@ai-sdk/google": "^1.2.3",
|
||||||
|
"@ai-sdk/vue": "^1.2.8",
|
||||||
|
|
||||||
"@prisma/extension-accelerate": "^1.3.0",
|
"@prisma/extension-accelerate": "^1.3.0",
|
||||||
|
|
||||||
"@orpc/server": "^1.0.3",
|
"@orpc/server": "^1.1.0",
|
||||||
"@orpc/react-query": "^1.0.3",
|
"@orpc/client": "^1.1.0",
|
||||||
"@orpc/client": "^1.0.3",
|
"@orpc/react-query": "^1.1.0",
|
||||||
|
"@orpc/vue-query": "^1.1.0",
|
||||||
|
|
||||||
"@trpc/tanstack-react-query": "^11.0.0",
|
"@trpc/tanstack-react-query": "^11.0.0",
|
||||||
"@trpc/server": "^11.0.0",
|
"@trpc/server": "^11.0.0",
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import type { ProjectConfig } from "../types";
|
|||||||
export async function setupAddons(config: ProjectConfig) {
|
export async function setupAddons(config: ProjectConfig) {
|
||||||
const { projectName, addons, frontend } = config;
|
const { projectName, addons, frontend } = config;
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
const hasWebFrontend =
|
const hasReactWebFrontend =
|
||||||
frontend.includes("react-router") || frontend.includes("tanstack-router");
|
frontend.includes("react-router") || frontend.includes("tanstack-router");
|
||||||
|
const hasNuxtFrontend = frontend.includes("nuxt");
|
||||||
|
|
||||||
if (addons.includes("turborepo")) {
|
if (addons.includes("turborepo")) {
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
@@ -20,10 +21,10 @@ export async function setupAddons(config: ProjectConfig) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addons.includes("pwa") && hasWebFrontend) {
|
if (addons.includes("pwa") && hasReactWebFrontend) {
|
||||||
await setupPwa(projectDir, frontend);
|
await setupPwa(projectDir, frontend);
|
||||||
}
|
}
|
||||||
if (addons.includes("tauri") && hasWebFrontend) {
|
if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend)) {
|
||||||
await setupTauri(config);
|
await setupTauri(config);
|
||||||
}
|
}
|
||||||
if (addons.includes("biome")) {
|
if (addons.includes("biome")) {
|
||||||
@@ -89,6 +90,11 @@ async function setupHusky(projectDir: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupPwa(projectDir: string, frontends: ProjectFrontend[]) {
|
async function setupPwa(projectDir: string, frontends: ProjectFrontend[]) {
|
||||||
|
const isCompatibleFrontend = frontends.some((f) =>
|
||||||
|
["react-router", "tanstack-router"].includes(f),
|
||||||
|
);
|
||||||
|
if (!isCompatibleFrontend) return;
|
||||||
|
|
||||||
const clientPackageDir = getWebAppDir(projectDir, frontends);
|
const clientPackageDir = getWebAppDir(projectDir, frontends);
|
||||||
|
|
||||||
if (!(await fs.pathExists(clientPackageDir))) {
|
if (!(await fs.pathExists(clientPackageDir))) {
|
||||||
|
|||||||
@@ -4,39 +4,84 @@ import type { ProjectConfig } from "../types";
|
|||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupApi(config: ProjectConfig): Promise<void> {
|
export async function setupApi(config: ProjectConfig): Promise<void> {
|
||||||
const { api, projectName } = config;
|
const { api, projectName, frontend } = config;
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
const webDir = path.join(projectDir, "apps/web");
|
const webDir = path.join(projectDir, "apps/web");
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
const webDirExists = await fs.pathExists(webDir);
|
const webDirExists = await fs.pathExists(webDir);
|
||||||
|
const hasReactWeb = frontend.some((f) =>
|
||||||
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
||||||
|
);
|
||||||
|
const hasNuxtWeb = frontend.includes("nuxt");
|
||||||
|
|
||||||
if (api === "orpc") {
|
if (api === "orpc") {
|
||||||
if (webDirExists) {
|
|
||||||
await addPackageDependency({
|
|
||||||
dependencies: ["@orpc/react-query", "@orpc/server", "@orpc/client"],
|
|
||||||
projectDir: webDir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["@orpc/server", "@orpc/client"],
|
dependencies: ["@orpc/server", "@orpc/client"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
}
|
} else if (api === "trpc") {
|
||||||
|
|
||||||
if (api === "trpc") {
|
|
||||||
if (webDirExists) {
|
|
||||||
await addPackageDependency({
|
|
||||||
dependencies: [
|
|
||||||
"@trpc/tanstack-react-query",
|
|
||||||
"@trpc/server",
|
|
||||||
"@trpc/client",
|
|
||||||
],
|
|
||||||
projectDir: webDir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["@trpc/server", "@trpc/client"],
|
dependencies: ["@trpc/server", "@trpc/client"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
|
if (config.backend === "hono") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@hono/trpc-server"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
} else if (config.backend === "elysia") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@elysiajs/trpc"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webDirExists) {
|
||||||
|
if (hasReactWeb) {
|
||||||
|
if (api === "orpc") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@orpc/react-query", "@orpc/client", "@orpc/server"],
|
||||||
|
projectDir: webDir,
|
||||||
|
});
|
||||||
|
} else if (api === "trpc") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: [
|
||||||
|
"@trpc/tanstack-react-query",
|
||||||
|
"@trpc/client",
|
||||||
|
"@trpc/server",
|
||||||
|
],
|
||||||
|
projectDir: webDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (hasNuxtWeb) {
|
||||||
|
if (api === "orpc") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@orpc/vue-query", "@orpc/client", "@orpc/server"],
|
||||||
|
projectDir: webDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frontend.includes("native")) {
|
||||||
|
const nativeDir = path.join(projectDir, "apps/native");
|
||||||
|
if (await fs.pathExists(nativeDir)) {
|
||||||
|
if (api === "trpc") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: [
|
||||||
|
"@trpc/tanstack-react-query",
|
||||||
|
"@trpc/client",
|
||||||
|
"@trpc/server",
|
||||||
|
],
|
||||||
|
projectDir: nativeDir,
|
||||||
|
});
|
||||||
|
} else if (api === "orpc") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@orpc/react-query", "@orpc/client", "@orpc/server"],
|
||||||
|
projectDir: nativeDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,15 @@ export async function setupAuth(config: ProjectConfig): Promise<void> {
|
|||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasWebFrontend =
|
const hasWebFrontend = frontend.some((f) =>
|
||||||
frontend.includes("react-router") ||
|
[
|
||||||
frontend.includes("tanstack-router") ||
|
"react-router",
|
||||||
frontend.includes("tanstack-start") ||
|
"tanstack-router",
|
||||||
frontend.includes("next");
|
"tanstack-start",
|
||||||
|
"next",
|
||||||
|
"nuxt",
|
||||||
|
].includes(f),
|
||||||
|
);
|
||||||
|
|
||||||
if (hasWebFrontend && clientDirExists) {
|
if (hasWebFrontend && clientDirExists) {
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { initializeGit, updatePackageConfigurations } from "./project-config";
|
|||||||
import { setupRuntime } from "./runtime-setup";
|
import { setupRuntime } from "./runtime-setup";
|
||||||
import {
|
import {
|
||||||
copyBaseTemplate,
|
copyBaseTemplate,
|
||||||
fixGitignoreFiles,
|
|
||||||
handleExtras,
|
handleExtras,
|
||||||
setupAddonsTemplate,
|
setupAddonsTemplate,
|
||||||
setupAuthTemplate,
|
setupAuthTemplate,
|
||||||
@@ -70,8 +69,6 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
|||||||
|
|
||||||
await initializeGit(projectDir, options.git);
|
await initializeGit(projectDir, options.git);
|
||||||
|
|
||||||
await fixGitignoreFiles(projectDir, options);
|
|
||||||
|
|
||||||
log.success("Project template successfully scaffolded!");
|
log.success("Project template successfully scaffolded!");
|
||||||
|
|
||||||
if (options.install) {
|
if (options.install) {
|
||||||
|
|||||||
@@ -54,13 +54,18 @@ export async function setupEnvironmentVariables(
|
|||||||
const hasTanStackRouter = options.frontend.includes("tanstack-router");
|
const hasTanStackRouter = options.frontend.includes("tanstack-router");
|
||||||
const hasTanStackStart = options.frontend.includes("tanstack-start");
|
const hasTanStackStart = options.frontend.includes("tanstack-start");
|
||||||
const hasNextJs = options.frontend.includes("next");
|
const hasNextJs = options.frontend.includes("next");
|
||||||
|
const hasNuxt = options.frontend.includes("nuxt");
|
||||||
const hasWebFrontend =
|
const hasWebFrontend =
|
||||||
hasReactRouter || hasTanStackRouter || hasTanStackStart || hasNextJs;
|
hasReactRouter ||
|
||||||
|
hasTanStackRouter ||
|
||||||
|
hasTanStackStart ||
|
||||||
|
hasNextJs ||
|
||||||
|
hasNuxt;
|
||||||
|
|
||||||
let corsOrigin = "http://localhost:3000";
|
let corsOrigin = "http://localhost:3001";
|
||||||
if (hasReactRouter) {
|
if (hasReactRouter) {
|
||||||
corsOrigin = "http://localhost:5173";
|
corsOrigin = "http://localhost:5173";
|
||||||
} else if (hasTanStackRouter || hasTanStackStart || hasNextJs) {
|
} else if (hasTanStackRouter || hasTanStackStart || hasNextJs || hasNuxt) {
|
||||||
corsOrigin = "http://localhost:3001";
|
corsOrigin = "http://localhost:3001";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +126,8 @@ export async function setupEnvironmentVariables(
|
|||||||
|
|
||||||
if (hasNextJs) {
|
if (hasNextJs) {
|
||||||
envVarName = "NEXT_PUBLIC_SERVER_URL";
|
envVarName = "NEXT_PUBLIC_SERVER_URL";
|
||||||
|
} else if (hasNuxt) {
|
||||||
|
envVarName = "NUXT_PUBLIC_SERVER_URL";
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientVars: EnvVariable[] = [
|
const clientVars: EnvVariable[] = [
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
|
import type { AvailableDependencies } from "../constants";
|
||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupExamples(config: ProjectConfig): Promise<void> {
|
export async function setupExamples(config: ProjectConfig): Promise<void> {
|
||||||
const { projectName, examples } = config;
|
const { projectName, examples, frontend } = config;
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
|
|
||||||
if (examples.includes("ai")) {
|
if (examples.includes("ai")) {
|
||||||
@@ -12,9 +13,15 @@ export async function setupExamples(config: ProjectConfig): Promise<void> {
|
|||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
const clientDirExists = await fs.pathExists(clientDir);
|
const clientDirExists = await fs.pathExists(clientDir);
|
||||||
|
|
||||||
|
const hasNuxt = frontend.includes("nuxt");
|
||||||
|
|
||||||
if (clientDirExists) {
|
if (clientDirExists) {
|
||||||
|
const dependencies: AvailableDependencies[] = ["ai"];
|
||||||
|
if (hasNuxt) {
|
||||||
|
dependencies.push("@ai-sdk/vue");
|
||||||
|
}
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["ai"],
|
dependencies,
|
||||||
projectDir: clientDir,
|
projectDir: clientDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,14 +37,22 @@ export function displayPostInstallInstructions(
|
|||||||
? getNativeInstructions()
|
? getNativeInstructions()
|
||||||
: "";
|
: "";
|
||||||
const pwaInstructions =
|
const pwaInstructions =
|
||||||
addons?.includes("pwa") && frontend?.includes("react-router")
|
addons?.includes("pwa") &&
|
||||||
|
(frontend?.includes("react-router") ||
|
||||||
|
frontend?.includes("tanstack-router")) // Exclude Nuxt from PWA instructions
|
||||||
? getPwaInstructions()
|
? getPwaInstructions()
|
||||||
: "";
|
: "";
|
||||||
const starlightInstructions = addons?.includes("starlight")
|
const starlightInstructions = addons?.includes("starlight")
|
||||||
? getStarlightInstructions(runCmd)
|
? getStarlightInstructions(runCmd)
|
||||||
: "";
|
: "";
|
||||||
const hasWeb = frontend?.some((f) =>
|
const hasWeb = frontend?.some((f) =>
|
||||||
["tanstack-router", "react-router", "next", "tanstack-start"].includes(f),
|
[
|
||||||
|
"tanstack-router",
|
||||||
|
"react-router",
|
||||||
|
"next",
|
||||||
|
"tanstack-start",
|
||||||
|
"nuxt", // Include Nuxt here
|
||||||
|
].includes(f),
|
||||||
);
|
);
|
||||||
const hasNative = frontend?.includes("native");
|
const hasNative = frontend?.includes("native");
|
||||||
const bunWebNativeWarning =
|
const bunWebNativeWarning =
|
||||||
@@ -57,12 +65,13 @@ export function displayPostInstallInstructions(
|
|||||||
const hasTanstackRouter = frontend?.includes("tanstack-router");
|
const hasTanstackRouter = frontend?.includes("tanstack-router");
|
||||||
const hasTanstackStart = frontend?.includes("tanstack-start");
|
const hasTanstackStart = frontend?.includes("tanstack-start");
|
||||||
const hasReactRouter = frontend?.includes("react-router");
|
const hasReactRouter = frontend?.includes("react-router");
|
||||||
|
const hasNuxt = frontend?.includes("nuxt"); // Add Nuxt check
|
||||||
const hasWebFrontend =
|
const hasWebFrontend =
|
||||||
hasTanstackRouter || hasReactRouter || hasTanstackStart;
|
hasTanstackRouter || hasReactRouter || hasTanstackStart || hasNuxt; // Include Nuxt
|
||||||
const hasNativeFrontend = frontend?.includes("native");
|
const hasNativeFrontend = frontend?.includes("native");
|
||||||
const hasFrontend = hasWebFrontend || hasNativeFrontend;
|
const hasFrontend = hasWebFrontend || hasNativeFrontend;
|
||||||
|
|
||||||
const webPort = hasReactRouter ? "5173" : "3001";
|
const webPort = hasReactRouter ? "5173" : "3001"; // Nuxt uses 3001, same as others
|
||||||
const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r");
|
const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r");
|
||||||
|
|
||||||
consola.box(
|
consola.box(
|
||||||
@@ -93,7 +102,9 @@ ${
|
|||||||
lintingInstructions ? `\n${lintingInstructions.trim()}` : ""
|
lintingInstructions ? `\n${lintingInstructions.trim()}` : ""
|
||||||
}${pwaInstructions ? `\n${pwaInstructions.trim()}` : ""}${
|
}${pwaInstructions ? `\n${pwaInstructions.trim()}` : ""}${
|
||||||
starlightInstructions ? `\n${starlightInstructions.trim()}` : ""
|
starlightInstructions ? `\n${starlightInstructions.trim()}` : ""
|
||||||
}${noOrmWarning ? `\n${noOrmWarning.trim()}` : ""}${bunWebNativeWarning ? `\n${bunWebNativeWarning.trim()}` : ""}
|
}${noOrmWarning ? `\n${noOrmWarning.trim()}` : ""}${
|
||||||
|
bunWebNativeWarning ? `\n${bunWebNativeWarning.trim()}` : ""
|
||||||
|
}
|
||||||
|
|
||||||
${pc.bold("Update all dependencies:\n")}${pc.cyan(tazeCommand)}
|
${pc.bold("Update all dependencies:\n")}${pc.cyan(tazeCommand)}
|
||||||
|
|
||||||
@@ -177,9 +188,13 @@ function getStarlightInstructions(runCmd?: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNoOrmWarning(): string {
|
function getNoOrmWarning(): string {
|
||||||
return `\n${pc.yellow("WARNING:")} Database selected without an ORM. Features requiring database access (e.g., examples, auth) need manual setup.\n`;
|
return `\n${pc.yellow(
|
||||||
|
"WARNING:",
|
||||||
|
)} Database selected without an ORM. Features requiring database access (e.g., examples, auth) need manual setup.\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBunWebNativeWarning(): string {
|
function getBunWebNativeWarning(): string {
|
||||||
return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo. Use 'pnpm' if problems arise.\n`;
|
return `\n${pc.yellow(
|
||||||
|
"WARNING:",
|
||||||
|
)} 'bun' might cause issues with web + native apps in a monorepo. Use 'pnpm' if problems arise.\n`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { log } from "@clack/prompts";
|
|||||||
import { $, execa } from "execa";
|
import { $, execa } from "execa";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
|
import { dependencyVersionMap } from "../constants";
|
||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export async function updatePackageConfigurations(
|
export async function updatePackageConfigurations(
|
||||||
|
|||||||
@@ -42,15 +42,19 @@ export async function setupTauri(config: ProjectConfig): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasReactRouter = frontend.includes("react-router");
|
const hasReactRouter = frontend.includes("react-router");
|
||||||
|
const hasNuxt = frontend.includes("nuxt");
|
||||||
|
|
||||||
const devUrl = hasReactRouter
|
const devUrl = hasReactRouter
|
||||||
? "http://localhost:5173"
|
? "http://localhost:5173"
|
||||||
: "http://localhost:3001";
|
: "http://localhost:3001";
|
||||||
|
|
||||||
|
const frontendDist = hasNuxt ? "../.output/public" : "../dist";
|
||||||
|
|
||||||
const tauriArgs = [
|
const tauriArgs = [
|
||||||
"init",
|
"init",
|
||||||
`--app-name=${path.basename(projectDir)}`,
|
`--app-name=${path.basename(projectDir)}`,
|
||||||
`--window-title=${path.basename(projectDir)}`,
|
`--window-title=${path.basename(projectDir)}`,
|
||||||
"--frontend-dist=../dist",
|
`--frontend-dist=${frontendDist}`,
|
||||||
`--dev-url=${devUrl}`,
|
`--dev-url=${devUrl}`,
|
||||||
`--before-dev-command=\"${packageManager} run dev\"`,
|
`--before-dev-command=\"${packageManager} run dev\"`,
|
||||||
`--before-build-command=\"${packageManager} run build\"`,
|
`--before-build-command=\"${packageManager} run build\"`,
|
||||||
|
|||||||
@@ -28,17 +28,21 @@ async function processAndCopyFiles(
|
|||||||
if (relativeSrcPath.endsWith(".hbs")) {
|
if (relativeSrcPath.endsWith(".hbs")) {
|
||||||
relativeDestPath = relativeSrcPath.slice(0, -4);
|
relativeDestPath = relativeSrcPath.slice(0, -4);
|
||||||
}
|
}
|
||||||
|
if (path.basename(relativeSrcPath) === "_gitignore") {
|
||||||
|
relativeDestPath = path.join(path.dirname(relativeSrcPath), ".gitignore");
|
||||||
|
}
|
||||||
|
|
||||||
const destPath = path.join(destDir, relativeDestPath);
|
const destPath = path.join(destDir, relativeDestPath);
|
||||||
|
|
||||||
await fs.ensureDir(path.dirname(destPath));
|
await fs.ensureDir(path.dirname(destPath));
|
||||||
|
|
||||||
|
if (!overwrite && (await fs.pathExists(destPath))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (srcPath.endsWith(".hbs")) {
|
if (srcPath.endsWith(".hbs")) {
|
||||||
await processTemplate(srcPath, destPath, context);
|
await processTemplate(srcPath, destPath, context);
|
||||||
} else {
|
} else {
|
||||||
if (!overwrite && (await fs.pathExists(destPath))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await fs.copy(srcPath, destPath, { overwrite: true });
|
await fs.copy(srcPath, destPath, { overwrite: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,62 +53,69 @@ export async function copyBaseTemplate(
|
|||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const templateDir = path.join(PKG_ROOT, "templates/base");
|
const templateDir = path.join(PKG_ROOT, "templates/base");
|
||||||
await processAndCopyFiles(
|
await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
|
||||||
["package.json", "_gitignore"],
|
|
||||||
templateDir,
|
|
||||||
projectDir,
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupFrontendTemplates(
|
export async function setupFrontendTemplates(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const webFrontends = context.frontend.filter(
|
const hasReactWeb = context.frontend.some((f) =>
|
||||||
(f) =>
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
||||||
f === "tanstack-router" ||
|
|
||||||
f === "react-router" ||
|
|
||||||
f === "tanstack-start" ||
|
|
||||||
f === "next",
|
|
||||||
);
|
);
|
||||||
|
const hasNuxtWeb = context.frontend.includes("nuxt");
|
||||||
const hasNative = context.frontend.includes("native");
|
const hasNative = context.frontend.includes("native");
|
||||||
|
|
||||||
if (webFrontends.length > 0) {
|
if (hasReactWeb || hasNuxtWeb) {
|
||||||
const webAppDir = path.join(projectDir, "apps/web");
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
await fs.ensureDir(webAppDir);
|
await fs.ensureDir(webAppDir);
|
||||||
|
|
||||||
const webBaseDir = path.join(PKG_ROOT, "templates/frontend/web-base");
|
if (hasReactWeb) {
|
||||||
if (await fs.pathExists(webBaseDir)) {
|
const webBaseDir = path.join(
|
||||||
await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const framework of webFrontends) {
|
|
||||||
const frameworkSrcDir = path.join(
|
|
||||||
PKG_ROOT,
|
PKG_ROOT,
|
||||||
`templates/frontend/${framework}`,
|
"templates/frontend/react/web-base",
|
||||||
);
|
);
|
||||||
if (await fs.pathExists(frameworkSrcDir)) {
|
if (await fs.pathExists(webBaseDir)) {
|
||||||
await processAndCopyFiles("**/*", frameworkSrcDir, webAppDir, context);
|
await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
|
||||||
|
}
|
||||||
|
const reactFramework = context.frontend.find((f) =>
|
||||||
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
|
||||||
|
f,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (reactFramework) {
|
||||||
|
const frameworkSrcDir = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/frontend/react/${reactFramework}`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(frameworkSrcDir)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
frameworkSrcDir,
|
||||||
|
webAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const apiWebBaseDir = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/api/${context.api}/web/react/base`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(apiWebBaseDir)) {
|
||||||
|
await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (hasNuxtWeb) {
|
||||||
|
const nuxtBaseDir = path.join(PKG_ROOT, "templates/frontend/nuxt");
|
||||||
|
if (await fs.pathExists(nuxtBaseDir)) {
|
||||||
|
await processAndCopyFiles("**/*", nuxtBaseDir, webAppDir, context);
|
||||||
|
}
|
||||||
|
const apiWebNuxtDir = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/api/${context.api}/web/nuxt`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(apiWebNuxtDir)) {
|
||||||
|
await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const webFramework = webFrontends[0];
|
|
||||||
|
|
||||||
const apiWebBaseDir = path.join(
|
|
||||||
PKG_ROOT,
|
|
||||||
`templates/api/${context.api}/web/base`,
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(apiWebBaseDir)) {
|
|
||||||
await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiWebFrameworkDir = path.join(
|
|
||||||
PKG_ROOT,
|
|
||||||
`templates/api/${context.api}/web/${webFramework}`,
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(apiWebFrameworkDir)) {
|
|
||||||
await processAndCopyFiles("**/*", apiWebFrameworkDir, webAppDir, context);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,14 +128,32 @@ export async function setupFrontendTemplates(
|
|||||||
await processAndCopyFiles("**/*", nativeBaseDir, nativeAppDir, context);
|
await processAndCopyFiles("**/*", nativeBaseDir, nativeAppDir, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiNativeSrcDir = path.join(
|
if (context.api === "trpc") {
|
||||||
PKG_ROOT,
|
const apiNativeSrcDir = path.join(
|
||||||
`templates/api/${context.api}/native`,
|
PKG_ROOT,
|
||||||
);
|
`templates/api/${context.api}/native`,
|
||||||
|
);
|
||||||
if (await fs.pathExists(apiNativeSrcDir)) {
|
if (await fs.pathExists(apiNativeSrcDir)) {
|
||||||
await processAndCopyFiles("**/*", apiNativeSrcDir, nativeAppDir, context);
|
await processAndCopyFiles(
|
||||||
} else {
|
"**/*",
|
||||||
|
apiNativeSrcDir,
|
||||||
|
nativeAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (context.api === "orpc") {
|
||||||
|
const apiNativeSrcDir = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/api/${context.api}/native`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(apiNativeSrcDir)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
apiNativeSrcDir,
|
||||||
|
nativeAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,13 +251,10 @@ export async function setupAuthTemplate(
|
|||||||
const webAppDirExists = await fs.pathExists(webAppDir);
|
const webAppDirExists = await fs.pathExists(webAppDir);
|
||||||
const nativeAppDirExists = await fs.pathExists(nativeAppDir);
|
const nativeAppDirExists = await fs.pathExists(nativeAppDir);
|
||||||
|
|
||||||
const webFrontends = context.frontend.filter(
|
const hasReactWeb = context.frontend.some((f) =>
|
||||||
(f) =>
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
||||||
f === "tanstack-router" ||
|
|
||||||
f === "react-router" ||
|
|
||||||
f === "tanstack-start" ||
|
|
||||||
f === "next",
|
|
||||||
);
|
);
|
||||||
|
const hasNuxtWeb = context.frontend.includes("nuxt");
|
||||||
const hasNative = context.frontend.includes("native");
|
const hasNative = context.frontend.includes("native");
|
||||||
|
|
||||||
if (serverAppDirExists) {
|
if (serverAppDirExists) {
|
||||||
@@ -240,12 +266,6 @@ export async function setupAuthTemplate(
|
|||||||
serverAppDir,
|
serverAppDir,
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
consola.warn(
|
|
||||||
pc.yellow(
|
|
||||||
`Warning: Base auth server template not found at ${authServerBaseSrc}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.backend === "next") {
|
if (context.backend === "next") {
|
||||||
@@ -260,12 +280,6 @@ export async function setupAuthTemplate(
|
|||||||
serverAppDir,
|
serverAppDir,
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
consola.warn(
|
|
||||||
pc.yellow(
|
|
||||||
`Warning: Next auth server template not found at ${authServerNextSrc}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,44 +308,40 @@ export async function setupAuthTemplate(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
consola.warn(
|
|
||||||
pc.yellow(
|
|
||||||
"Warning: apps/server directory does not exist, skipping server-side auth template setup.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webFrontends.length > 0 && webAppDirExists) {
|
if ((hasReactWeb || hasNuxtWeb) && webAppDirExists) {
|
||||||
const authWebBaseSrc = path.join(PKG_ROOT, "templates/auth/web/base");
|
if (hasReactWeb) {
|
||||||
if (await fs.pathExists(authWebBaseSrc)) {
|
const authWebBaseSrc = path.join(
|
||||||
await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
|
PKG_ROOT,
|
||||||
} else {
|
"templates/auth/web/react/base",
|
||||||
consola.warn(
|
);
|
||||||
pc.yellow(
|
if (await fs.pathExists(authWebBaseSrc)) {
|
||||||
`Warning: Base auth web template not found at ${authWebBaseSrc}`,
|
await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
|
||||||
|
}
|
||||||
|
const reactFramework = context.frontend.find((f) =>
|
||||||
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
|
||||||
|
f,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
if (reactFramework) {
|
||||||
|
const authWebFrameworkSrc = path.join(
|
||||||
for (const framework of webFrontends) {
|
PKG_ROOT,
|
||||||
const authWebFrameworkSrc = path.join(
|
`templates/auth/web/react/${reactFramework}`,
|
||||||
PKG_ROOT,
|
|
||||||
`templates/auth/web/${framework}`,
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(authWebFrameworkSrc)) {
|
|
||||||
await processAndCopyFiles(
|
|
||||||
"**/*",
|
|
||||||
authWebFrameworkSrc,
|
|
||||||
webAppDir,
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
consola.warn(
|
|
||||||
pc.yellow(
|
|
||||||
`Warning: Auth web template for ${framework} not found at ${authWebFrameworkSrc}`,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
if (await fs.pathExists(authWebFrameworkSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
authWebFrameworkSrc,
|
||||||
|
webAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (hasNuxtWeb) {
|
||||||
|
const authWebNuxtSrc = path.join(PKG_ROOT, "templates/auth/web/nuxt");
|
||||||
|
if (await fs.pathExists(authWebNuxtSrc)) {
|
||||||
|
await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,50 +364,25 @@ export async function setupAddonsTemplate(
|
|||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (context.addons.includes("turborepo")) {
|
if (!context.addons || context.addons.length === 0) return;
|
||||||
const turboSrcDir = path.join(PKG_ROOT, "templates/addons/turborepo");
|
|
||||||
if (await fs.pathExists(turboSrcDir)) {
|
|
||||||
await processAndCopyFiles("**/*", turboSrcDir, projectDir, context);
|
|
||||||
} else {
|
|
||||||
consola.warn(pc.yellow("Warning: Turborepo addon template not found."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.addons.includes("husky")) {
|
for (const addon of context.addons) {
|
||||||
const huskySrcDir = path.join(PKG_ROOT, "templates/addons/husky");
|
if (addon === "none") continue;
|
||||||
if (await fs.pathExists(huskySrcDir)) {
|
|
||||||
await processAndCopyFiles("**/*", huskySrcDir, projectDir, context);
|
|
||||||
} else {
|
|
||||||
consola.warn(pc.yellow("Warning: Husky addon template not found."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.addons.includes("biome")) {
|
let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
|
||||||
const biomeSrcDir = path.join(PKG_ROOT, "templates/addons/biome");
|
let addonDestDir = projectDir;
|
||||||
if (await fs.pathExists(biomeSrcDir)) {
|
|
||||||
await processAndCopyFiles("**/*", biomeSrcDir, projectDir, context);
|
|
||||||
} else {
|
|
||||||
consola.warn(pc.yellow("Warning: Biome addon template not found."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.addons.includes("pwa")) {
|
if (addon === "pwa") {
|
||||||
const pwaSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web");
|
addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web");
|
||||||
const webAppDir = path.join(projectDir, "apps/web");
|
addonDestDir = path.join(projectDir, "apps/web");
|
||||||
const webAppDirExists = await fs.pathExists(webAppDir);
|
if (!(await fs.pathExists(addonDestDir))) {
|
||||||
|
continue;
|
||||||
if (await fs.pathExists(pwaSrcDir)) {
|
|
||||||
if (webAppDirExists) {
|
|
||||||
await processAndCopyFiles("**/*", pwaSrcDir, webAppDir, context);
|
|
||||||
} else {
|
|
||||||
consola.warn(
|
|
||||||
pc.yellow(
|
|
||||||
"Warning: apps/web directory not found, cannot setup PWA addon template.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await fs.pathExists(addonSrcDir)) {
|
||||||
|
await processAndCopyFiles("**/*", addonSrcDir, addonDestDir, context);
|
||||||
} else {
|
} else {
|
||||||
consola.warn(pc.yellow("Warning: PWA addon template not found."));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,7 +399,14 @@ export async function setupExamplesTemplate(
|
|||||||
const serverAppDirExists = await fs.pathExists(serverAppDir);
|
const serverAppDirExists = await fs.pathExists(serverAppDir);
|
||||||
const webAppDirExists = await fs.pathExists(webAppDir);
|
const webAppDirExists = await fs.pathExists(webAppDir);
|
||||||
|
|
||||||
|
const hasReactWeb = context.frontend.some((f) =>
|
||||||
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
||||||
|
);
|
||||||
|
const hasNuxtWeb = context.frontend.includes("nuxt");
|
||||||
|
|
||||||
for (const example of context.examples) {
|
for (const example of context.examples) {
|
||||||
|
if (example === "none") continue;
|
||||||
|
|
||||||
const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`);
|
const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`);
|
||||||
|
|
||||||
if (serverAppDirExists) {
|
if (serverAppDirExists) {
|
||||||
@@ -456,10 +448,10 @@ export async function setupExamplesTemplate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webAppDirExists) {
|
if (hasReactWeb && webAppDirExists) {
|
||||||
const exampleWebSrc = path.join(exampleBaseDir, "web");
|
const exampleWebSrc = path.join(exampleBaseDir, "web/react");
|
||||||
if (await fs.pathExists(exampleWebSrc)) {
|
if (await fs.pathExists(exampleWebSrc)) {
|
||||||
const webFrameworks = context.frontend.filter((f) =>
|
const reactFramework = context.frontend.find((f) =>
|
||||||
[
|
[
|
||||||
"next",
|
"next",
|
||||||
"react-router",
|
"react-router",
|
||||||
@@ -467,8 +459,11 @@ export async function setupExamplesTemplate(
|
|||||||
"tanstack-start",
|
"tanstack-start",
|
||||||
].includes(f),
|
].includes(f),
|
||||||
);
|
);
|
||||||
for (const framework of webFrameworks) {
|
if (reactFramework) {
|
||||||
const exampleWebFrameworkSrc = path.join(exampleWebSrc, framework);
|
const exampleWebFrameworkSrc = path.join(
|
||||||
|
exampleWebSrc,
|
||||||
|
reactFramework,
|
||||||
|
);
|
||||||
if (await fs.pathExists(exampleWebFrameworkSrc)) {
|
if (await fs.pathExists(exampleWebFrameworkSrc)) {
|
||||||
await processAndCopyFiles(
|
await processAndCopyFiles(
|
||||||
"**/*",
|
"**/*",
|
||||||
@@ -480,36 +475,27 @@ export async function setupExamplesTemplate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (hasNuxtWeb && webAppDirExists) {
|
||||||
}
|
// Only copy Nuxt examples if the API is oRPC (as tRPC is not supported)
|
||||||
}
|
if (context.api === "orpc") {
|
||||||
|
const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt");
|
||||||
export async function fixGitignoreFiles(
|
if (await fs.pathExists(exampleWebNuxtSrc)) {
|
||||||
projectDir: string,
|
await processAndCopyFiles(
|
||||||
context: ProjectConfig,
|
"**/*",
|
||||||
): Promise<void> {
|
exampleWebNuxtSrc,
|
||||||
const gitignoreFiles = await globby(["**/.gitignore.hbs", "**/_gitignore"], {
|
webAppDir,
|
||||||
cwd: projectDir,
|
context,
|
||||||
dot: true,
|
false,
|
||||||
onlyFiles: true,
|
);
|
||||||
absolute: true,
|
} else {
|
||||||
ignore: ["**/node_modules/**", "**/.git/**"],
|
consola.info(
|
||||||
});
|
pc.gray(
|
||||||
|
`Skipping Nuxt web template for example '${example}' (template not found).`,
|
||||||
for (const currentPath of gitignoreFiles) {
|
),
|
||||||
const dir = path.dirname(currentPath);
|
);
|
||||||
const filename = path.basename(currentPath);
|
}
|
||||||
const destPath = path.join(dir, ".gitignore");
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (filename === ".gitignore.hbs") {
|
|
||||||
await processTemplate(currentPath, destPath, context);
|
|
||||||
await fs.remove(currentPath);
|
|
||||||
} else if (filename === "_gitignore") {
|
|
||||||
await fs.move(currentPath, destPath, { overwrite: true });
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
// If API is tRPC, skip Nuxt examples silently as CLI validation prevents this combo.
|
||||||
consola.error(`Error processing gitignore file ${currentPath}:`, error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -518,24 +504,24 @@ export async function handleExtras(
|
|||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const extrasDir = path.join(PKG_ROOT, "templates/extras");
|
||||||
|
|
||||||
if (context.packageManager === "pnpm") {
|
if (context.packageManager === "pnpm") {
|
||||||
const pnpmWorkspaceSrc = path.join(
|
const pnpmWorkspaceSrc = path.join(extrasDir, "pnpm-workspace.yaml");
|
||||||
PKG_ROOT,
|
|
||||||
"templates/extras/pnpm-workspace.yaml",
|
|
||||||
);
|
|
||||||
const pnpmWorkspaceDest = path.join(projectDir, "pnpm-workspace.yaml");
|
const pnpmWorkspaceDest = path.join(projectDir, "pnpm-workspace.yaml");
|
||||||
if (await fs.pathExists(pnpmWorkspaceSrc)) {
|
if (await fs.pathExists(pnpmWorkspaceSrc)) {
|
||||||
await fs.copy(pnpmWorkspaceSrc, pnpmWorkspaceDest);
|
await fs.copy(pnpmWorkspaceSrc, pnpmWorkspaceDest);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.frontend.includes("native")) {
|
if (
|
||||||
const npmrcSrc = path.join(PKG_ROOT, "templates/extras/_npmrc");
|
context.packageManager === "pnpm" &&
|
||||||
|
(context.frontend.includes("native") || context.frontend.includes("nuxt"))
|
||||||
|
) {
|
||||||
|
const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
|
||||||
const npmrcDest = path.join(projectDir, ".npmrc");
|
const npmrcDest = path.join(projectDir, ".npmrc");
|
||||||
if (await fs.pathExists(npmrcSrc)) {
|
if (await fs.pathExists(npmrcTemplateSrc)) {
|
||||||
await fs.copy(npmrcSrc, npmrcDest);
|
await processTemplate(npmrcTemplateSrc, npmrcDest, context);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ async function main() {
|
|||||||
"react-router",
|
"react-router",
|
||||||
"tanstack-start",
|
"tanstack-start",
|
||||||
"next",
|
"next",
|
||||||
|
"nuxt",
|
||||||
"native",
|
"native",
|
||||||
"none",
|
"none",
|
||||||
],
|
],
|
||||||
@@ -268,11 +269,12 @@ function processAndValidateFlags(
|
|||||||
f === "tanstack-router" ||
|
f === "tanstack-router" ||
|
||||||
f === "react-router" ||
|
f === "react-router" ||
|
||||||
f === "tanstack-start" ||
|
f === "tanstack-start" ||
|
||||||
f === "next",
|
f === "next" ||
|
||||||
|
f === "nuxt",
|
||||||
);
|
);
|
||||||
if (webFrontends.length > 1) {
|
if (webFrontends.length > 1) {
|
||||||
consola.fatal(
|
consola.fatal(
|
||||||
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next",
|
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt",
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -445,18 +447,20 @@ function processAndValidateFlags(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const includesNative = effectiveFrontend?.includes("native");
|
const includesNative = effectiveFrontend?.includes("native");
|
||||||
if (includesNative && effectiveApi === "orpc") {
|
const includesNuxt = effectiveFrontend?.includes("nuxt");
|
||||||
|
|
||||||
|
if (includesNuxt && effectiveApi === "trpc") {
|
||||||
consola.fatal(
|
consola.fatal(
|
||||||
`oRPC API is not supported with 'native' frontend. Please use --api trpc or remove 'native' from --frontend.`,
|
`tRPC API is not supported with 'nuxt' frontend. Please use --api orpc or remove 'nuxt' from --frontend.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
includesNative &&
|
includesNuxt &&
|
||||||
effectiveApi !== "trpc" &&
|
effectiveApi !== "orpc" &&
|
||||||
(!options.api || (options.yes && options.api !== "orpc"))
|
(!options.api || (options.yes && options.api !== "trpc"))
|
||||||
) {
|
) {
|
||||||
config.api = "trpc";
|
config.api = "orpc";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.addons && config.addons.length > 0) {
|
if (config.addons && config.addons.length > 0) {
|
||||||
@@ -465,18 +469,35 @@ function processAndValidateFlags(
|
|||||||
webSpecificAddons.includes(addon),
|
webSpecificAddons.includes(addon),
|
||||||
);
|
);
|
||||||
const hasCompatibleWebFrontend = effectiveFrontend?.some(
|
const hasCompatibleWebFrontend = effectiveFrontend?.some(
|
||||||
(f) => f === "tanstack-router" || f === "react-router",
|
(f) =>
|
||||||
|
f === "tanstack-router" ||
|
||||||
|
f === "react-router" ||
|
||||||
|
(f === "nuxt" &&
|
||||||
|
config.addons?.includes("tauri") &&
|
||||||
|
!config.addons?.includes("pwa")), // Nuxt compatible with Tauri, not PWA
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
|
if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
|
||||||
|
let incompatibleAddon = "";
|
||||||
|
if (config.addons.includes("pwa") && includesNuxt) {
|
||||||
|
incompatibleAddon = "PWA addon is not compatible with Nuxt.";
|
||||||
|
} else if (
|
||||||
|
config.addons.includes("pwa") ||
|
||||||
|
config.addons.includes("tauri")
|
||||||
|
) {
|
||||||
|
incompatibleAddon =
|
||||||
|
"PWA and Tauri addons require tanstack-router, react-router, or Nuxt (Tauri only).";
|
||||||
|
}
|
||||||
consola.fatal(
|
consola.fatal(
|
||||||
"PWA and Tauri addons require tanstack-router or react-router. Cannot use these addons with your frontend selection.",
|
`${incompatibleAddon} Cannot use these addons with your frontend selection.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.addons.includes("husky") && !config.addons.includes("biome")) {
|
if (config.addons.includes("husky") && !config.addons.includes("biome")) {
|
||||||
config.addons.push("biome");
|
consola.warn(
|
||||||
|
"Husky addon is recommended to be used with Biome for lint-staged configuration.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
config.addons = [...new Set(config.addons)];
|
config.addons = [...new Set(config.addons)];
|
||||||
}
|
}
|
||||||
@@ -484,17 +505,24 @@ function processAndValidateFlags(
|
|||||||
if (config.examples && config.examples.length > 0) {
|
if (config.examples && config.examples.length > 0) {
|
||||||
if (config.examples.includes("ai") && effectiveBackend === "elysia") {
|
if (config.examples.includes("ai") && effectiveBackend === "elysia") {
|
||||||
consola.fatal(
|
consola.fatal(
|
||||||
"AI example is not compatible with Elysia backend. Cannot use --examples ai with --backend elysia",
|
"The 'ai' example is not compatible with the Elysia backend.",
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasWebFrontendForExamples = effectiveFrontend?.some((f) =>
|
const hasWebFrontendForExamples = effectiveFrontend?.some((f) =>
|
||||||
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
[
|
||||||
|
"tanstack-router",
|
||||||
|
"react-router",
|
||||||
|
"tanstack-start",
|
||||||
|
"next",
|
||||||
|
"nuxt",
|
||||||
|
].includes(f),
|
||||||
);
|
);
|
||||||
if (!hasWebFrontendForExamples) {
|
|
||||||
|
if (config.examples.length > 0 && !hasWebFrontendForExamples) {
|
||||||
consola.fatal(
|
consola.fatal(
|
||||||
"Examples require a web frontend (tanstack-router, react-router, tanstack-start, or next). Cannot use --examples with your frontend selection.",
|
"Examples require a web frontend (tanstack-router, react-router, tanstack-start, next, or nuxt).",
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -507,12 +535,8 @@ main().catch((err) => {
|
|||||||
consola.error("Aborting installation due to unexpected error...");
|
consola.error("Aborting installation due to unexpected error...");
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
consola.error(err.message);
|
consola.error(err.message);
|
||||||
console.error(err.stack);
|
|
||||||
} else {
|
} else {
|
||||||
consola.error(
|
|
||||||
"An unknown error has occurred. Please open an issue on GitHub with the below:",
|
|
||||||
);
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
process.exit(1);
|
process.exit(1); // Ensure exit on error
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export async function getApiChoice(
|
|||||||
): Promise<ProjectApi> {
|
): Promise<ProjectApi> {
|
||||||
if (Api) return Api;
|
if (Api) return Api;
|
||||||
|
|
||||||
const includesNative = frontend?.includes("native");
|
const includesNuxt = frontend?.includes("nuxt");
|
||||||
|
|
||||||
let apiOptions = [
|
let apiOptions = [
|
||||||
{
|
{
|
||||||
@@ -24,12 +24,12 @@ export async function getApiChoice(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (includesNative) {
|
if (includesNuxt) {
|
||||||
apiOptions = [
|
apiOptions = [
|
||||||
{
|
{
|
||||||
value: "trpc" as const,
|
value: "orpc" as const,
|
||||||
label: "tRPC",
|
label: "oRPC",
|
||||||
hint: "End-to-end typesafe APIs made easy (Required for Native frontend)",
|
hint: "End-to-end type-safe APIs (Required for Nuxt frontend)",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ export async function getApiChoice(
|
|||||||
const apiType = await select<ProjectApi>({
|
const apiType = await select<ProjectApi>({
|
||||||
message: "Select API type",
|
message: "Select API type",
|
||||||
options: apiOptions,
|
options: apiOptions,
|
||||||
initialValue: includesNative ? "trpc" : DEFAULT_CONFIG.api,
|
initialValue: includesNuxt ? "orpc" : DEFAULT_CONFIG.api,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCancel(apiType)) {
|
if (isCancel(apiType)) {
|
||||||
@@ -45,8 +45,8 @@ export async function getApiChoice(
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includesNative && apiType !== "trpc") {
|
if (includesNuxt && apiType !== "orpc") {
|
||||||
return "trpc";
|
return "orpc";
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiType;
|
return apiType;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { cancel, isCancel, multiselect } from "@clack/prompts";
|
|||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import { DEFAULT_CONFIG } from "../constants";
|
import { DEFAULT_CONFIG } from "../constants";
|
||||||
import type {
|
import type {
|
||||||
ProjectApi,
|
|
||||||
ProjectBackend,
|
ProjectBackend,
|
||||||
ProjectDatabase,
|
ProjectDatabase,
|
||||||
ProjectExamples,
|
ProjectExamples,
|
||||||
@@ -22,46 +21,36 @@ export async function getExamplesChoice(
|
|||||||
const hasWebFrontend =
|
const hasWebFrontend =
|
||||||
frontends?.includes("react-router") ||
|
frontends?.includes("react-router") ||
|
||||||
frontends?.includes("tanstack-router") ||
|
frontends?.includes("tanstack-router") ||
|
||||||
frontends?.includes("tanstack-start");
|
frontends?.includes("tanstack-start") ||
|
||||||
|
frontends?.includes("next") || // Added next
|
||||||
|
frontends?.includes("nuxt"); // Added nuxt
|
||||||
|
|
||||||
if (!hasWebFrontend) return [];
|
if (!hasWebFrontend) return [];
|
||||||
|
|
||||||
let response: ProjectExamples[] | symbol = [];
|
let response: ProjectExamples[] | symbol = [];
|
||||||
|
const options: { value: ProjectExamples; label: string; hint: string }[] = [
|
||||||
|
{
|
||||||
|
value: "todo" as const,
|
||||||
|
label: "Todo App",
|
||||||
|
hint: "A simple CRUD example app",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (backend === "elysia") {
|
// AI example is available for hono, express, next backends, and Nuxt (if backend is not elysia)
|
||||||
response = await multiselect<ProjectExamples>({
|
if (backend !== "elysia") {
|
||||||
message: "Include examples",
|
options.push({
|
||||||
options: [
|
value: "ai" as const,
|
||||||
{
|
label: "AI Chat",
|
||||||
value: "todo",
|
hint: "A simple AI chat interface using AI SDK",
|
||||||
label: "Todo App",
|
|
||||||
hint: "A simple CRUD example app",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
required: false,
|
|
||||||
initialValues: DEFAULT_CONFIG.examples,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backend === "hono" || backend === "express") {
|
response = await multiselect<ProjectExamples>({
|
||||||
response = await multiselect<ProjectExamples>({
|
message: "Include examples",
|
||||||
message: "Include examples",
|
options: options,
|
||||||
options: [
|
required: false,
|
||||||
{
|
initialValues: DEFAULT_CONFIG.examples,
|
||||||
value: "todo",
|
});
|
||||||
label: "Todo App",
|
|
||||||
hint: "A simple CRUD example app",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "ai",
|
|
||||||
label: "AI Chat",
|
|
||||||
hint: "A simple AI chat interface using AI SDK",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
required: false,
|
|
||||||
initialValues: DEFAULT_CONFIG.examples,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCancel(response)) {
|
if (isCancel(response)) {
|
||||||
cancel(pc.red("Operation cancelled"));
|
cancel(pc.red("Operation cancelled"));
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export async function getFrontendChoice(
|
|||||||
{
|
{
|
||||||
value: "web",
|
value: "web",
|
||||||
label: "Web",
|
label: "Web",
|
||||||
hint: "React Web Application",
|
hint: "React or Vue Web Application",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "native",
|
value: "native",
|
||||||
@@ -28,7 +28,8 @@ export async function getFrontendChoice(
|
|||||||
f === "tanstack-router" ||
|
f === "tanstack-router" ||
|
||||||
f === "react-router" ||
|
f === "react-router" ||
|
||||||
f === "tanstack-start" ||
|
f === "tanstack-start" ||
|
||||||
f === "next",
|
f === "next" ||
|
||||||
|
f === "nuxt",
|
||||||
)
|
)
|
||||||
? ["web"]
|
? ["web"]
|
||||||
: [],
|
: [],
|
||||||
@@ -60,6 +61,11 @@ export async function getFrontendChoice(
|
|||||||
label: "Next.js",
|
label: "Next.js",
|
||||||
hint: "The React Framework for the Web",
|
hint: "The React Framework for the Web",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "nuxt",
|
||||||
|
label: "Nuxt",
|
||||||
|
hint: "The Progressive Web Framework for Vue.js",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "tanstack-start",
|
value: "tanstack-start",
|
||||||
label: "TanStack Start (beta)",
|
label: "TanStack Start (beta)",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type ProjectFrontend =
|
|||||||
| "tanstack-router"
|
| "tanstack-router"
|
||||||
| "tanstack-start"
|
| "tanstack-start"
|
||||||
| "next"
|
| "next"
|
||||||
|
| "nuxt"
|
||||||
| "native"
|
| "native"
|
||||||
| "none";
|
| "none";
|
||||||
export type ProjectDBSetup =
|
export type ProjectDBSetup =
|
||||||
|
|||||||
49
apps/cli/templates/api/orpc/native/utils/orpc.ts.hbs
Normal file
49
apps/cli/templates/api/orpc/native/utils/orpc.ts.hbs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { createORPCClient } from "@orpc/client";
|
||||||
|
import { RPCLink } from "@orpc/client/fetch";
|
||||||
|
import { createORPCReactQueryUtils } from "@orpc/react-query";
|
||||||
|
import type { RouterUtils } from "@orpc/react-query";
|
||||||
|
import type { RouterClient } from "@orpc/server";
|
||||||
|
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
||||||
|
import { createContext, useContext } from "react";
|
||||||
|
import type { appRouter } from "../../server/src/routers";
|
||||||
|
{{#if auth}}
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
type ORPCReactUtils = RouterUtils<RouterClient<typeof appRouter>>;
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
queryCache: new QueryCache({
|
||||||
|
onError: (error) => {
|
||||||
|
console.log(error)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const link = new RPCLink({
|
||||||
|
url: `${process.env.EXPO_PUBLIC_SERVER_URL}/rpc`,
|
||||||
|
{{#if auth}}
|
||||||
|
headers() {
|
||||||
|
const headers = new Map<string, string>();
|
||||||
|
const cookies = authClient.getCookie();
|
||||||
|
if (cookies) {
|
||||||
|
headers.set("Cookie", cookies);
|
||||||
|
}
|
||||||
|
return Object.fromEntries(headers);
|
||||||
|
},
|
||||||
|
{{/if}}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const client: RouterClient<typeof appRouter> = createORPCClient(link);
|
||||||
|
|
||||||
|
export const orpc = createORPCReactQueryUtils(client);
|
||||||
|
|
||||||
|
export const ORPCContext = createContext<ORPCReactUtils | undefined>(undefined);
|
||||||
|
|
||||||
|
export function useORPC(): ORPCReactUtils {
|
||||||
|
const orpc = useContext(ORPCContext);
|
||||||
|
if (!orpc) {
|
||||||
|
throw new Error("ORPCContext is not set up properly");
|
||||||
|
}
|
||||||
|
return orpc;
|
||||||
|
}
|
||||||
35
apps/cli/templates/api/orpc/web/nuxt/app/plugins/orpc.ts.hbs
Normal file
35
apps/cli/templates/api/orpc/web/nuxt/app/plugins/orpc.ts.hbs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { defineNuxtPlugin, useRuntimeConfig } from '#app'
|
||||||
|
import type { RouterClient } from '@orpc/server'
|
||||||
|
import type { appRouter } from "../../../server/src/routers/index";
|
||||||
|
import { createORPCClient } from '@orpc/client'
|
||||||
|
import { RPCLink } from '@orpc/client/fetch'
|
||||||
|
import { createORPCVueQueryUtils } from '@orpc/vue-query'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const serverUrl = config.public.serverURL
|
||||||
|
|
||||||
|
const rpcUrl = `${serverUrl}/rpc`;
|
||||||
|
|
||||||
|
const rpcLink = new RPCLink({
|
||||||
|
url: rpcUrl,
|
||||||
|
{{#if auth}}
|
||||||
|
fetch(url, options) {
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{{/if}}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const client: RouterClient<typeof appRouter> = createORPCClient(rpcLink)
|
||||||
|
const orpcUtils = createORPCVueQueryUtils(client)
|
||||||
|
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
orpc: orpcUtils
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
{{#if auth}}
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
{{/if}}
|
||||||
import { QueryClient } from "@tanstack/react-query";
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
||||||
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
||||||
@@ -10,6 +12,7 @@ const trpcClient = createTRPCClient<AppRouter>({
|
|||||||
links: [
|
links: [
|
||||||
httpBatchLink({
|
httpBatchLink({
|
||||||
url: `${process.env.EXPO_PUBLIC_SERVER_URL}/trpc`,
|
url: `${process.env.EXPO_PUBLIC_SERVER_URL}/trpc`,
|
||||||
|
{{#if auth}}
|
||||||
headers() {
|
headers() {
|
||||||
const headers = new Map<string, string>();
|
const headers = new Map<string, string>();
|
||||||
const cookies = authClient.getCookie();
|
const cookies = authClient.getCookie();
|
||||||
@@ -18,6 +21,7 @@ const trpcClient = createTRPCClient<AppRouter>({
|
|||||||
}
|
}
|
||||||
return Object.fromEntries(headers);
|
return Object.fromEntries(headers);
|
||||||
},
|
},
|
||||||
|
{{/if}}
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { QueryCache, QueryClient } from '@tanstack/react-query';
|
import { QueryCache, QueryClient } from '@tanstack/react-query';
|
||||||
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
||||||
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
||||||
import type { AppRouter } from '../../../server/src/routers'; {{! Adjust path if necessary }}
|
import type { AppRouter } from '../../../server/src/routers';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
export const queryClient = new QueryClient({
|
||||||
@@ -5,11 +5,22 @@ import { ScrollView, Text, TouchableOpacity, View } from "react-native";
|
|||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import { SignIn } from "@/components/sign-in";
|
import { SignIn } from "@/components/sign-in";
|
||||||
import { SignUp } from "@/components/sign-up";
|
import { SignUp } from "@/components/sign-up";
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
import { queryClient, orpc } from "@/utils/orpc";
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
import { queryClient, trpc } from "@/utils/trpc";
|
import { queryClient, trpc } from "@/utils/trpc";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
const healthCheck = useQuery(orpc.healthCheck.queryOptions());
|
||||||
|
const privateData = useQuery(orpc.privateData.queryOptions());
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
|
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
|
||||||
const privateData = useQuery(trpc.privateData.queryOptions());
|
const privateData = useQuery(trpc.privateData.queryOptions());
|
||||||
|
{{/if}}
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
import { queryClient } from "@/utils/trpc";
|
import { queryClient } from "@/utils/trpc";
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
import { queryClient } from "@/utils/orpc";
|
||||||
|
{{/if}}
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
import { queryClient } from "@/utils/trpc";
|
import { queryClient } from "@/utils/trpc";
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
import { queryClient } from "@/utils/orpc";
|
||||||
|
{{/if}}
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
{{#if (eq orm "prisma")}}
|
{{#if (eq orm "prisma")}}
|
||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
||||||
|
{{#if (includes frontend "native")}}
|
||||||
|
import { expo } from "@better-auth/expo";
|
||||||
|
{{/if}}
|
||||||
import prisma from "../../prisma";
|
import prisma from "../../prisma";
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
@@ -9,14 +12,27 @@ export const auth = betterAuth({
|
|||||||
{{#if (eq database "sqlite")}}provider: "sqlite"{{/if}}
|
{{#if (eq database "sqlite")}}provider: "sqlite"{{/if}}
|
||||||
{{#if (eq database "mysql")}}provider: "mysql"{{/if}}
|
{{#if (eq database "mysql")}}provider: "mysql"{{/if}}
|
||||||
}),
|
}),
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
trustedOrigins: [
|
||||||
emailAndPassword: { enabled: true }
|
process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}}
|
||||||
|
"my-better-t-app://",{{/if}}
|
||||||
|
],
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
{{~#if (includes frontend "native")}}
|
||||||
|
,
|
||||||
|
plugins: [expo()]
|
||||||
|
{{/if~}}
|
||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if (eq orm "drizzle")}}
|
{{#if (eq orm "drizzle")}}
|
||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
|
{{#if (includes frontend "native")}}
|
||||||
|
import { expo } from "@better-auth/expo";
|
||||||
|
{{/if}}
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import * as schema from "../db/schema/auth";
|
import * as schema from "../db/schema/auth";
|
||||||
|
|
||||||
@@ -25,19 +41,42 @@ export const auth = betterAuth({
|
|||||||
{{#if (eq database "postgres")}}provider: "pg",{{/if}}
|
{{#if (eq database "postgres")}}provider: "pg",{{/if}}
|
||||||
{{#if (eq database "sqlite")}}provider: "sqlite",{{/if}}
|
{{#if (eq database "sqlite")}}provider: "sqlite",{{/if}}
|
||||||
{{#if (eq database "mysql")}}provider: "mysql",{{/if}}
|
{{#if (eq database "mysql")}}provider: "mysql",{{/if}}
|
||||||
schema: schema
|
schema: schema,
|
||||||
}),
|
}),
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
trustedOrigins: [
|
||||||
emailAndPassword: { enabled: true }
|
process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}}
|
||||||
|
"my-better-t-app://",{{/if}}
|
||||||
|
],
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
{{~#if (includes frontend "native")}}
|
||||||
|
,
|
||||||
|
plugins: [expo()]
|
||||||
|
{{/if~}}
|
||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if (eq orm "none")}}
|
{{#if (eq orm "none")}}
|
||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
|
{{#if (includes frontend "native")}}
|
||||||
|
import { expo } from "@better-auth/expo";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: "",
|
database: "", // Invalid configuration
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
trustedOrigins: [
|
||||||
emailAndPassword: { enabled: true }
|
process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}}
|
||||||
|
"my-better-t-app://", // Use hardcoded scheme{{/if}}
|
||||||
|
],
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
{{~#if (includes frontend "native")}}
|
||||||
|
,
|
||||||
|
plugins: [expo()]
|
||||||
|
{{/if~}}
|
||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { z } from 'zod'
|
||||||
|
// import { authClient } from "~/lib/auth-client";
|
||||||
|
const {$authClient} = useNuxtApp()
|
||||||
|
import type { FormSubmitEvent } from '#ui/types'
|
||||||
|
|
||||||
|
const emit = defineEmits(['switchToSignUp'])
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
email: z.string().email('Invalid email address'),
|
||||||
|
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||||
|
})
|
||||||
|
|
||||||
|
type Schema = z.output<typeof schema>
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
async function onSubmit (event: FormSubmitEvent<Schema>) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await $authClient.signIn.email(
|
||||||
|
{
|
||||||
|
email: event.data.email,
|
||||||
|
password: event.data.password,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.add({ title: 'Sign in successful' })
|
||||||
|
navigateTo('/dashboard', { replace: true })
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.add({ title: 'Sign in failed', description: error.error.message })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.add({ title: 'An unexpected error occurred', description: error.message || 'Please try again.' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mx-auto w-full mt-10 max-w-md p-6">
|
||||||
|
<h1 class="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
|
||||||
|
|
||||||
|
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
|
||||||
|
<UFormField label="Email" name="email">
|
||||||
|
<UInput v-model="state.email" type="email" class="w-full" />
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Password" name="password">
|
||||||
|
<UInput v-model="state.password" type="password" class="w-full" />
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UButton type="submit" block :loading="loading">
|
||||||
|
Sign In
|
||||||
|
</UButton>
|
||||||
|
</UForm>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<UButton
|
||||||
|
variant="link"
|
||||||
|
@click="$emit('switchToSignUp')"
|
||||||
|
class="text-primary hover:text-primary-dark"
|
||||||
|
>
|
||||||
|
Need an account? Sign Up
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { z } from 'zod'
|
||||||
|
import type { FormSubmitEvent } from '#ui/types'
|
||||||
|
// import { authClient } from "~/lib/auth-client";
|
||||||
|
const {$authClient} = useNuxtApp()
|
||||||
|
|
||||||
|
const emit = defineEmits(['switchToSignIn'])
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().min(2, 'Name must be at least 2 characters'),
|
||||||
|
email: z.string().email('Invalid email address'),
|
||||||
|
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||||
|
})
|
||||||
|
|
||||||
|
type Schema = z.output<typeof schema>
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
async function onSubmit (event: FormSubmitEvent<Schema>) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await $authClient.signUp.email(
|
||||||
|
{
|
||||||
|
name: event.data.name,
|
||||||
|
email: event.data.email,
|
||||||
|
password: event.data.password,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.add({ title: 'Sign up successful' })
|
||||||
|
navigateTo('/dashboard', { replace: true })
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.add({ title: 'Sign up failed', description: error.error.message })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.add({ title: 'An unexpected error occurred', description: error.message || 'Please try again.' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mx-auto w-full mt-10 max-w-md p-6">
|
||||||
|
<h1 class="mb-6 text-center text-3xl font-bold">Create Account</h1>
|
||||||
|
|
||||||
|
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
|
||||||
|
<UFormField label="Name" name="name">
|
||||||
|
<UInput v-model="state.name" class="w-full" />
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Email" name="email">
|
||||||
|
<UInput v-model="state.email" type="email" class="w-full" />
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Password" name="password">
|
||||||
|
<UInput v-model="state.password" type="password" class="w-full" />
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UButton type="submit" block :loading="loading">
|
||||||
|
Sign Up
|
||||||
|
</UButton>
|
||||||
|
</UForm>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<UButton
|
||||||
|
variant="link"
|
||||||
|
@click="$emit('switchToSignIn')"
|
||||||
|
class="text-primary hover:text-primary-dark"
|
||||||
|
>
|
||||||
|
Already have an account? Sign In
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
43
apps/cli/templates/auth/web/nuxt/app/components/UserMenu.vue
Normal file
43
apps/cli/templates/auth/web/nuxt/app/components/UserMenu.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
// import { authClient } from "~/lib/auth-client";
|
||||||
|
const {$authClient} = useNuxtApp()
|
||||||
|
const session = $authClient.useSession()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
try {
|
||||||
|
await $authClient.signOut({
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess: async () => {
|
||||||
|
toast.add({ title: 'Signed out successfully' })
|
||||||
|
await navigateTo('/', { replace: true, external: true })
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.add({ title: 'Sign out failed', description: error?.error?.message || 'Unknown error'})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.add({ title: 'An unexpected error occurred during sign out', description: error.message || 'Please try again.'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<USkeleton v-if="session.isPending" class="h-9 w-24" />
|
||||||
|
|
||||||
|
<UButton v-else-if="!session.data" variant="outline" to="/login">
|
||||||
|
Sign In
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
v-else
|
||||||
|
variant="solid"
|
||||||
|
icon="i-lucide-log-out"
|
||||||
|
label="Sign out"
|
||||||
|
@click="handleSignOut()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
12
apps/cli/templates/auth/web/nuxt/app/middleware/auth.ts
Normal file
12
apps/cli/templates/auth/web/nuxt/app/middleware/auth.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
if (import.meta.server) return
|
||||||
|
|
||||||
|
const { $authClient } = useNuxtApp()
|
||||||
|
const session = $authClient.useSession()
|
||||||
|
|
||||||
|
if (session.value.isPending || !session.value) {
|
||||||
|
if (to.path === "/dashboard") {
|
||||||
|
return navigateTo("/login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
27
apps/cli/templates/auth/web/nuxt/app/pages/dashboard.vue
Normal file
27
apps/cli/templates/auth/web/nuxt/app/pages/dashboard.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
const {$authClient} = useNuxtApp()
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['auth']
|
||||||
|
})
|
||||||
|
|
||||||
|
const { $orpc } = useNuxtApp()
|
||||||
|
|
||||||
|
const session = $authClient.useSession()
|
||||||
|
|
||||||
|
const privateData = useQuery($orpc.privateData.queryOptions())
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto p-4">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Dashboard</h1>
|
||||||
|
<div v-if="session?.data?.user">
|
||||||
|
<p class="mb-2">Welcome {{ session.data.user.name }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="privateData.status.value === 'pending'">Loading private data...</div>
|
||||||
|
<div v-else-if="privateData.status.value === 'error'">Error loading private data: {{ privateData.error.value?.message }}</div>
|
||||||
|
<p v-else-if="privateData.data.value">Private Data: {{ privateData.data.value.message }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
24
apps/cli/templates/auth/web/nuxt/app/pages/login.vue
Normal file
24
apps/cli/templates/auth/web/nuxt/app/pages/login.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { $authClient } = useNuxtApp();
|
||||||
|
import SignInForm from "~/components/SignInForm.vue";
|
||||||
|
import SignUpForm from "~/components/SignUpForm.vue";
|
||||||
|
|
||||||
|
const session = $authClient.useSession();
|
||||||
|
const showSignIn = ref(true);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (!session?.value.isPending && session?.value.data) {
|
||||||
|
navigateTo("/dashboard", { replace: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Loader v-if="session.isPending" />
|
||||||
|
<div v-else-if="!session.data">
|
||||||
|
<SignInForm v-if="showSignIn" @switch-to-sign-up="showSignIn = false" />
|
||||||
|
<SignUpForm v-else @switch-to-sign-in="showSignIn = true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
16
apps/cli/templates/auth/web/nuxt/app/plugins/auth-client.ts
Normal file
16
apps/cli/templates/auth/web/nuxt/app/plugins/auth-client.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { createAuthClient } from "better-auth/vue";
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(nuxtApp => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const serverUrl = config.public.serverURL
|
||||||
|
|
||||||
|
const authClient = createAuthClient({
|
||||||
|
baseURL: serverUrl
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
authClient: authClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
64
apps/cli/templates/examples/ai/web/nuxt/app/pages/ai.vue
Normal file
64
apps/cli/templates/examples/ai/web/nuxt/app/pages/ai.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, nextTick } from 'vue'
|
||||||
|
import { useChat } from '@ai-sdk/vue'
|
||||||
|
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const serverUrl = config.public.serverURL
|
||||||
|
|
||||||
|
const { messages, input, handleSubmit } = useChat({
|
||||||
|
api: `${serverUrl}/ai`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const messagesEndRef = ref<null | HTMLDivElement>(null)
|
||||||
|
|
||||||
|
watch(messages, async () => {
|
||||||
|
await nextTick()
|
||||||
|
messagesEndRef.value?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Helper: Concatenate all text parts for a message
|
||||||
|
function getMessageText(message: any) {
|
||||||
|
return message.parts
|
||||||
|
.filter((part: any) => part.type === 'text')
|
||||||
|
.map((part: any) => part.text)
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
|
||||||
|
<div class="overflow-y-auto space-y-4 pb-4">
|
||||||
|
<div v-if="messages.length === 0" class="text-center text-muted-foreground mt-8">
|
||||||
|
Ask me anything to get started!
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="message in messages"
|
||||||
|
:key="message.id"
|
||||||
|
:class="[
|
||||||
|
'p-3 rounded-lg',
|
||||||
|
message.role === 'user' ? 'bg-primary/10 ml-8' : 'bg-secondary/20 mr-8'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<p class="text-sm font-semibold mb-1">
|
||||||
|
{{ message.role === 'user' ? 'You' : 'AI Assistant' }}
|
||||||
|
</p>
|
||||||
|
<div class="whitespace-pre-wrap">{{ getMessageText(message) }}</div>
|
||||||
|
</div>
|
||||||
|
<div ref="messagesEndRef" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit" class="w-full flex items-center space-x-2 pt-2 border-t">
|
||||||
|
<UInput
|
||||||
|
name="prompt"
|
||||||
|
v-model="input"
|
||||||
|
placeholder="Type your message..."
|
||||||
|
class="flex-1"
|
||||||
|
autocomplete="off"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<UButton type="submit" color="primary" size="md" square>
|
||||||
|
<UIcon name="i-lucide-send" class="w-5 h-5" />
|
||||||
|
</UButton>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
108
apps/cli/templates/examples/todo/web/nuxt/app/pages/todos.vue
Normal file
108
apps/cli/templates/examples/todo/web/nuxt/app/pages/todos.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||||
|
|
||||||
|
const { $orpc } = useNuxtApp()
|
||||||
|
|
||||||
|
const newTodoText = ref('')
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const todos = useQuery($orpc.todo.getAll.queryOptions())
|
||||||
|
|
||||||
|
const createMutation = useMutation($orpc.todo.create.mutationOptions({
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries()
|
||||||
|
newTodoText.value = ''
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const toggleMutation = useMutation($orpc.todo.toggle.mutationOptions({
|
||||||
|
onSuccess: () => queryClient.invalidateQueries()
|
||||||
|
}))
|
||||||
|
|
||||||
|
const deleteMutation = useMutation($orpc.todo.delete.mutationOptions({
|
||||||
|
onSuccess: () => queryClient.invalidateQueries()
|
||||||
|
}))
|
||||||
|
|
||||||
|
function handleAddTodo() {
|
||||||
|
if (newTodoText.value.trim()) {
|
||||||
|
createMutation.mutate({ text: newTodoText.value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggleTodo(id: number, completed: boolean) {
|
||||||
|
toggleMutation.mutate({ id, completed: !completed })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteTodo(id: number) {
|
||||||
|
deleteMutation.mutate({ id })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mx-auto w-full max-w-md py-10">
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div class="text-xl font-bold">Todo List</div>
|
||||||
|
<div class="text-muted text-sm">Manage your tasks efficiently</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<form @submit.prevent="handleAddTodo" class="mb-6 flex items-center gap-2">
|
||||||
|
<UInput
|
||||||
|
v-model="newTodoText"
|
||||||
|
placeholder="Add a new task..."
|
||||||
|
autocomplete="off"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</UButton>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div v-if="todos.status.value === 'pending'" class="flex justify-center py-4">
|
||||||
|
<UIcon name="i-lucide-loader-2" class="animate-spin w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<p v-else-if="todos.status.value === 'error'" class="py-4 text-center text-red-500">
|
||||||
|
Error: {{ todos.error.value?.message || 'Failed to load todos' }}
|
||||||
|
</p>
|
||||||
|
<p v-else-if="todos.data.value?.length === 0" class="py-4 text-center">
|
||||||
|
No todos yet. Add one above!
|
||||||
|
</p>
|
||||||
|
<ul v-else class="space-y-2">
|
||||||
|
<li
|
||||||
|
v-for="todo in todos.data.value"
|
||||||
|
:key="todo.id"
|
||||||
|
class="flex items-center justify-between rounded-md border p-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UCheckbox
|
||||||
|
:model-value="todo.completed"
|
||||||
|
@update:model-value="() => handleToggleTodo(todo.id, todo.completed)"
|
||||||
|
:id="`todo-${todo.id}`"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
:for="`todo-${todo.id}`"
|
||||||
|
:class="{ 'line-through text-muted': todo.completed }"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
{{ todo.text }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
square
|
||||||
|
@click="handleDeleteTodo(todo.id)"
|
||||||
|
aria-label="Delete todo"
|
||||||
|
icon="i-lucide-trash-2"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1 +0,0 @@
|
|||||||
node-linker=hoisted
|
|
||||||
5
apps/cli/templates/extras/_npmrc.hbs
Normal file
5
apps/cli/templates/extras/_npmrc.hbs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node-linker=hoisted
|
||||||
|
{{#if (includes frontend "nuxt")}}
|
||||||
|
shamefully-hoist=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
{{/if}}
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { View, Text, ScrollView } from "react-native";
|
import { View, Text, ScrollView } from "react-native";
|
||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
import { orpc } from "@/utils/orpc";
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
import { trpc } from "@/utils/trpc";
|
import { trpc } from "@/utils/trpc";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
const healthCheck = useQuery(orpc.healthCheck.queryOptions());
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
|
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@@ -9,7 +9,12 @@ import {
|
|||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
import "../global.css";
|
import "../global.css";
|
||||||
|
{{#if (eq api "trpc")}}
|
||||||
import { queryClient } from "@/utils/trpc";
|
import { queryClient } from "@/utils/trpc";
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq api "orpc")}}
|
||||||
|
import { queryClient } from "@/utils/orpc";
|
||||||
|
{{/if}}
|
||||||
import { NAV_THEME } from "@/lib/constants";
|
import { NAV_THEME } from "@/lib/constants";
|
||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { useColorScheme } from "@/lib/use-color-scheme";
|
import { useColorScheme } from "@/lib/use-color-scheme";
|
||||||
@@ -56,12 +61,12 @@ export default function RootLayout() {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
|
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
|
||||||
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
|
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style=\{{ flex: 1 }}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
|
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="modal"
|
name="modal"
|
||||||
options={{ title: "Modal", presentation: "modal" }}
|
options=\{{ title: "Modal", presentation: "modal" }}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
@@ -16,9 +16,6 @@
|
|||||||
"@react-navigation/native": "^7.0.14",
|
"@react-navigation/native": "^7.0.14",
|
||||||
"@tanstack/react-form": "^1.0.5",
|
"@tanstack/react-form": "^1.0.5",
|
||||||
"@tanstack/react-query": "^5.69.2",
|
"@tanstack/react-query": "^5.69.2",
|
||||||
"@trpc/client": "^11.0.0",
|
|
||||||
"@trpc/server": "^11.0.0",
|
|
||||||
"@trpc/tanstack-react-query": "^11.0.0",
|
|
||||||
"expo": "^52.0.44",
|
"expo": "^52.0.44",
|
||||||
"expo-constants": "~17.0.8",
|
"expo-constants": "~17.0.8",
|
||||||
"expo-linking": "~7.0.5",
|
"expo-linking": "~7.0.5",
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { AppRouter } from "../../server/src/routers";
|
|
||||||
import { QueryClient } from "@tanstack/react-query";
|
|
||||||
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
||||||
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
|
||||||
|
|
||||||
export const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
const trpcClient = createTRPCClient<AppRouter>({
|
|
||||||
links: [
|
|
||||||
httpBatchLink({
|
|
||||||
url: `${process.env.EXPO_PUBLIC_SERVER_URL}/trpc`,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
|
||||||
client: trpcClient,
|
|
||||||
queryClient,
|
|
||||||
});
|
|
||||||
24
apps/cli/templates/frontend/nuxt/_gitignore
Normal file
24
apps/cli/templates/frontend/nuxt/_gitignore
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
15
apps/cli/templates/frontend/nuxt/app/app.config.ts
Normal file
15
apps/cli/templates/frontend/nuxt/app/app.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default defineAppConfig({
|
||||||
|
// https://ui.nuxt.com/getting-started/theme#design-system
|
||||||
|
ui: {
|
||||||
|
colors: {
|
||||||
|
primary: 'emerald',
|
||||||
|
neutral: 'slate',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
defaultVariants: {
|
||||||
|
// Set default button color to neutral
|
||||||
|
// color: 'neutral'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
13
apps/cli/templates/frontend/nuxt/app/app.vue
Normal file
13
apps/cli/templates/frontend/nuxt/app/app.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NuxtLoadingIndicator />
|
||||||
|
<UApp>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</UApp>
|
||||||
|
<VueQueryDevtools />
|
||||||
|
</template>
|
||||||
2
apps/cli/templates/frontend/nuxt/app/assets/css/main.css
Normal file
2
apps/cli/templates/frontend/nuxt/app/assets/css/main.css
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "@nuxt/ui";
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { USeparator } from '#components';
|
||||||
|
import ModeToggle from './ModeToggle.vue'
|
||||||
|
{{#if auth}}
|
||||||
|
import UserMenu from './UserMenu.vue'
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{ to: "/", label: "Home" },
|
||||||
|
{{#if auth}}
|
||||||
|
{ to: "/dashboard", label: "Dashboard" },
|
||||||
|
{{/if}}
|
||||||
|
{{#if (includes examples "todo")}}
|
||||||
|
{ to: "/todos", label: "Todos" },
|
||||||
|
{{/if}}
|
||||||
|
{{#if (includes examples "ai")}}
|
||||||
|
{ to: "/ai", label: "AI Chat" },
|
||||||
|
{{/if}}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-row items-center justify-between px-2 py-1">
|
||||||
|
<nav class="flex gap-4 text-lg">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="link in links"
|
||||||
|
:key="link.to"
|
||||||
|
:to="link.to"
|
||||||
|
class="text-foreground hover:text-primary"
|
||||||
|
active-class="text-primary font-semibold"
|
||||||
|
>
|
||||||
|
\{{ link.label }}
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ModeToggle />
|
||||||
|
{{#if auth}}
|
||||||
|
<UserMenu />
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<USeparator />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full items-center justify-center pt-8">
|
||||||
|
<UIcon name="i-lucide-loader-2" class="animate-spin text-2xl" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
const isDark = computed({
|
||||||
|
get () {
|
||||||
|
return colorMode.value === 'dark'
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
colorMode.preference = value ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<USwitch
|
||||||
|
v-model="isDark"
|
||||||
|
:checked-icon="isDark ? 'i-lucide-moon' : ''"
|
||||||
|
:unchecked-icon="!isDark ? 'i-lucide-sun' : ''"
|
||||||
|
class="mr-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
11
apps/cli/templates/frontend/nuxt/app/layouts/default.vue
Normal file
11
apps/cli/templates/frontend/nuxt/app/layouts/default.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="grid grid-rows-[auto_1fr] h-full">
|
||||||
|
<Header />
|
||||||
|
<main class="overflow-y-auto">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
63
apps/cli/templates/frontend/nuxt/app/pages/index.vue
Normal file
63
apps/cli/templates/frontend/nuxt/app/pages/index.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { $orpc } = useNuxtApp()
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
|
||||||
|
const TITLE_TEXT = `
|
||||||
|
██████╗ ███████╗████████╗████████╗███████╗██████╗
|
||||||
|
██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
|
||||||
|
██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
|
||||||
|
██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
|
||||||
|
██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
|
||||||
|
╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
|
||||||
|
|
||||||
|
████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
|
||||||
|
╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
|
||||||
|
██║ ███████╗ ██║ ███████║██║ █████╔╝
|
||||||
|
██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
|
||||||
|
██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
|
||||||
|
╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
|
||||||
|
`;
|
||||||
|
|
||||||
|
const healthCheck = useQuery($orpc.healthCheck.queryOptions())
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto max-w-3xl px-4 py-2">
|
||||||
|
<pre class="overflow-x-auto font-mono text-sm whitespace-pre-wrap">{{ TITLE_TEXT }}</pre>
|
||||||
|
<div class="grid gap-6 mt-4">
|
||||||
|
<section class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-2 font-medium">API Status</h2>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 rounded-full"
|
||||||
|
:class="{
|
||||||
|
'bg-yellow-500 animate-pulse': healthCheck.status.value === 'pending',
|
||||||
|
'bg-green-500': healthCheck.status.value === 'success',
|
||||||
|
'bg-red-500': healthCheck.status.value === 'error',
|
||||||
|
'bg-gray-400': healthCheck.status.value !== 'pending' &&
|
||||||
|
healthCheck.status.value !== 'success' &&
|
||||||
|
healthCheck.status.value !== 'error'
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<span class="text-sm text-muted-foreground">
|
||||||
|
<template v-if="healthCheck.status.value === 'pending'">
|
||||||
|
Checking...
|
||||||
|
</template>
|
||||||
|
<template v-else-if="healthCheck.status.value === 'success'">
|
||||||
|
Connected ({{ healthCheck.data.value }})
|
||||||
|
</template>
|
||||||
|
<template v-else-if="healthCheck.status.value === 'error'">
|
||||||
|
Error: {{ healthCheck.error.value?.message || 'Failed to connect' }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Idle
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
44
apps/cli/templates/frontend/nuxt/app/plugins/vue-query.ts
Normal file
44
apps/cli/templates/frontend/nuxt/app/plugins/vue-query.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type {
|
||||||
|
DehydratedState,
|
||||||
|
VueQueryPluginOptions,
|
||||||
|
} from '@tanstack/vue-query'
|
||||||
|
import {
|
||||||
|
dehydrate,
|
||||||
|
hydrate,
|
||||||
|
QueryCache,
|
||||||
|
QueryClient,
|
||||||
|
VueQueryPlugin,
|
||||||
|
} from '@tanstack/vue-query'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxt) => {
|
||||||
|
const vueQueryState = useState<DehydratedState | null>('vue-query')
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
queryCache: new QueryCache({
|
||||||
|
onError: (error) => {
|
||||||
|
console.log(error)
|
||||||
|
toast.add({
|
||||||
|
title: 'Error',
|
||||||
|
description: error?.message || 'An unexpected error occurred.',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const options: VueQueryPluginOptions = { queryClient }
|
||||||
|
|
||||||
|
nuxt.vueApp.use(VueQueryPlugin, options)
|
||||||
|
|
||||||
|
if (import.meta.server) {
|
||||||
|
nuxt.hooks.hook('app:rendered', () => {
|
||||||
|
vueQueryState.value = dehydrate(queryClient)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.client) {
|
||||||
|
nuxt.hooks.hook('app:created', () => {
|
||||||
|
hydrate(queryClient, vueQueryState.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
19
apps/cli/templates/frontend/nuxt/nuxt.config.ts.hbs
Normal file
19
apps/cli/templates/frontend/nuxt/nuxt.config.ts.hbs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2024-11-01',
|
||||||
|
future: {
|
||||||
|
compatibilityVersion: 4
|
||||||
|
},
|
||||||
|
devtools: { enabled: true },
|
||||||
|
modules: ['@nuxt/ui'],
|
||||||
|
css: ['~/assets/css/main.css'],
|
||||||
|
devServer: {
|
||||||
|
port: 3001
|
||||||
|
},
|
||||||
|
ssr: false,
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
serverURL: process.env.NUXT_PUBLIC_SERVER_URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
25
apps/cli/templates/frontend/nuxt/package.json
Normal file
25
apps/cli/templates/frontend/nuxt/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt/ui": "3.0.2",
|
||||||
|
"@tanstack/vue-query": "^5.74.5",
|
||||||
|
"nuxt": "^3.16.2",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.0",
|
||||||
|
"zod": "^3.24.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tanstack/vue-query-devtools": "^5.74.5",
|
||||||
|
"@iconify-json/lucide": "^1.2.38"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/cli/templates/frontend/nuxt/public/favicon.ico
Normal file
BIN
apps/cli/templates/frontend/nuxt/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
apps/cli/templates/frontend/nuxt/public/robots.txt
Normal file
2
apps/cli/templates/frontend/nuxt/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
3
apps/cli/templates/frontend/nuxt/server/tsconfig.json
Normal file
3
apps/cli/templates/frontend/nuxt/server/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../.nuxt/tsconfig.server.json"
|
||||||
|
}
|
||||||
4
apps/cli/templates/frontend/nuxt/tsconfig.json
Normal file
4
apps/cli/templates/frontend/nuxt/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user